Binary file not shown.
BIN
Binary file not shown.
@@ -20,7 +20,7 @@ jobs:
|
||||
POSTGRES_USER: investmoz_test
|
||||
POSTGRES_PASSWORD: test-only-password
|
||||
ports:
|
||||
- 5432:5432
|
||||
- 55441:5432
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U investmoz_test -d investmoz_test"
|
||||
--health-interval 10s --health-timeout 5s --health-retries 5
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
APP_ENV: testing
|
||||
DB_CONNECTION: pgsql
|
||||
DB_HOST: 127.0.0.1
|
||||
DB_PORT: 5432
|
||||
DB_PORT: 55441
|
||||
DB_DATABASE: investmoz_test
|
||||
DB_USERNAME: investmoz_test
|
||||
DB_PASSWORD: test-only-password
|
||||
@@ -71,5 +71,5 @@ jobs:
|
||||
INVESTMOZ_SCHEMA_TESTS: '1'
|
||||
run: php vendor/bin/phpunit --configuration=phpunit.investmoz.xml --do-not-cache-result
|
||||
|
||||
- name: Test frontend API contracts
|
||||
run: node --test tests/frontend/portal-api.test.mjs
|
||||
- name: Test all frontend contracts and calculators
|
||||
run: node --test tests/frontend/*.test.mjs
|
||||
|
||||
+1101
-853
File diff suppressed because it is too large
Load Diff
@@ -46,6 +46,7 @@ class CheckDocumentLibrarySchema extends Command
|
||||
foreach ($required as $table => $columns) {
|
||||
if (! Schema::hasTable($table)) {
|
||||
$missing[] = "tabela {$table}";
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ class CheckMalwareScanner extends Command
|
||||
public function handle(MalwareScanner $scanner): int
|
||||
{
|
||||
$health = $scanner->health();
|
||||
$this->line('Scanner: '.($health['engine'] ?? 'ClamAV'));
|
||||
$this->line('Scanner: '.($health['engine']));
|
||||
$this->line('Available: '.($health['available'] ? 'yes' : 'no'));
|
||||
$this->line('Engine version: '.($health['engine_version'] ?? 'unavailable'));
|
||||
$this->line('Signatures UTC: '.($health['signatures_at']?->format(DATE_ATOM) ?? 'unavailable'));
|
||||
|
||||
@@ -131,7 +131,7 @@ class ImportDocumentBatch extends Command
|
||||
}
|
||||
|
||||
try {
|
||||
$resourceId = DB::transaction(function () use ($validated, $documentId, $storageKey, $hash, $identity, $disk, $actorId): int {
|
||||
$resourceId = DB::transaction(function () use ($validated, $documentId, $storageKey, $hash, $identity, $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()) {
|
||||
|
||||
@@ -17,7 +17,7 @@ class ImportIndustrialParkCandidates extends Command
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$document = json_decode(file_get_contents(database_path('data/industrial-park-candidate-drafts.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$document = json_decode($this->readSource(database_path('data/industrial-park-candidate-drafts.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
if (($document['source_document_attached'] ?? null) !== true
|
||||
|| ($document['source_document_reconciled'] ?? null) !== true) {
|
||||
$this->error('The source document must be attached and reconciled before importing drafts. No records written.');
|
||||
@@ -106,4 +106,14 @@ class ImportIndustrialParkCandidates extends Command
|
||||
{
|
||||
return Str::upper(Str::ascii(trim($value)));
|
||||
}
|
||||
|
||||
private function readSource(string $path): string
|
||||
{
|
||||
$bytes = file_get_contents($path);
|
||||
if ($bytes === false) {
|
||||
throw new RuntimeException('Source file is unavailable: '.$path);
|
||||
}
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ class ImportInvestmentZones extends Command
|
||||
public function handle(): int
|
||||
{
|
||||
$path = database_path('data/investment-zones-initial.json');
|
||||
$document = json_decode(file_get_contents($path), true, flags: JSON_THROW_ON_ERROR);
|
||||
$document = json_decode($this->readSource($path), true, flags: JSON_THROW_ON_ERROR);
|
||||
$rows = [];
|
||||
foreach ($document['records'] as $record) {
|
||||
$existing = DB::table('investment_zones')->where('code', $record['code'])->first(['id', 'name', 'category', 'publication_status']);
|
||||
@@ -59,12 +59,12 @@ class ImportInvestmentZones extends Command
|
||||
|
||||
continue;
|
||||
}
|
||||
$record = collect($document['records'])->firstWhere('code', $row['code']);
|
||||
$record = collect(is_array($document['records'] ?? null) ? $document['records'] : [])->firstWhere('code', $row['code']);
|
||||
$requiresLegalSource = in_array($record['category'], ['zee', 'zfi'], true);
|
||||
$source = $requiresLegalSource
|
||||
? DB::table('investment_legal_sources')->where('url', $record['source']['url'])->where('diploma', $record['source']['diploma'])->first()
|
||||
: null;
|
||||
$sourceId = $requiresLegalSource ? ($source?->id ?? (string) Str::uuid()) : null;
|
||||
$sourceId = $requiresLegalSource ? ($source->id ?? (string) Str::uuid()) : null;
|
||||
if ($requiresLegalSource && ! $source) {
|
||||
DB::table('investment_legal_sources')->insert([
|
||||
'id' => $sourceId, ...$record['source'], 'validation_status' => 'pending', 'owner_id' => $actor,
|
||||
@@ -120,4 +120,14 @@ class ImportInvestmentZones extends Command
|
||||
{
|
||||
return Str::upper(Str::ascii(trim($value)));
|
||||
}
|
||||
|
||||
private function readSource(string $path): string
|
||||
{
|
||||
$bytes = file_get_contents($path);
|
||||
if ($bytes === false) {
|
||||
throw new RuntimeException('Source file is unavailable: '.$path);
|
||||
}
|
||||
|
||||
return $bytes;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +36,7 @@ class ImportLegalSourceArchive extends Command
|
||||
|
||||
continue;
|
||||
}
|
||||
$withinApprovedRoot = $realSource && collect($approvedRoots)->contains(fn (string $root) =>
|
||||
str_starts_with(strtolower($realSource), strtolower($root.DIRECTORY_SEPARATOR)));
|
||||
$withinApprovedRoot = $realSource && collect($approvedRoots)->contains(fn (string $root) => str_starts_with(strtolower($realSource), strtolower($root.DIRECTORY_SEPARATOR)));
|
||||
if (! $realSource || ! $withinApprovedRoot || ! is_file($realSource)) {
|
||||
$this->error($record['external_id'].': archive file missing or outside the approved source archive.');
|
||||
$failures++;
|
||||
@@ -83,7 +82,7 @@ class ImportLegalSourceArchive extends Command
|
||||
continue;
|
||||
}
|
||||
|
||||
$missingEvidence = collect($record['associations'])->filter(fn (array $association) => $association['target_type'] === 'requirement')
|
||||
$missingEvidence = collect(is_array($record['associations'] ?? null) ? $record['associations'] : [])->filter(fn (array $association) => $association['target_type'] === 'requirement')
|
||||
->reject(fn (array $association) => DB::table('portal_resource_links')->where('resource_id', $resource->id)
|
||||
->where('target_type', 'requirement')->where('target_key', $association['target_key'])->exists())->values();
|
||||
if ($apply && $missingEvidence->isNotEmpty()) {
|
||||
@@ -110,6 +109,7 @@ class ImportLegalSourceArchive extends Command
|
||||
}
|
||||
|
||||
$this->line($record['external_id'].' | reused/reconciled | document='.$existing->id.' | published='.($resource->published ? 'yes' : 'no'));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -263,13 +263,13 @@ class ImportLegalSourceArchive extends Command
|
||||
...array_map(fn (array $item) => ['target_type' => 'requirement', 'target_key' => $item[0], 'relationship' => 'cites',
|
||||
'applicability' => 'evidence', 'review_status' => 'confirmed', 'condition_text' => 'Citação localizada no texto primário; não confirma interpretação, vigência integral, elegibilidade nem regra executável.',
|
||||
'basis_reference' => $item[1].' — '.$item[2].' BR n.º 248/2025, Suplemento n.º 1, Lei de 29-12-2025.'], [
|
||||
['fiscal.ice.lei_7_2025', 'Lei n.º 7/2025 (ICE)', 'arts. 1–5 e tabela anexa; pp. 1–2. Art. 1 prorroga as taxas para 2027; art. 5 indica entrada em vigor em 01-01-2026.'],
|
||||
['fiscal.customs.lei_8_2025', 'Lei n.º 8/2025 (Pauta Aduaneira)', 'art. 1 altera as Instruções Preliminares, arts. 21(2), 22 e 27; arts. 2–5 e tabela anexa; pp. 2–7.'],
|
||||
['fiscal.ispc.lei_9_2025', 'Lei n.º 9/2025 (ISPC)', 'arts. 1–7 e republicação anexa da Lei n.º 5/2009; pp. 8–11.'],
|
||||
['fiscal.iva.lei_10_2025', 'Lei n.º 10/2025 (IVA)', 'arts. 1–4; alterações do CIVA identificadas no diploma, incluindo arts. 18, 19, 21, 23, 25-A, 27 e 32–34; pp. 12–15.'],
|
||||
['fiscal.irps.lei_11_2025', 'Lei n.º 11/2025 (IRPS)', 'arts. 1–4; alterações do CIRPS, incluindo arts. 21, 22, 48, 50, 54-A, 57, 65 e 65-A; pp. 16–18.'],
|
||||
['fiscal.irpc.lei_12_2025', 'Lei n.º 12/2025 (IRPC)', 'arts. 1–4; alterações do CIRPC, incluindo arts. 3, 5, 20, 62, 67, 75 e aditamento do art. 61-A; pp. 19–20.'],
|
||||
]),
|
||||
['fiscal.ice.lei_7_2025', 'Lei n.º 7/2025 (ICE)', 'arts. 1–5 e tabela anexa; pp. 1–2. Art. 1 prorroga as taxas para 2027; art. 5 indica entrada em vigor em 01-01-2026.'],
|
||||
['fiscal.customs.lei_8_2025', 'Lei n.º 8/2025 (Pauta Aduaneira)', 'art. 1 altera as Instruções Preliminares, arts. 21(2), 22 e 27; arts. 2–5 e tabela anexa; pp. 2–7.'],
|
||||
['fiscal.ispc.lei_9_2025', 'Lei n.º 9/2025 (ISPC)', 'arts. 1–7 e republicação anexa da Lei n.º 5/2009; pp. 8–11.'],
|
||||
['fiscal.iva.lei_10_2025', 'Lei n.º 10/2025 (IVA)', 'arts. 1–4; alterações do CIVA identificadas no diploma, incluindo arts. 18, 19, 21, 23, 25-A, 27 e 32–34; pp. 12–15.'],
|
||||
['fiscal.irps.lei_11_2025', 'Lei n.º 11/2025 (IRPS)', 'arts. 1–4; alterações do CIRPS, incluindo arts. 21, 22, 48, 50, 54-A, 57, 65 e 65-A; pp. 16–18.'],
|
||||
['fiscal.irpc.lei_12_2025', 'Lei n.º 12/2025 (IRPC)', 'arts. 1–4; alterações do CIRPC, incluindo arts. 3, 5, 20, 62, 67, 75 e aditamento do art. 61-A; pp. 19–20.'],
|
||||
]),
|
||||
],
|
||||
],
|
||||
[
|
||||
@@ -310,4 +310,3 @@ class ImportLegalSourceArchive extends Command
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ class InvestmentZoneAttachmentController extends Controller
|
||||
|
||||
return DB::transaction(function () use ($request, $id, $action, $data): JsonResponse {
|
||||
$attachment = DB::table('investment_zone_attachments')->where('id', $id)->lockForUpdate()->first();
|
||||
abort_unless($attachment, 404);
|
||||
abort_unless($attachment !== null, 404);
|
||||
abort_if((int) $attachment->version_no !== (int) $data['version_no'], 409, 'O anexo foi alterado. Recarregue antes de continuar.');
|
||||
$version = (int) $attachment->version_no + 1;
|
||||
$changes = ['version_no' => $version, 'updated_at' => now()];
|
||||
@@ -137,7 +137,7 @@ class InvestmentZoneAttachmentController extends Controller
|
||||
->where('a.id', $id)->where('a.zone_id', $zoneId)->where('d.status', 'active')->whereNull('d.deleted_at')
|
||||
->where('d.malware_scan_status', 'clean')->whereColumn('d.malware_scanned_sha256', 'd.sha256')
|
||||
->first(['d.file_name', 'd.storage_key', 'd.sha256', 'd.media_type', 'd.malware_scanned_sha256']);
|
||||
abort_unless($record, 404);
|
||||
abort_unless($record !== null, 404);
|
||||
|
||||
return $this->verifiedDownload($zoneId, $record);
|
||||
}
|
||||
@@ -150,7 +150,7 @@ class InvestmentZoneAttachmentController extends Controller
|
||||
->where('a.review_status', 'reviewed')->where('a.rights_status', 'confirmed')->where('a.personal_data_status', 'none')
|
||||
->where('d.status', 'active')->whereNull('d.deleted_at')->where('d.malware_scan_status', 'clean')
|
||||
->whereColumn('d.malware_scanned_sha256', 'd.sha256')->first(['d.file_name', 'd.storage_key', 'd.sha256', 'd.media_type', 'd.malware_scanned_sha256']);
|
||||
abort_unless($record, 404);
|
||||
abort_unless($record !== null, 404);
|
||||
|
||||
$extension = match ($record->media_type) {
|
||||
'application/pdf' => 'pdf', 'image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp', default => 'bin'
|
||||
@@ -159,7 +159,7 @@ class InvestmentZoneAttachmentController extends Controller
|
||||
return $this->verifiedDownload($zoneId, $record, str_starts_with($record->media_type, 'image/'), 'zone-attachment-'.$id.'.'.$extension);
|
||||
}
|
||||
|
||||
private function verifiedDownload(string $zoneId, object $document, bool $inline = false, ?string $downloadName = null): BinaryFileResponse
|
||||
private function verifiedDownload(string $zoneId, \stdClass $document, bool $inline = false, ?string $downloadName = null): BinaryFileResponse
|
||||
{
|
||||
$root = realpath(Storage::disk('local')->path('investment-zones/'.$zoneId));
|
||||
$path = realpath(Storage::disk('local')->path($document->storage_key));
|
||||
@@ -195,6 +195,7 @@ class InvestmentZoneAttachmentController extends Controller
|
||||
abort_unless($expected !== null && $image[2] === $expected, 422, 'O conteúdo da imagem não corresponde ao formato declarado.');
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $metadata */
|
||||
private function event(string $attachmentId, int $actor, string $action, int $version, array $metadata = []): void
|
||||
{
|
||||
DB::table('investment_zone_attachment_events')->insert([
|
||||
|
||||
@@ -17,6 +17,7 @@ use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class InvestmentZoneController extends Controller
|
||||
{
|
||||
@@ -92,7 +93,7 @@ class InvestmentZoneController extends Controller
|
||||
$publicColumns = array_values(array_filter($publicColumns, fn ($column) => Schema::hasColumn('investment_zones', $column)));
|
||||
$rows = $zones->search($filters, $management)->orderBy('name')->orderBy('id')
|
||||
->paginate(12, $management ? ['*'] : $publicColumns)->withQueryString();
|
||||
$rows->getCollection()->transform(function ($zone) use ($management) {
|
||||
$rows->getCollection()->transform(function (InvestmentZone $zone) use ($management) {
|
||||
$territoryPath = app(InvestmentGeography::class)->path($zone->primary_territory_id, $management);
|
||||
$zone->setAttribute('province', collect($territoryPath)->firstWhere('location_type', 'province')->name ?? $zone->province);
|
||||
$zone->setAttribute('district', collect($territoryPath)->firstWhere('location_type', 'district')->name ?? $zone->district);
|
||||
@@ -110,7 +111,7 @@ class InvestmentZoneController extends Controller
|
||||
return $request->is('api/*') ? response()->json($props) : Inertia::render('investment/zones', $props);
|
||||
}
|
||||
|
||||
public function export(Request $request, InvestmentZones $zones)
|
||||
public function export(Request $request, InvestmentZones $zones): StreamedResponse
|
||||
{
|
||||
$filters = $request->validate(['category' => ['nullable', 'in:zee,zfi,industrial_park'], 'physical_category' => ['nullable', 'in:economic_zone,industrial_park,agro_industrial_park,industrial_logistics_park,industrial_free_zone,integrated_tourism_area,other,unknown'],
|
||||
'legal_regime' => ['nullable', 'in:zee,zfi,general,pending,not_applicable'], 'development_status' => ['nullable', 'in:announced,planned,in_development,partially_operational,operational,suspended,unknown'],
|
||||
@@ -123,6 +124,9 @@ class InvestmentZoneController extends Controller
|
||||
return response()->streamDownload(function () use ($query): void {
|
||||
$geography = app(InvestmentGeography::class);
|
||||
$stream = fopen('php://output', 'wb');
|
||||
if ($stream === false) {
|
||||
throw new \RuntimeException('CSV output stream unavailable.');
|
||||
}
|
||||
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, $geography): void {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Ai\EvidenceRepository;
|
||||
use App\Services\Ai\InvestorGuidance;
|
||||
use App\Services\VirtualDesk;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class InvestorGuidanceController extends Controller
|
||||
{
|
||||
public function contexts(Request $request, VirtualDesk $desk): JsonResponse
|
||||
{
|
||||
$actor = $request->user();
|
||||
$organizations = DB::table('memberships')->where('user_id', $actor->id)->whereNull('deleted_at')->pluck('organization_id');
|
||||
$cases = DB::table('desk_cases')->whereNull('closed_at')->where(fn ($q) => $q->where(fn ($p) => $p->where('user_id', $actor->id)->whereNull('organization_id'))->orWhereIn('organization_id', $organizations))
|
||||
->orderByDesc('updated_at')->get(['id', 'title']);
|
||||
|
||||
return response()->json(['cases' => $cases]);
|
||||
}
|
||||
|
||||
public function show(Request $request, string $id, InvestorGuidance $guidance): JsonResponse
|
||||
{
|
||||
return response()->json($guidance->view($request->user(), $id));
|
||||
}
|
||||
|
||||
public function assumptions(Request $request, string $id, InvestorGuidance $guidance): JsonResponse|RedirectResponse
|
||||
{
|
||||
$guidance->assumptions($request->user(), $id, $request->all());
|
||||
|
||||
return $this->completed($request, 'Pressupostos corrigidos; os dados dos módulos de origem foram preservados.');
|
||||
}
|
||||
|
||||
public function feedback(Request $request, string $id, InvestorGuidance $guidance): JsonResponse|RedirectResponse
|
||||
{
|
||||
$guidance->feedback($request->user(), $id, $request->all());
|
||||
|
||||
return $this->completed($request, 'Revisão registada. Assumir uma tarefa continua a ser uma acção separada.');
|
||||
}
|
||||
|
||||
public function task(Request $request, string $id, InvestorGuidance $guidance): JsonResponse|RedirectResponse
|
||||
{
|
||||
$task = $guidance->acceptTask($request->user(), $id, $request->all());
|
||||
|
||||
return $request->expectsJson() ? response()->json(['id' => $task->id], 201) : back()->with('success', 'Tarefa assumida no CRM interno. Sincronização externa pendente de finalidade e autorização.');
|
||||
}
|
||||
|
||||
public function updateTask(Request $request, string $id, string $task, InvestorGuidance $guidance): JsonResponse|RedirectResponse
|
||||
{
|
||||
$guidance->updateTask($request->user(), $id, $task, $request->all());
|
||||
|
||||
return $this->completed($request, 'Resultado da tarefa registado no mesmo histórico do dossier.');
|
||||
}
|
||||
|
||||
public function search(Request $request, string $id, VirtualDesk $desk, EvidenceRepository $evidence): JsonResponse
|
||||
{
|
||||
$desk->case($request->user(), $id);
|
||||
$data = $request->validate(['query' => 'required|string|min:3|max:300']);
|
||||
$enabled = config('investmoz_ai.enabled') && Schema::hasTable('ai_controls') && DB::table('ai_controls')->where('capability', 'search')->where('suspended', false)->exists();
|
||||
$items = $enabled ? $evidence->search($data['query'], 'pt', ['public_only' => true]) : [];
|
||||
|
||||
return response()->json(['mode' => 'lexical_evidence', 'model' => null, 'status' => ! $enabled ? 'unavailable' : ($items ? 'evidence_found' : 'no_evidence'),
|
||||
'consulted' => $items, 'limitations' => ['Apenas excertos das fontes públicas autorizadas indexadas; não é uma consulta exaustiva nem validação jurídica.']]);
|
||||
}
|
||||
|
||||
private function completed(Request $request, string $message): JsonResponse|RedirectResponse
|
||||
{
|
||||
return $request->expectsJson() ? response()->json(['message' => $message]) : back()->with('success', $message);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\ScanDocumentForMalware;
|
||||
use App\Http\Requests\StoreLandAssistanceCaseRequest;
|
||||
use App\Jobs\ScanDocumentForMalware;
|
||||
use App\Models\InterestRequest;
|
||||
use App\Models\LandAssistanceCase;
|
||||
use App\Services\InterestSubmission;
|
||||
|
||||
@@ -8,10 +8,11 @@ use App\Models\Sector;
|
||||
use App\Services\IdempotencyConflict;
|
||||
use App\Services\InterestSubmission;
|
||||
use App\Services\Malware\MalwareScanner;
|
||||
use App\Services\VirtualDesk;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
@@ -47,7 +48,7 @@ class PortalServicesController extends Controller
|
||||
->where('d.malware_scan_status', 'clean')->whereColumn('d.malware_scanned_sha256', 'd.sha256');
|
||||
}
|
||||
|
||||
public function page(Request $request): Response|JsonResponse
|
||||
public function page(Request $request): Response|JsonResponse|RedirectResponse
|
||||
{
|
||||
$page = $request->route()->defaults['page'];
|
||||
if ($page === 'partnerships' && Schema::hasColumn('portal_partnerships', 'partnership_type')) {
|
||||
@@ -105,12 +106,12 @@ class PortalServicesController extends Controller
|
||||
}
|
||||
$pageResult = $query->orderByRaw("CASE WHEN r.document_type IN ('official_form','digital_adaptation','guidance_template') THEN 1 ELSE 0 END")
|
||||
->orderByDesc('r.essential')->orderBy('r.title')->select([
|
||||
'r.id', 'r.document_id', 'r.title', 'r.description', 'r.category', 'r.topic', 'r.document_type', 'r.document_number', 'r.document_date',
|
||||
'r.applicable_from', 'r.applicable_until',
|
||||
'r.source_url', 'r.language', 'r.version_label', 'r.document_group_id', 'r.territorial_scope', 'r.territory_id',
|
||||
'r.source_confirmation', 'r.legal_status', 'r.rights_status', 'r.last_reviewed_at', 'r.last_reviewed_by', 'r.independent_review', 'r.metadata_version', 'r.source_reference', 'r.essential',
|
||||
'd.file_name', 'd.media_type', 'd.size_bytes', 'd.issuer', 'd.issued_on', 'd.valid_until', 'd.version_no',
|
||||
])->paginate(20)->withQueryString();
|
||||
'r.id', 'r.document_id', 'r.title', 'r.description', 'r.category', 'r.topic', 'r.document_type', 'r.document_number', 'r.document_date',
|
||||
'r.applicable_from', 'r.applicable_until',
|
||||
'r.source_url', 'r.language', 'r.version_label', 'r.document_group_id', 'r.territorial_scope', 'r.territory_id',
|
||||
'r.source_confirmation', 'r.legal_status', 'r.rights_status', 'r.last_reviewed_at', 'r.last_reviewed_by', 'r.independent_review', 'r.metadata_version', 'r.source_reference', 'r.essential',
|
||||
'd.file_name', 'd.media_type', 'd.size_bytes', 'd.issuer', 'd.issued_on', 'd.valid_until', 'd.version_no',
|
||||
])->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($this->resourceLinkColumns());
|
||||
@@ -153,6 +154,9 @@ class PortalServicesController extends Controller
|
||||
$props['partnerships'] = $query->orderByDesc('id')->select(['id', 'title', 'sector', 'province', 'offer', 'seeking'])->paginate(20)->withQueryString();
|
||||
}
|
||||
if ($page === 'desk') {
|
||||
if (app(VirtualDesk::class)->installed()) {
|
||||
return redirect('/balcao-virtual/painel');
|
||||
}
|
||||
$props['requests'] = InterestRequest::where('metadata->portal_user_id', $request->user()->id)->latest()->limit(100)->get(['reference', 'status', 'created_at', 'metadata']);
|
||||
$props['myPartnerships'] = DB::table('portal_partnerships')->where('user_id', $request->user()->id)->latest()->limit(100)->get(['id', 'title', 'status']);
|
||||
}
|
||||
@@ -161,7 +165,7 @@ class PortalServicesController extends Controller
|
||||
return Inertia::render('portal/services', $props);
|
||||
}
|
||||
|
||||
public function recommendations(Request $request)
|
||||
public function recommendations(Request $request): JsonResponse
|
||||
{
|
||||
$data = $request->validate([
|
||||
'activity_id' => ['nullable', 'string', 'max:180'], 'province_id' => ['nullable', 'uuid'],
|
||||
@@ -199,10 +203,11 @@ class PortalServicesController extends Controller
|
||||
? 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.'),
|
||||
'applicability' => $matched?->applicability ?? 'support',
|
||||
'recommendation_reason' => $matched->condition_text ?? ('Relacionado com '.$matched?->target_type.' '.$matched?->target_key.'; confirme a aplicabilidade ao seu projecto.'),
|
||||
'applicability' => $matched->applicability ?? 'support',
|
||||
'basis_reference' => $matched?->basis_reference,
|
||||
'documentary_evidence' => $evidence->map(fn ($link) => ['key' => $link->target_key,
|
||||
'label' => config('document-requirements.catalogue.'.$link->target_key.'.label', $link->target_key),
|
||||
@@ -357,7 +362,7 @@ class PortalServicesController extends Controller
|
||||
|
||||
return Inertia::render('portal/manage', ['resources' => $resources,
|
||||
'requirementCatalogue' => $this->requirementEvidenceEnabled()
|
||||
? collect(config('document-requirements.catalogue'))->map(fn ($item, $key) => ['key' => $key, ...$item])->values()
|
||||
? collect(is_array($catalogue = config('document-requirements.catalogue', [])) ? $catalogue : [])->map(fn ($item, $key) => ['key' => $key, ...$item])->values()
|
||||
: collect(),
|
||||
'requirementAssociationsEnabled' => $this->requirementEvidenceEnabled(),
|
||||
'partnerships' => DB::table('portal_partnerships')->latest()->limit(100)->get(),
|
||||
@@ -548,10 +553,12 @@ class PortalServicesController extends Controller
|
||||
]);
|
||||
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);
|
||||
@@ -565,12 +572,14 @@ class PortalServicesController extends Controller
|
||||
|| ! 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;
|
||||
}
|
||||
|
||||
@@ -600,6 +609,7 @@ class PortalServicesController extends Controller
|
||||
->with('disclosureResults', ['published' => $published, 'skipped' => $skipped]);
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $additional */
|
||||
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,
|
||||
@@ -641,6 +651,7 @@ class PortalServicesController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
private function validateResource(Request $request): array
|
||||
{
|
||||
$data = $this->validateManagement($request, [
|
||||
@@ -691,6 +702,9 @@ class PortalServicesController extends Controller
|
||||
|
||||
/** Keep validation redirects on the management GET page even if the request
|
||||
* arrived with a stale Referer pointing at a write-only resource endpoint.
|
||||
*
|
||||
* @param array<string,mixed> $rules
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
private function validateManagement(Request $request, array $rules): array
|
||||
{
|
||||
@@ -701,6 +715,7 @@ class PortalServicesController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string,string|list<string>> $errors */
|
||||
private function managementValidationException(array $errors): ValidationException
|
||||
{
|
||||
$exception = ValidationException::withMessages($errors);
|
||||
@@ -709,6 +724,8 @@ class PortalServicesController extends Controller
|
||||
return $exception;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $data
|
||||
* @return array<string,mixed> */
|
||||
private function resourceFields(array $data, int $actor, ?string $identity, ?int $duplicate, bool $creating): array
|
||||
{
|
||||
unset($data['associations'], $data['issuer'], $data['data_classification']);
|
||||
@@ -725,6 +742,7 @@ class PortalServicesController extends Controller
|
||||
return $data;
|
||||
}
|
||||
|
||||
/** @param list<array<string,mixed>> $links */
|
||||
private function syncResourceLinks(int $resourceId, array $links): void
|
||||
{
|
||||
foreach ($links as $link) {
|
||||
@@ -741,7 +759,8 @@ class PortalServicesController extends Controller
|
||||
}
|
||||
}
|
||||
|
||||
private function assertPublicClassificationEligible(array $data, object $document): void
|
||||
/** @param array<string,mixed> $data */
|
||||
private function assertPublicClassificationEligible(array $data, \stdClass $document): void
|
||||
{
|
||||
if (($data['data_classification'] ?? $document->data_classification) !== 'public') {
|
||||
return;
|
||||
@@ -756,6 +775,7 @@ class PortalServicesController extends Controller
|
||||
'Identifique e confirme a fonte antes de classificar o ficheiro como público.');
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $data */
|
||||
private function identityKey(array $data): ?string
|
||||
{
|
||||
if (empty($data['issuer']) || empty($data['document_number'])) {
|
||||
@@ -782,6 +802,7 @@ class PortalServicesController extends Controller
|
||||
&& DB::table('migrations')->where('migration', '2026_10_06_200000_add_document_requirement_evidence_links')->exists();
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private function resourceLinkColumns(): array
|
||||
{
|
||||
$columns = ['target_type', 'target_key', 'relationship', 'applicability', 'condition_text', 'basis_reference'];
|
||||
@@ -792,9 +813,10 @@ class PortalServicesController extends Controller
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/** @return list<string> */
|
||||
private function territoryScopeIds(string $id): array
|
||||
{
|
||||
return collect(DB::select(<<<'SQL'
|
||||
$rows = DB::select(<<<'SQL'
|
||||
WITH RECURSIVE territory_tree AS (
|
||||
SELECT id FROM geo_locations WHERE id = ? AND deleted_at IS NULL AND data_classification = 'public'
|
||||
UNION ALL
|
||||
@@ -803,6 +825,15 @@ WITH RECURSIVE territory_tree AS (
|
||||
WHERE child.deleted_at IS NULL AND child.data_classification = 'public'
|
||||
)
|
||||
SELECT id FROM territory_tree
|
||||
SQL, [$id]))->pluck('id')->all() ?: [$id];
|
||||
SQL, [$id]);
|
||||
$ids = [];
|
||||
foreach ($rows as $row) {
|
||||
if (! is_string($row->id)) {
|
||||
throw new \RuntimeException('Territory identifier must be a string.');
|
||||
}
|
||||
$ids[] = $row->id;
|
||||
}
|
||||
|
||||
return $ids ?: [$id];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,29 +4,41 @@ namespace App\Http\Controllers;
|
||||
|
||||
use App\Jobs\ScanDocumentForMalware;
|
||||
use App\Models\User;
|
||||
use App\Services\Ai\InvestorGuidance;
|
||||
use App\Services\InstitutionalLinkMonitor;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\VirtualDesk;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
|
||||
class VirtualDeskController extends Controller
|
||||
{
|
||||
public function catalogue(Request $request, VirtualDesk $desk)
|
||||
public function catalogue(Request $request, VirtualDesk $desk): JsonResponse|Response
|
||||
{
|
||||
$filters = $request->validate(['q' => 'nullable|string|max:120', 'theme' => 'nullable|string|max:80', 'profile' => 'nullable|string|max:100']);
|
||||
$installed = $desk->installed();
|
||||
$services = $installed ? DB::table('desk_service_versions')->where('status', 'published')->orderBy('code')->orderByDesc('version')->get()->unique('code')->map(fn ($s) => ['id' => $s->id, 'version' => $s->version, 'definition' => json_decode($s->definition, true), 'source' => json_decode($s->source, true)])->values() : collect();
|
||||
$services = $services->filter(fn ($s) => (! ($filters['q'] ?? '') || str_contains(mb_strtolower(json_encode($s['definition'], JSON_UNESCAPED_UNICODE)), mb_strtolower($filters['q'])))
|
||||
$services = $services->filter(fn ($s) => (! ($filters['q'] ?? '') || str_contains(mb_strtolower(json_encode($s['definition'], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)), mb_strtolower($filters['q'])))
|
||||
&& (! ($filters['theme'] ?? '') || $s['definition']['theme'] === $filters['theme']) && (! ($filters['profile'] ?? '') || $s['definition']['profile'] === $filters['profile']))->values();
|
||||
$page = max(1, (int) $request->query('page', 1));
|
||||
$data = ['installed' => $installed, 'services' => $services->slice(($page - 1) * 12, 12)->values(), 'total' => $services->count(), 'page' => $page, 'filters' => $filters,
|
||||
'prepared' => $installed ? [] : config('virtual-desk.services'), 'source' => config('virtual-desk.source'), 'authenticated' => (bool) $request->user(), 'support' => config('portal.support')];
|
||||
|
||||
return $request->expectsJson() ? response()->json($data) : Inertia::render('desk/catalogue', $data);
|
||||
}
|
||||
|
||||
public function panel(Request $request, VirtualDesk $desk)
|
||||
public function panel(Request $request, VirtualDesk $desk): JsonResponse|Response
|
||||
{
|
||||
$actor = $request->user();
|
||||
$organizations = DB::table('memberships')->where('user_id', $actor->id)->whereNull('deleted_at')->pluck('organization_id');
|
||||
@@ -34,188 +46,269 @@ class VirtualDeskController extends Controller
|
||||
$queue = DB::table('desk_requests as r')->join('desk_cases as c', 'c.id', '=', 'r.case_id')->whereNotIn('r.status', ['draft', 'cancelled'])->when(! $actor->can('manage-portal'), fn ($q) => $q->where('r.analyst_id', $actor->id))
|
||||
->orderByRaw('r.due_at ASC NULLS LAST')->get(['r.id', 'r.protocol', 'r.status', 'r.version', 'r.analyst_id', 'r.due_at', 'c.title']);
|
||||
$data = ['cases' => $cases, 'queue' => $queue, 'notifications' => DB::table('desk_notifications')->where('user_id', $actor->id)->orderByDesc('id')->limit(100)->get(),
|
||||
'legacyRequests' => DB::table('interest_requests')->where('metadata->portal_user_id', $actor->id)->orderByDesc('created_at')->get(['reference', 'status', 'created_at']),
|
||||
'legacyRequests' => DB::table('interest_requests')->where('metadata->portal_user_id', $actor->id)->whereNull('deleted_at')->orderByDesc('created_at')->get(['id', 'reference', 'status', 'created_at']),
|
||||
'companies' => DB::table('investment_company_declarations')->where('user_id', $actor->id)->get(['id', 'payload'])->map(fn ($c) => ['id' => $c->id, 'name' => $desk->decode($c->payload)['name'] ?? 'Declaração empresarial']),
|
||||
'submissions' => DB::table('investment_submissions')->where('user_id', $actor->id)->get(['id', 'status']),
|
||||
'journeys' => DB::table('investor_journeys')->where('user_id', $actor->id)->whereNull('deleted_at')->get(['id', 'dossier_id', 'status']),
|
||||
'simulationDrafts' => DB::table('simulation_form_drafts')->where('owner_user_id', $actor->id)->whereNull('deleted_at')->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))->get(['id', 'version_no']),
|
||||
'guidanceTasks' => app(InvestorGuidance::class)->installed() ? DB::table('tasks as t')->join('desk_cases as c', 'c.id', '=', 't.desk_case_id')
|
||||
->whereNull('t.deleted_at')->when(! $actor->can('manage-portal'), fn ($q) => $q->where(fn ($p) => $p->where('t.assignee_user_id', $actor->id)->orWhere(fn ($s) => $s->where('c.user_id', $actor->id)->whereNull('c.organization_id'))->orWhereIn('c.organization_id', $organizations)))
|
||||
->orderByRaw('t.due_at ASC NULLS LAST')->get(['t.id', 't.title', 't.status', 't.due_at', 'c.id as case_id', 'c.title as case_title']) : [],
|
||||
'organizations' => DB::table('organizations')->whereIn('id', $organizations)->get(['id', 'name']), 'manager' => $actor->can('manage-portal'), 'states' => config('virtual-desk.request_states')];
|
||||
|
||||
return $request->expectsJson() ? response()->json($data) : Inertia::render('desk/panel', $data);
|
||||
}
|
||||
|
||||
public function createCase(Request $request, VirtualDesk $desk)
|
||||
public function createCase(Request $request, VirtualDesk $desk): JsonResponse|RedirectResponse
|
||||
{
|
||||
$case = $desk->createCase($request->user(), $request->all());
|
||||
return $request->expectsJson() ? response()->json(['id' => $case->id, 'version' => $case->version], 201) : redirect('/balcao-virtual/dossiers/'.$case->id);
|
||||
|
||||
return $request->expectsJson() ? response()->json(['id' => $case->id, 'version' => $case->version], 201) : redirect('/balcao-virtual/dossiers/'.$case->id)
|
||||
->with('success', 'Dossier guardado e disponível para continuar. Próxima ação: escolha um serviço publicado e prepare o rascunho de apoio. Nenhum pedido foi submetido.');
|
||||
}
|
||||
|
||||
public function showCase(Request $request, string $id, VirtualDesk $desk)
|
||||
public function showCase(Request $request, string $id, VirtualDesk $desk): JsonResponse|Response
|
||||
{
|
||||
$case = $desk->case($request->user(), $id); $owner = $desk->owns($request->user(), $case, false);
|
||||
$case = $desk->case($request->user(), $id);
|
||||
$owner = $desk->owns($request->user(), $case, false);
|
||||
$this->auditRead($request, 'desk_cases', $id);
|
||||
$data = ['case' => [...(array) $case, 'payload' => \Illuminate\Support\Arr::except($desk->decode($case->payload), ['_creation_hash']), 'workflow' => json_decode($case->workflow, true)],
|
||||
$data = ['case' => [...(array) $case, 'payload' => Arr::except($desk->decode($case->payload), $owner ? ['_creation_hash'] : ['_creation_hash', 'profile', 'company', 'investment', 'name', 'email']), 'workflow' => json_decode($case->workflow, true)],
|
||||
'legacyRequest' => ($case->interest_request_id ?? null) ? DB::table('interest_requests as r')->leftJoin('users as u', 'u.id', '=', 'r.assigned_to')->where('r.id', $case->interest_request_id)->whereNull('r.deleted_at')
|
||||
->when(! $request->user()->can('manage-portal'), fn ($q) => $q->where('r.metadata->portal_user_id', $request->user()->id))
|
||||
->first(['r.id', 'r.reference', 'r.status', 'r.next_action', 'r.next_action_due_at', 'u.name as responsible_name']) : null,
|
||||
'owner' => $owner, 'canWrite' => $desk->owns($request->user(), $case), 'manager' => $request->user()->can('manage-portal'),
|
||||
'requests' => DB::table('desk_requests')->where('case_id', $id)->when(! $owner && ! $request->user()->can('manage-portal'), fn ($q) => $q->where('analyst_id', $request->user()->id))->orderByDesc('updated_at')->get(['id', 'status', 'protocol', 'service_version_id', 'version']),
|
||||
'services' => DB::table('desk_service_versions')->where('status', 'published')->orderByDesc('version')->get(['id', 'code', 'version', 'definition'])->unique('code')->values()->map(fn ($s) => ['id' => $s->id, 'title' => json_decode($s->definition, true)['title'], 'version' => $s->version]),
|
||||
'documents' => $this->documents($id), 'events' => $this->events($id, $desk), 'stepStates' => config('virtual-desk.step_states'), 'states' => config('virtual-desk.request_states'),
|
||||
'links' => DB::table('desk_institutional_links')->where('status', 'active')->whereNotNull('verified_at')->get(['id', 'stage', 'entity', 'purpose', 'contact', 'verified_at']), 'source' => config('virtual-desk.source')];
|
||||
'documents' => $this->documents($id, $request->user()), 'events' => $this->events($id, $desk, $request->user()), 'stepStates' => config('virtual-desk.step_states'), 'states' => config('virtual-desk.request_states'),
|
||||
'links' => DB::table('desk_institutional_links')->get()->map(fn ($link) => [...(array) $link, 'monitor' => app(InstitutionalLinkMonitor::class)->status($link)]), 'source' => config('virtual-desk.source'), 'support' => config('portal.support'),
|
||||
'guidance' => app(InvestorGuidance::class)->installed() ? app(InvestorGuidance::class)->view($request->user(), $id) : null];
|
||||
|
||||
return $request->expectsJson() ? response()->json($data) : Inertia::render('desk/case', $data);
|
||||
}
|
||||
|
||||
private function documents(string $case)
|
||||
/** @return Collection<int,\stdClass> */
|
||||
private function documents(string $case, User $actor): Collection
|
||||
{
|
||||
return DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $case)->whereNull('d.deleted_at')
|
||||
->when(! app(VirtualDesk::class)->owns($actor, app(VirtualDesk::class)->case($actor, $case), false) && ! $actor->can('manage-portal'), fn ($q) => $q->where(fn ($p) => $p->whereNull('l.request_id')->orWhereIn('l.request_id', DB::table('desk_requests')->where('analyst_id', $actor->id)->select('id'))))
|
||||
->get(['l.id', 'l.document_id', 'l.request_id', 'l.category', 'l.stage', 'l.source_reference', 'l.valid_until', 'd.file_name', 'd.status', 'd.malware_scan_status', 'd.sha256']);
|
||||
}
|
||||
|
||||
private function events(string $case, VirtualDesk $desk)
|
||||
/** @return Collection<int,array{id:mixed,request_id:mixed,action:mixed,version:mixed,created_at:mixed,name:mixed,payload:array<string,mixed>}> */
|
||||
private function events(string $case, VirtualDesk $desk, User $actor): Collection
|
||||
{
|
||||
return DB::table('desk_events as e')->join('users as u', 'u.id', '=', 'e.actor_id')->where('e.case_id', $case)->orderBy('e.created_at')->get(['e.id', 'e.request_id', 'e.action', 'e.payload', 'e.version', 'e.created_at', 'u.name'])->map(fn ($e) => [...(array) $e, 'payload' => $desk->decode($e->payload)]);
|
||||
return DB::table('desk_events as e')->join('users as u', 'u.id', '=', 'e.actor_id')->where('e.case_id', $case)
|
||||
->when(! $desk->owns($actor, $desk->case($actor, $case), false) && ! $actor->can('manage-portal'), fn ($q) => $q->where(fn ($p) => $p->whereNull('e.request_id')->orWhereIn('e.request_id', DB::table('desk_requests')->where('analyst_id', $actor->id)->select('id'))))
|
||||
->orderBy('e.event_order')->get(['e.id', 'e.request_id', 'e.action', 'e.payload', 'e.version', 'e.created_at', 'u.name'])->map(fn ($e) => ['id' => $e->id, 'request_id' => $e->request_id, 'action' => $e->action, 'version' => $e->version, 'created_at' => $e->created_at, 'name' => $e->name, 'payload' => $desk->decode($e->payload)]);
|
||||
}
|
||||
|
||||
public function stage(Request $request, string $id, VirtualDesk $desk)
|
||||
public function stage(Request $request, string $id, VirtualDesk $desk): JsonResponse|RedirectResponse
|
||||
{
|
||||
$case = $desk->stage($request->user(), $id, $request->all());
|
||||
|
||||
return $request->expectsJson() ? response()->json(['version' => $case->version]) : back()->with('success', 'Etapa e evidência registadas.');
|
||||
}
|
||||
|
||||
public function createRequest(Request $request, string $id, VirtualDesk $desk)
|
||||
public function createRequest(Request $request, string $id, VirtualDesk $desk): JsonResponse|RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['service_version_id' => 'required|uuid', 'client_key' => 'required|uuid']);
|
||||
$row = $desk->createRequest($request->user(), $id, $data['service_version_id'], $data['client_key']);
|
||||
|
||||
return $request->expectsJson() ? response()->json(['id' => $row->id, 'version' => $row->version], 201) : redirect('/balcao-virtual/pedidos/'.$row->id);
|
||||
}
|
||||
|
||||
public function showRequest(Request $request, string $id, VirtualDesk $desk)
|
||||
public function showRequest(Request $request, string $id, VirtualDesk $desk): JsonResponse|Response
|
||||
{
|
||||
$row = $desk->request($request->user(), $id); $case = $desk->case($request->user(), $row->case_id); $payload = $desk->decode($row->payload);
|
||||
$row = $desk->request($request->user(), $id);
|
||||
$case = $desk->case($request->user(), $row->case_id);
|
||||
$payload = $desk->decode($row->payload);
|
||||
$messageKinds = [];
|
||||
foreach (DB::table('desk_events')->where('request_id', $id)->where('action', 'message_added')->get() as $event) {
|
||||
$original = $desk->decode($event->payload);
|
||||
if (isset($original['message_id'], $original['kind'])) {
|
||||
$messageKinds[$original['message_id']] = $original['kind'];
|
||||
}
|
||||
}
|
||||
$this->auditRead($request, 'desk_requests', $id);
|
||||
$data = ['request' => [...(array) $row, 'payload' => $payload, 'submitted_snapshot' => $desk->decode($row->submitted_snapshot), 'editable_fields' => json_decode($row->editable_fields ?? '[]', true)],
|
||||
'case' => ['id' => $case->id, 'title' => $case->title], 'definition' => $desk->definition($row),
|
||||
'case' => ['id' => $case->id, 'title' => $case->title], 'caseVersion' => $case->version, 'definition' => $desk->definition($row),
|
||||
'serviceSource' => json_decode(DB::table('desk_service_versions')->where('id', $row->service_version_id)->value('source'), true),
|
||||
'owner' => $desk->owns($request->user(), $case), 'manager' => $request->user()->can('manage-portal'), 'analyst' => (int) $row->analyst_id === $request->user()->id,
|
||||
'fieldsEditable' => in_array($row->status, ['draft', 'returned'], true), 'states' => config('virtual-desk.request_states'), 'transitions' => config('virtual-desk.transitions.'.$row->status, []),
|
||||
'findings' => $desk->findings($payload), 'ai' => config('virtual-desk.ai'), 'documents' => $this->documents($case->id)->filter(fn ($d) => ! $d->request_id || $d->request_id === $id)->values(),
|
||||
'messages' => DB::table('desk_messages as m')->join('users as u', 'u.id', '=', 'm.actor_id')->where('request_id', $id)->orderBy('m.created_at')->get(['m.id', 'm.body', 'm.created_at', 'u.name'])->map(fn ($m) => [...(array) $m, 'body' => \Illuminate\Support\Facades\Crypt::decryptString($m->body)]),
|
||||
'events' => $this->events($case->id, $desk)->filter(fn ($e) => $e['request_id'] === $id)->values(),
|
||||
'findings' => $desk->findings($payload), 'ai' => config('virtual-desk.ai'), 'documents' => $this->documents($case->id, $request->user())->filter(fn ($d) => ! $d->request_id || $d->request_id === $id)->values(),
|
||||
'messages' => DB::table('desk_messages as m')->join('users as u', 'u.id', '=', 'm.actor_id')->where('request_id', $id)->orderBy('m.created_at')->get(['m.id', 'm.actor_id', 'm.body', 'm.created_at', 'u.name'])->map(fn ($m) => [...(array) $m, 'body' => Crypt::decryptString($m->body),
|
||||
'kind' => $messageKinds[$m->id] ?? 'historical_unclassified']),
|
||||
'events' => $this->events($case->id, $desk, $request->user())->filter(fn ($e) => $e['request_id'] === $id)->values(),
|
||||
'revisions' => DB::table('desk_revisions')->where('request_id', $id)->orderByDesc('version')->get(['version', 'snapshot', 'created_at'])->map(fn ($r) => [...(array) $r, 'snapshot' => $desk->decode($r->snapshot)]),
|
||||
'payments' => DB::table('desk_payments')->where('request_id', $id)->get(),
|
||||
'analysts' => $request->user()->can('manage-portal') ? User::orderBy('name')->get(['id', 'name']) : [],
|
||||
'territories' => app(\App\Services\InvestmentGeography::class)->publicReferences()->orderBy('name')->get(['id', 'name', 'location_type'])];
|
||||
'territories' => app(InvestmentGeography::class)->publicReferences()->orderBy('name')->get(['id', 'name', 'location_type'])];
|
||||
|
||||
return $request->expectsJson() ? response()->json($data) : Inertia::render('desk/request', $data);
|
||||
}
|
||||
|
||||
public function save(Request $request, string $id, VirtualDesk $desk)
|
||||
public function save(Request $request, string $id, VirtualDesk $desk): JsonResponse|RedirectResponse
|
||||
{
|
||||
$row = $desk->save($request->user(), $id, $request->all(), $request->boolean('submit'));
|
||||
|
||||
return $request->expectsJson() ? response()->json(['id' => $row->id, 'version' => $row->version, 'status' => $row->status, 'protocol' => $row->protocol]) : back()->with('success', $request->boolean('submit') ? 'Pedido submetido; protocolo disponível.' : 'Rascunho guardado.');
|
||||
}
|
||||
|
||||
public function transition(Request $request, string $id, VirtualDesk $desk)
|
||||
public function transition(Request $request, string $id, VirtualDesk $desk): JsonResponse|RedirectResponse
|
||||
{
|
||||
$row = $desk->transition($request->user(), $id, $request->all());
|
||||
|
||||
return $request->expectsJson() ? response()->json(['version' => $row->version, 'status' => $row->status]) : back()->with('success', 'Decisão interna registada.');
|
||||
}
|
||||
|
||||
public function assign(Request $request, string $id, VirtualDesk $desk)
|
||||
public function assign(Request $request, string $id, VirtualDesk $desk): JsonResponse|RedirectResponse
|
||||
{
|
||||
abort_unless($request->user()->can('manage-portal'), 403);
|
||||
$data = $request->validate(['version' => 'required|integer', 'analyst_id' => 'required|integer|exists:users,id', 'reason' => 'required|string|min:3|max:3000', 'due_at' => 'nullable|date']);
|
||||
DB::transaction(function () use ($request, $id, $desk, $data) {
|
||||
$row = $desk->request($request->user(), $id, false, true); $case = $desk->case($request->user(), $row->case_id);
|
||||
$row = $desk->request($request->user(), $id, false, true);
|
||||
$case = $desk->case($request->user(), $row->case_id);
|
||||
abort_unless((int) $row->version === (int) $data['version'] && ! in_array($row->status, ['draft', 'cancelled', 'closed'], true), 409);
|
||||
$analyst = User::findOrFail($data['analyst_id']); abort_if($desk->owns($analyst, $case, false), 422, 'O proponente não pode ser o analista.');
|
||||
$analyst = User::where('id', $data['analyst_id'])->firstOrFail();
|
||||
abort_unless($analyst->hasVerifiedEmail(), 422, 'O responsável deve ter conta verificada.');
|
||||
abort_if($desk->owns($analyst, $case, false), 422, 'O proponente não pode ser o analista.');
|
||||
DB::table('desk_requests')->where('id', $id)->update(['analyst_id' => $analyst->id, 'due_at' => $data['due_at'] ?? null, 'version' => $row->version + 1, 'updated_at' => now()]);
|
||||
$desk->event($case, $request->user(), 'analyst_assigned', [...$data, 'deadline_basis' => 'internal_target_not_legal_deadline'], DB::table('desk_requests')->where('id', $id)->first());
|
||||
});
|
||||
|
||||
return $request->expectsJson() ? response()->json(['ok' => true]) : back()->with('success', 'Analista e meta interna atribuídos.');
|
||||
}
|
||||
|
||||
public function message(Request $request, string $id, VirtualDesk $desk)
|
||||
public function message(Request $request, string $id, VirtualDesk $desk): JsonResponse|RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['body' => 'required|string|min:1|max:10000', 'client_key' => 'required|uuid']);
|
||||
DB::transaction(function () use ($request, $id, $desk, $data) {
|
||||
$row = $desk->request($request->user(), $id, false, true); $case = $desk->case($request->user(), $row->case_id);
|
||||
$row = $desk->request($request->user(), $id, false, true);
|
||||
$case = $desk->case($request->user(), $row->case_id);
|
||||
abort_if(in_array($row->status, ['closed', 'cancelled'], true), 409, 'Pedido encerrado.');
|
||||
abort_unless($desk->owns($request->user(), $case) || $request->user()->can('manage-portal') || (int) $row->analyst_id === $request->user()->id, 403);
|
||||
$existing = DB::table('desk_messages')->where('request_id', $id)->where('actor_id', $request->user()->id)->where('client_key', $data['client_key'])->first();
|
||||
if ($existing) { abort_unless(\Illuminate\Support\Facades\Crypt::decryptString($existing->body) === $data['body'], 409); return; }
|
||||
DB::table('desk_messages')->insert(['id' => (string) Str::uuid(), 'request_id' => $id, 'actor_id' => $request->user()->id, 'client_key' => $data['client_key'], 'body' => \Illuminate\Support\Facades\Crypt::encryptString($data['body'])]);
|
||||
$desk->event($case, $request->user(), 'message_added', [], $row);
|
||||
if ($existing) {
|
||||
abort_unless(Crypt::decryptString($existing->body) === $data['body'], 409);
|
||||
|
||||
return;
|
||||
}
|
||||
$messageId = (string) Str::uuid();
|
||||
DB::table('desk_messages')->insert(['id' => $messageId, 'request_id' => $id, 'actor_id' => $request->user()->id, 'client_key' => $data['client_key'], 'body' => Crypt::encryptString($data['body'])]);
|
||||
$desk->event($case, $request->user(), 'message_added', ['message_id' => $messageId,
|
||||
'kind' => $desk->owns($request->user(), $case) ? 'investor_message' : 'responsible_response'], $row);
|
||||
});
|
||||
|
||||
return $request->expectsJson() ? response()->json(['ok' => true]) : back()->with('success', 'Mensagem privada guardada.');
|
||||
}
|
||||
|
||||
public function receipt(Request $request, string $id, VirtualDesk $desk)
|
||||
public function receipt(Request $request, string $id, VirtualDesk $desk): StreamedResponse
|
||||
{
|
||||
$row = $desk->request($request->user(), $id); abort_unless($row->protocol && $row->submitted_snapshot, 404);
|
||||
$row = $desk->request($request->user(), $id);
|
||||
abort_unless($row->protocol && $row->submitted_snapshot, 404);
|
||||
$snapshot = $desk->decode($row->submitted_snapshot);
|
||||
$this->auditRead($request, 'desk_requests', $id, 'receipt_downloaded');
|
||||
|
||||
return response()->streamDownload(function () use ($row, $snapshot) {
|
||||
echo 'Balcão Virtual — recibo de submissão interna'.PHP_EOL.$row->protocol.PHP_EOL.'Data: '.$row->submitted_at.PHP_EOL.'Versão: '.$row->version.PHP_EOL;
|
||||
echo json_encode($snapshot, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
||||
}, $row->protocol.'.txt', ['Content-Type' => 'text/plain; charset=utf-8', 'Cache-Control' => 'private, no-store']);
|
||||
}
|
||||
|
||||
public function attach(Request $request, string $id, VirtualDesk $desk)
|
||||
public function attach(Request $request, string $id, VirtualDesk $desk): JsonResponse|RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['version' => 'required|integer', 'file' => 'nullable|required_without:document_id|file|mimetypes:application/pdf,image/png,image/jpeg,text/plain|max:10240',
|
||||
'document_id' => 'nullable|required_without:file|uuid', 'category' => 'required|string|max:80', 'stage' => 'nullable|string|max:3', 'request_id' => 'nullable|uuid', 'valid_until' => 'nullable|date_format:Y-m-d', 'source_reference' => 'required|string|max:3000', 'consent' => 'accepted']);
|
||||
$key = null; $newId = null;
|
||||
$key = null;
|
||||
$newId = null;
|
||||
try {
|
||||
DB::transaction(function () use ($request, $id, $desk, $data, &$key, &$newId) {
|
||||
$case = $desk->case($request->user(), $id, true, true); abort_unless((int) $case->version === (int) $data['version'], 409);
|
||||
if ($data['stage'] ?? null) abort_unless(array_key_exists($data['stage'], config('virtual-desk.steps')), 422);
|
||||
$case = $desk->case($request->user(), $id, true, true);
|
||||
abort_if($case->closed_at, 409, 'Dossier encerrado.');
|
||||
abort_unless((int) $case->version === (int) $data['version'], 409);
|
||||
if ($data['stage'] ?? null) {
|
||||
abort_unless(array_key_exists($data['stage'], config('virtual-desk.steps')), 422);
|
||||
}
|
||||
if ($data['request_id'] ?? null) {
|
||||
$row = $desk->request($request->user(), $data['request_id'], true, true);
|
||||
abort_unless($row->case_id === $id && in_array($row->status, ['draft', 'returned'], true), 409, 'Anexos de pedidos submetidos são imutáveis; use a devolução para corrigir.');
|
||||
}
|
||||
abort_if(DB::table('desk_documents')->where('case_id', $id)->count() >= 100, 422, 'Limite de anexos do dossier atingido.');
|
||||
if ($request->hasFile('file')) {
|
||||
$file = $request->file('file'); $hash = hash_file('sha256', $file->getRealPath());
|
||||
$file = $request->file('file');
|
||||
$hash = hash_file('sha256', $file->getRealPath());
|
||||
$duplicate = DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $id)->where('d.sha256', $hash)->first();
|
||||
if ($duplicate) return;
|
||||
$newId = (string) Str::uuid(); $key = $file->storeAs('virtual-desk/'.$id, $newId, 'local'); abort_unless($key, 500);
|
||||
if ($duplicate) {
|
||||
return;
|
||||
}
|
||||
$newId = (string) Str::uuid();
|
||||
$key = $file->storeAs('virtual-desk/'.$id, $newId, 'local');
|
||||
abort_unless(is_string($key) && $key !== '', 500);
|
||||
DB::table('documents')->insert(['id' => $newId, 'source_system' => 'virtual-desk', 'file_name' => basename($file->getClientOriginalName()), 'media_type' => $file->getMimeType(), 'storage_key' => $key,
|
||||
'sha256' => $hash, 'size_bytes' => $file->getSize(), 'uploaded_by' => $request->user()->id, 'status' => 'quarantined', 'data_classification' => 'confidential']);
|
||||
$documentId = $newId;
|
||||
} else {
|
||||
$documentId = $data['document_id'];
|
||||
$document = DB::table('documents')->where('id', $documentId)->whereNull('deleted_at')->first(); abort_unless($document, 404);
|
||||
$document = DB::table('documents')->where('id', $documentId)->whereNull('deleted_at')->first();
|
||||
abort_unless($document !== null, 404);
|
||||
$owned = (int) $document->uploaded_by === $request->user()->id || DB::table('desk_documents as l')->join('desk_cases as c', 'c.id', '=', 'l.case_id')->where('l.document_id', $documentId)->where('c.user_id', $request->user()->id)->whereNull('c.organization_id')->exists();
|
||||
abort_unless($owned, 403, 'Não há autorização para reutilizar este documento.');
|
||||
if (DB::table('desk_documents')->where('case_id', $id)->where('document_id', $documentId)->exists()) return;
|
||||
if (DB::table('desk_documents')->where('case_id', $id)->where('document_id', $documentId)->exists()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
DB::table('desk_documents')->insert(['id' => (string) Str::uuid(), 'case_id' => $id, 'document_id' => $documentId, 'request_id' => $data['request_id'] ?? null,
|
||||
'category' => $data['category'], 'stage' => $data['stage'] ?? null, 'valid_until' => $data['valid_until'] ?? null, 'source_reference' => $data['source_reference'], 'linked_by' => $request->user()->id]);
|
||||
DB::table('desk_cases')->where('id', $id)->update(['version' => $case->version + 1, 'updated_at' => now()]);
|
||||
$desk->event(DB::table('desk_cases')->where('id', $id)->first(), $request->user(), 'document_linked', ['document_id' => $documentId, 'source' => $data['source_reference'], 'consent' => true]);
|
||||
});
|
||||
} catch (\Throwable $e) { if ($key) Storage::disk('local')->delete($key); throw $e; }
|
||||
if ($newId) ScanDocumentForMalware::dispatch($newId)->afterCommit();
|
||||
} catch (\Throwable $e) {
|
||||
if ($key) {
|
||||
Storage::disk('local')->delete($key);
|
||||
} throw $e;
|
||||
}
|
||||
if ($newId) {
|
||||
ScanDocumentForMalware::dispatch($newId)->afterCommit();
|
||||
}
|
||||
|
||||
return $request->expectsJson() ? response()->json(['ok' => true]) : back()->with('success', 'Documento privado associado; consulte o estado da análise.');
|
||||
}
|
||||
|
||||
public function download(Request $request, string $id, string $document, VirtualDesk $desk)
|
||||
public function download(Request $request, string $id, string $document, VirtualDesk $desk): BinaryFileResponse
|
||||
{
|
||||
$desk->case($request->user(), $id);
|
||||
$doc = DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $id)->where('l.id', $document)->whereNull('d.deleted_at')->first(['d.*']);
|
||||
$doc = DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $id)->where('l.id', $document)->whereNull('d.deleted_at')->first(['d.*', 'l.request_id']);
|
||||
if ($doc && $doc->request_id) {
|
||||
$desk->request($request->user(), $doc->request_id);
|
||||
}
|
||||
abort_unless($doc && $doc->status === 'active' && $doc->malware_scan_status === 'clean' && trim($doc->sha256) === $doc->malware_scanned_sha256, 404);
|
||||
$path = realpath(Storage::disk('local')->path($doc->storage_key)); $root = realpath(Storage::disk('local')->path(''));
|
||||
abort_unless($path && $root && str_starts_with($path, $root.DIRECTORY_SEPARATOR) && is_file($path) && hash_equals(trim($doc->sha256), hash_file('sha256', $path)), 404);
|
||||
$path = realpath(Storage::disk('local')->path($doc->storage_key));
|
||||
$root = realpath(Storage::disk('local')->path(''));
|
||||
abort_unless($path && $root && str_starts_with($path, $root.DIRECTORY_SEPARATOR) && is_file($path) && is_string($actualHash = hash_file('sha256', $path)) && hash_equals(trim($doc->sha256), $actualHash), 404);
|
||||
$this->auditRead($request, 'documents', $doc->id, 'document_downloaded');
|
||||
|
||||
return response()->download($path, $doc->file_name, ['Cache-Control' => 'private, no-store']);
|
||||
}
|
||||
|
||||
public function notification(Request $request, int $id)
|
||||
public function notification(Request $request, int $id): RedirectResponse
|
||||
{
|
||||
DB::table('desk_notifications')->where('id', $id)->where('user_id', $request->user()->id)->update(['read_at' => now()]);
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function redirectLink(Request $request, string $id, VirtualDesk $desk)
|
||||
public function redirectLink(Request $request, string $id, VirtualDesk $desk): RedirectResponse
|
||||
{
|
||||
$case = $desk->case($request->user(), $request->validate(['case_id' => 'required|uuid'])['case_id']);
|
||||
$link = DB::table('desk_institutional_links')->where('id', $id)->where('status', 'active')->whereNotNull('verified_at')->first(); abort_unless($link, 404);
|
||||
$link = DB::table('desk_institutional_links')->where('id', $id)->first();
|
||||
if (! $link || ! app(InstitutionalLinkMonitor::class)->status($link)['available'] || $request->boolean('unavailable')) {
|
||||
$desk->event($case, $request->user(), 'institutional_link_unavailable', ['link_id' => $id, 'contact' => $link?->contact, 'scope' => 'internal_services_remain_available']);
|
||||
|
||||
return redirect('/balcao-virtual/dossiers/'.$case->id)->with('warning', 'Ligação indisponível ou por verificar. Contacto alternativo: '.($link?->contact ?: (config('portal.support.email') ?: 'utilize o pedido de apoio neste dossier')).'. Pode continuar a preparação e os serviços internos.');
|
||||
}
|
||||
abort_unless(filter_var($link->url, FILTER_VALIDATE_URL) && parse_url($link->url, PHP_URL_SCHEME) === 'https', 422);
|
||||
$desk->event($case, $request->user(), 'institutional_referral', ['link_id' => $id, 'version' => $link->version, 'purpose' => $link->purpose]);
|
||||
|
||||
// No personal data, case ID or protocol appended to the external URL.
|
||||
return redirect()->away($link->url)->withHeaders(['Referrer-Policy' => 'no-referrer']);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\VirtualDeskInstallation;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
|
||||
class VirtualDeskInstallationController extends Controller
|
||||
{
|
||||
public function reviewLink(Request $request, string $id): JsonResponse|RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['version' => 'required|integer|min:1', 'available' => 'required|boolean',
|
||||
'evidence' => 'required|string|min:10|max:3000', 'contact' => 'nullable|string|max:255']);
|
||||
DB::transaction(function () use ($request, $id, $data) {
|
||||
$link = DB::table('desk_institutional_links')->where('id', $id)->lockForUpdate()->first();
|
||||
abort_unless($link !== null, 404);
|
||||
abort_unless((int) $link->version === (int) $data['version'], 409);
|
||||
abort_unless(filter_var($link->url, FILTER_VALIDATE_URL) && parse_url($link->url, PHP_URL_SCHEME) === 'https', 422);
|
||||
DB::table('desk_institutional_links')->where('id', $id)->update(['status' => $data['available'] ? 'active' : 'unavailable',
|
||||
'contact' => $data['contact'] ?? $link->contact, 'verified_at' => now(), 'verified_by' => $request->user()->id,
|
||||
'version' => $link->version + 1, 'updated_at' => now()]);
|
||||
DB::table('audit_events')->insert(['source_system' => 'virtual-desk', 'entity_type' => 'desk_institutional_links', 'entity_id' => $id,
|
||||
'actor_user_id' => $request->user()->id, 'action' => 'institutional_link_checked',
|
||||
'after_data' => json_encode(['url' => $link->url, 'prior_version' => $link->version, ...$data, 'scope' => 'availability_only_not_legal_approval'])]);
|
||||
});
|
||||
|
||||
return $request->expectsJson() ? response()->json(['checked' => true]) : back()->with('success', 'Conferência de disponibilidade registada por sete dias.');
|
||||
}
|
||||
|
||||
public function sources(Request $request): JsonResponse|Response
|
||||
{
|
||||
$read = function (string $name): array {
|
||||
$file = storage_path('app/private/virtual-desk/'.$name.'.json');
|
||||
if (! is_file($file)) {
|
||||
return [];
|
||||
}
|
||||
$bytes = file_get_contents($file);
|
||||
abort_unless(is_string($bytes), 503);
|
||||
|
||||
return json_decode($bytes, true, 512, JSON_THROW_ON_ERROR);
|
||||
};
|
||||
$inventory = $read('source-inventory');
|
||||
$forms = $read('form-register');
|
||||
$data = ['total' => count($inventory), 'attachmentCount' => array_sum(array_map(fn ($item) => count($item['attachments'] ?? []), $inventory)),
|
||||
'forms' => array_map(fn ($item) => ['identifier' => $item['identifier'], 'sha256' => $item['sha256'], 'size' => $item['size'], 'edition' => $item['edition'],
|
||||
'provenance' => str_contains($item['path'], DIRECTORY_SEPARATOR.'Scan'.DIRECTORY_SEPARATOR) ? 'Acervo disponibilizado — digitalização' : 'Acervo disponibilizado — fonte de desenho ou template',
|
||||
'consultation' => $item['consultation'], 'legal_documentary_status' => $item['legal_documentary_status'], 'rule_use' => $item['rule_use'], 'official_confirmation' => $item['official_confirmation'], 'fields_status' => $item['fields_status']], $forms),
|
||||
'source' => config('virtual-desk.source')];
|
||||
|
||||
return $request->expectsJson() ? response()->json($data) : Inertia::render('desk/sources', $data);
|
||||
}
|
||||
|
||||
public function preview(Request $request, VirtualDeskInstallation $installation): JsonResponse|Response
|
||||
{
|
||||
$data = $installation->preview();
|
||||
|
||||
return $request->expectsJson() ? response()->json($data) : Inertia::render('desk/install', $data);
|
||||
}
|
||||
|
||||
public function apply(Request $request, VirtualDeskInstallation $installation): JsonResponse|RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['hash' => 'required|string|size:64', 'confirm' => 'accepted']);
|
||||
$installation->apply($data['hash'], $request->user());
|
||||
|
||||
return $request->expectsJson() ? response()->json(['installed' => true]) : back()->with('success', 'Instalação concluída; serviços ainda exigem publicação explícita.');
|
||||
}
|
||||
|
||||
public function publishService(Request $request): JsonResponse|RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['code' => ['required', Rule::in(array_keys(config('virtual-desk.services')))], 'confirm' => 'accepted']);
|
||||
$row = DB::transaction(function () use ($request, $data) {
|
||||
DB::select('select pg_advisory_xact_lock(261008, 2)');
|
||||
$definition = config('virtual-desk.services.'.$data['code']);
|
||||
$existing = DB::table('desk_service_versions')->where('code', $data['code'])->orderByDesc('version')->first();
|
||||
if ($existing && $existing->status === 'published' && json_decode($existing->definition, true) === $definition) {
|
||||
return $existing;
|
||||
}
|
||||
$id = (string) Str::uuid();
|
||||
DB::table('desk_service_versions')->insert(['id' => $id, 'code' => $data['code'], 'version' => ($existing->version ?? 0) + 1, 'status' => 'published',
|
||||
'definition' => json_encode($definition), 'source' => json_encode(config('virtual-desk.source')), 'author_id' => $request->user()->id,
|
||||
'published_by' => $request->user()->id, 'published_at' => now(), 'created_at' => now(), 'updated_at' => now()]);
|
||||
DB::table('audit_events')->insert(['source_system' => 'virtual-desk', 'entity_type' => 'desk_service_versions', 'entity_id' => $id, 'actor_user_id' => $request->user()->id,
|
||||
'action' => 'internal_service_published', 'after_data' => json_encode(['code' => $data['code'], 'nature' => 'digital_preparation_not_official_form'])]);
|
||||
|
||||
return DB::table('desk_service_versions')->where('id', $id)->first();
|
||||
});
|
||||
|
||||
return $request->expectsJson() ? response()->json(['id' => $row->id], 201) : back()->with('success', 'Serviço de preparação interna publicado.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Ai\InvestorGuidanceEvidence;
|
||||
use App\Services\VirtualDesk;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
/** Manual operations use the same requests, documents and encrypted audit history. */
|
||||
class VirtualDeskOperationsController extends Controller
|
||||
{
|
||||
private function result(Request $request): JsonResponse|RedirectResponse
|
||||
{
|
||||
return $request->expectsJson() ? response()->json(['ok' => true]) : back()->with('success', 'Registo interno guardado; não constitui confirmação de uma instituição externa.');
|
||||
}
|
||||
|
||||
private function reviewer(Request $request, VirtualDesk $desk, \stdClass $case, ?\stdClass $row): void
|
||||
{
|
||||
abort_unless(! $desk->owns($request->user(), $case, false) && ($request->user()->can('manage-portal') || ($row && (int) $row->analyst_id === $request->user()->id)), 403);
|
||||
}
|
||||
|
||||
private function cleanDocument(string $caseId, string $documentId, ?string $requestId): \stdClass
|
||||
{
|
||||
$doc = DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')
|
||||
->where('l.case_id', $caseId)->where('d.id', $documentId)->whereNull('d.deleted_at')
|
||||
->where(fn ($q) => $q->whereNull('l.request_id')->when($requestId, fn ($p) => $p->orWhere('l.request_id', $requestId)))
|
||||
->first(['d.*', 'l.id as link_id']);
|
||||
abort_unless($doc !== null && $doc->status === 'active' && $doc->malware_scan_status === 'clean' && trim($doc->sha256) === $doc->malware_scanned_sha256, 422, 'Documento indisponível para conferência: verifique autorização, integridade e análise antimalware.');
|
||||
$path = realpath(Storage::disk('local')->path($doc->storage_key));
|
||||
$root = realpath(Storage::disk('local')->path(''));
|
||||
abort_unless($path && $root && str_starts_with($path, $root.DIRECTORY_SEPARATOR) && is_file($path) && is_string($hash = hash_file('sha256', $path)) && hash_equals(trim($doc->sha256), $hash), 422, 'O ficheiro original ou a sua integridade não pôde ser confirmado.');
|
||||
|
||||
return $doc;
|
||||
}
|
||||
|
||||
public function documentReview(Request $request, string $id, string $document, VirtualDesk $desk): JsonResponse|RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['version' => 'required|integer|min:1', 'request_id' => 'nullable|uuid', 'outcome' => 'required|in:internal_verified,internal_issue,contradiction_identified', 'what_checked' => 'required|string|min:3|max:3000', 'evidence' => 'required|string|min:3|max:5000', 'supports' => 'nullable|array|max:4', 'supports.*' => ['string', Rule::in(InvestorGuidanceEvidence::FIELDS)]]);
|
||||
DB::transaction(function () use ($request, $id, $document, $desk, $data) {
|
||||
$case = $desk->case($request->user(), $id, false, true);
|
||||
abort_unless(! $case->closed_at && (int) $case->version === (int) $data['version'], 409);
|
||||
$link = DB::table('desk_documents')->where('case_id', $id)->where('id', $document)->first();
|
||||
abort_unless($link !== null, 404);
|
||||
$requestId = $link->request_id ?: ($data['request_id'] ?? null);
|
||||
$row = $requestId ? $desk->request($request->user(), $requestId, false, true) : null;
|
||||
abort_if($row && $row->case_id !== $id, 422);
|
||||
$this->reviewer($request, $desk, $case, $row);
|
||||
$doc = $this->cleanDocument($id, $link->document_id, $requestId);
|
||||
DB::table('desk_cases')->where('id', $id)->update(['version' => $case->version + 1, 'updated_at' => now()]);
|
||||
$desk->event(DB::table('desk_cases')->where('id', $id)->firstOrFail(), $request->user(), 'document_reviewed', [
|
||||
...$data, 'document_id' => $doc->id, 'link_id' => $link->id, 'sha256' => trim($doc->sha256), 'document_version' => (int) $doc->version_no, 'reviewer_id' => $request->user()->id,
|
||||
'scope' => 'internal_documentary_check_not_official_approval_registration_or_license',
|
||||
], $row);
|
||||
});
|
||||
|
||||
return $this->result($request);
|
||||
}
|
||||
|
||||
public function payment(Request $request, string $id, VirtualDesk $desk): JsonResponse|RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['reference' => 'required|string|max:255', 'amount' => ['required', 'regex:/^\d{1,12}(\.\d{1,2})?$/', 'numeric', 'gt:0'], 'currency' => 'required|in:MZN,USD,EUR', 'source_reference' => 'required|string|min:3|max:3000', 'document_id' => 'nullable|uuid', 'consent' => 'accepted']);
|
||||
DB::transaction(function () use ($request, $id, $desk, $data) {
|
||||
$row = $desk->request($request->user(), $id, true, true);
|
||||
$case = $desk->case($request->user(), $row->case_id);
|
||||
abort_if(in_array($row->status, ['closed', 'cancelled'], true), 409);
|
||||
if ($data['document_id'] ?? null) {
|
||||
abort_unless(DB::table('desk_documents')->where('case_id', $case->id)->where('document_id', $data['document_id'])->where(fn ($q) => $q->whereNull('request_id')->orWhere('request_id', $id))->exists(), 422);
|
||||
}
|
||||
// References are supplied by the investor; no bank reference or fee is generated.
|
||||
$existing = DB::table('desk_payments')->where('reference', $data['reference'])->first();
|
||||
if ($existing) {
|
||||
$parts = explode('.', (string) $data['amount']);
|
||||
$amount = (ltrim($parts[0], '0') ?: '0').'.'.str_pad($parts[1] ?? '', 2, '0');
|
||||
abort_unless($existing->request_id === $id && (string) $existing->amount === $amount && $existing->currency === $data['currency'] && $existing->source_reference === $data['source_reference'] && $existing->document_id === ($data['document_id'] ?? null), 409);
|
||||
|
||||
return;
|
||||
}
|
||||
$payment = (string) Str::uuid();
|
||||
DB::table('desk_payments')->insert(['id' => $payment, 'request_id' => $id, 'reference' => $data['reference'], 'amount' => $data['amount'], 'currency' => $data['currency'], 'source_reference' => $data['source_reference'], 'document_id' => $data['document_id'] ?? null, 'status' => 'pending', 'created_at' => now(), 'updated_at' => now()]);
|
||||
$desk->event($case, $request->user(), 'payment_declared', ['payment_id' => $payment, 'consent' => true, 'scope' => 'manual_declaration_no_bank_confirmation'], $row);
|
||||
});
|
||||
|
||||
return $this->result($request);
|
||||
}
|
||||
|
||||
public function paymentReview(Request $request, string $id, string $payment, VirtualDesk $desk): JsonResponse|RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['version' => 'required|integer|min:1', 'status' => 'required|in:internal_verified,internal_issue', 'reason' => 'required|string|min:3|max:3000', 'evidence' => 'required|string|min:3|max:5000']);
|
||||
DB::transaction(function () use ($request, $id, $payment, $desk, $data) {
|
||||
$row = $desk->request($request->user(), $id, false, true);
|
||||
$case = $desk->case($request->user(), $row->case_id);
|
||||
$this->reviewer($request, $desk, $case, $row);
|
||||
abort_if(in_array($row->status, ['closed', 'cancelled'], true), 409);
|
||||
$record = DB::table('desk_payments')->where('request_id', $id)->where('id', $payment)->lockForUpdate()->first();
|
||||
abort_unless($record !== null, 404);
|
||||
abort_unless((int) $record->version === (int) $data['version'], 409);
|
||||
$doc = null;
|
||||
if ($data['status'] === 'internal_verified') {
|
||||
abort_unless($record->document_id, 422, 'A conferência exige um comprovativo autorizado.');
|
||||
$doc = $this->cleanDocument($case->id, $record->document_id, $id);
|
||||
}
|
||||
DB::table('desk_payments')->where('id', $payment)->update(['status' => $data['status'], 'reason' => $data['reason'], 'verified_by' => $request->user()->id, 'version' => $record->version + 1, 'updated_at' => now()]);
|
||||
$desk->event($case, $request->user(), 'payment_reviewed', [...$data, 'payment_id' => $payment, 'document_id' => $doc?->id, 'sha256' => $doc ? trim($doc->sha256) : null, 'reviewer_id' => $request->user()->id, 'scope' => 'internal_proof_check_no_bank_confirmation'], $row);
|
||||
});
|
||||
|
||||
return $this->result($request);
|
||||
}
|
||||
|
||||
public function communication(Request $request, string $id, VirtualDesk $desk): JsonResponse|RedirectResponse
|
||||
{
|
||||
$data = $request->validate(['channel' => 'required|in:phone,email,sms,whatsapp,in_person', 'direction' => 'required|in:received,sent', 'occurred_at' => 'required|date|before_or_equal:now', 'summary' => 'required|string|min:3|max:5000', 'evidence' => 'required|string|min:3|max:3000', 'client_key' => 'required|uuid']);
|
||||
DB::transaction(function () use ($request, $id, $desk, $data) {
|
||||
$row = $desk->request($request->user(), $id, false, true);
|
||||
$case = $desk->case($request->user(), $row->case_id);
|
||||
abort_unless($desk->owns($request->user(), $case) || $request->user()->can('manage-portal') || (int) $row->analyst_id === $request->user()->id, 403);
|
||||
abort_if(in_array($row->status, ['closed', 'cancelled'], true), 409);
|
||||
foreach (DB::table('desk_events')->where('request_id', $id)->where('actor_id', $request->user()->id)->where('action', 'manual_communication_recorded')->get(['payload']) as $event) {
|
||||
$saved = $desk->decode($event->payload);
|
||||
if (($saved['client_key'] ?? null) === $data['client_key']) {
|
||||
abort_unless(array_intersect_key($saved, $data) === $data, 409);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
$desk->event($case, $request->user(), 'manual_communication_recorded', [...$data, 'scope' => 'manual_record_not_portal_delivery_confirmation'], $row);
|
||||
});
|
||||
|
||||
return $this->result($request);
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
@@ -44,7 +45,7 @@ final class EnsureSimulationRecoveryCookie
|
||||
$expiry = is_array($payload) ? ($payload['expires_at'] ?? null) : null;
|
||||
if (is_string($expiry)) {
|
||||
try {
|
||||
$secondsLeft = \Illuminate\Support\Carbon::parse($expiry)->getTimestamp() - now()->getTimestamp();
|
||||
$secondsLeft = Carbon::parse($expiry)->getTimestamp() - now()->getTimestamp();
|
||||
$minutes = max(1, (int) ceil($secondsLeft / 60));
|
||||
} catch (\Throwable) {
|
||||
// Preserve the standard cookie lifetime if the response has no valid deadline.
|
||||
|
||||
@@ -38,10 +38,11 @@ class HandleInertiaRequests extends Middleware
|
||||
return [
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'portalOperator' => config('portal.operator_name'),
|
||||
'educationEnabled' => (bool) config('education.enabled', false),
|
||||
'aiEnabled' => (bool) config('investmoz_ai.enabled', false),
|
||||
'existingBusinessEnabled' => (bool) config('existing-business.enabled', false),
|
||||
'flash' => ['success' => fn () => $request->session()->get('success')],
|
||||
'flash' => ['success' => fn () => $request->session()->get('success'), 'warning' => fn () => $request->session()->get('warning')],
|
||||
'auth' => [
|
||||
'user' => $request->user(),
|
||||
'canManagePortal' => $request->user()?->can('manage-portal') ?? false,
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\VirtualDesk;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class VirtualDeskInstalled
|
||||
{
|
||||
/** @param Closure(Request): Response $next */
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
abort_unless(app(VirtualDesk::class)->installed(), 503, 'Balcão em preparação. A instalação autenticada exige backup/restauro e QA verificados. Os pedidos existentes continuam no painel anterior.');
|
||||
|
||||
return $next($request);
|
||||
}
|
||||
}
|
||||
@@ -68,16 +68,16 @@ class StoreSimulationFormDraftRequest extends FormRequest
|
||||
'economic_snapshot.journey_type' => ['nullable', 'in:personal,small_business,company,investor'],
|
||||
'economic_snapshot.detail_level' => ['nullable', 'in:simple,advanced'],
|
||||
'economic_snapshot.business_plan_snapshot' => ['nullable', 'string', 'max:60000'],
|
||||
'economic_snapshot.financial_tools_snapshot' => ['nullable', 'string', 'max:30000'],
|
||||
'economic_snapshot.project_team_snapshot' => ['nullable', 'string', 'max:30000'],
|
||||
'economic_snapshot.project_execution_snapshot' => ['nullable', 'string', 'max:30000'],
|
||||
'economic_snapshot.project_risk_responses_snapshot' => ['nullable', 'string', 'max:30000'],
|
||||
'economic_snapshot.project_questionnaire_version' => ['nullable', 'string', 'max:100'],
|
||||
'economic_snapshot.customer_validation_status' => ['nullable', 'in:yes,no,partial,unknown,not_applicable'],
|
||||
'economic_snapshot.cost_estimate_status' => ['nullable', 'in:yes,no,partial,unknown,not_applicable'],
|
||||
'economic_snapshot.location_assessment_status' => ['nullable', 'in:yes,no,partial,unknown,not_applicable'],
|
||||
'economic_snapshot.receivables_timing_status' => ['nullable', 'in:yes,no,partial,unknown,not_applicable'],
|
||||
'economic_snapshot.funding_status' => ['nullable', 'in:confirmed,in_process,not_started,unknown,not_applicable'],
|
||||
'economic_snapshot.financial_tools_snapshot' => ['nullable', 'string', 'max:30000'],
|
||||
'economic_snapshot.project_team_snapshot' => ['nullable', 'string', 'max:30000'],
|
||||
'economic_snapshot.project_execution_snapshot' => ['nullable', 'string', 'max:30000'],
|
||||
'economic_snapshot.project_risk_responses_snapshot' => ['nullable', 'string', 'max:30000'],
|
||||
'economic_snapshot.project_questionnaire_version' => ['nullable', 'string', 'max:100'],
|
||||
'economic_snapshot.customer_validation_status' => ['nullable', 'in:yes,no,partial,unknown,not_applicable'],
|
||||
'economic_snapshot.cost_estimate_status' => ['nullable', 'in:yes,no,partial,unknown,not_applicable'],
|
||||
'economic_snapshot.location_assessment_status' => ['nullable', 'in:yes,no,partial,unknown,not_applicable'],
|
||||
'economic_snapshot.receivables_timing_status' => ['nullable', 'in:yes,no,partial,unknown,not_applicable'],
|
||||
'economic_snapshot.funding_status' => ['nullable', 'in:confirmed,in_process,not_started,unknown,not_applicable'],
|
||||
'economic_snapshot.manifestation_type' => ['nullable', 'in:investment,business'],
|
||||
'economic_snapshot.preferred_language' => ['nullable', 'in:pt-MZ,en'],
|
||||
'economic_snapshot.project_type' => ['nullable', 'in:new,expansion,acquisition,partnership,concession,supplier,buyer,other'],
|
||||
|
||||
@@ -14,7 +14,7 @@ use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Throwable;
|
||||
|
||||
class ScanDocumentForMalware implements ShouldQueue, ShouldBeUnique
|
||||
class ScanDocumentForMalware implements ShouldBeUnique, ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
@@ -31,6 +31,7 @@ class ScanDocumentForMalware implements ShouldQueue, ShouldBeUnique
|
||||
return $this->documentId;
|
||||
}
|
||||
|
||||
/** @return list<int> */
|
||||
public function backoff(): array
|
||||
{
|
||||
return [15, 60];
|
||||
@@ -38,7 +39,7 @@ class ScanDocumentForMalware implements ShouldQueue, ShouldBeUnique
|
||||
|
||||
public function handle(MalwareScanner $scanner): void
|
||||
{
|
||||
$document = DB::transaction(function (): ?object {
|
||||
$document = DB::transaction(function (): ?\stdClass {
|
||||
$row = DB::table('documents')->where('id', $this->documentId)->lockForUpdate()->first();
|
||||
if (! $row || $row->status === 'deleted' || $row->deleted_at !== null) {
|
||||
return null;
|
||||
@@ -66,8 +67,8 @@ class ScanDocumentForMalware implements ShouldQueue, ShouldBeUnique
|
||||
$disk = Storage::disk('local');
|
||||
$root = $disk->path('');
|
||||
$path = $disk->path($document->storage_key);
|
||||
$beforeHash = is_file($path) ? hash_file('sha256', $path) : false;
|
||||
if (! is_string($beforeHash) || ! hash_equals(trim($document->sha256), $beforeHash)) {
|
||||
$beforeHash = $this->fileHash($path);
|
||||
if ($beforeHash === null || ! hash_equals(trim($document->sha256), $beforeHash)) {
|
||||
$this->record($document, [
|
||||
'result' => 'inconclusive', 'reason_code' => 'file_hash_mismatch', 'engine' => 'ClamAV',
|
||||
'engine_version' => 'unknown', 'signatures_at' => null, 'duration_ms' => 0, 'detail' => null,
|
||||
@@ -78,8 +79,8 @@ class ScanDocumentForMalware implements ShouldQueue, ShouldBeUnique
|
||||
|
||||
$result = $scanner->scan($path, (int) ($document->size_bytes ?? filesize($path)), $root);
|
||||
|
||||
$afterHash = is_file($path) ? hash_file('sha256', $path) : false;
|
||||
if (! is_string($afterHash) || ! hash_equals(trim($document->sha256), $afterHash)) {
|
||||
$afterHash = $this->fileHash($path);
|
||||
if ($afterHash === null || ! hash_equals(trim($document->sha256), $afterHash)) {
|
||||
$result['result'] = 'inconclusive';
|
||||
$result['reason_code'] = 'file_changed_during_scan';
|
||||
$result['detail'] = null;
|
||||
@@ -102,7 +103,17 @@ class ScanDocumentForMalware implements ShouldQueue, ShouldBeUnique
|
||||
], now(), 'failed');
|
||||
}
|
||||
|
||||
private function record(object $document, array $result, \DateTimeInterface $startedAt, string $state): void
|
||||
private function fileHash(string $path): ?string
|
||||
{
|
||||
// Re-read the filesystem after the scanner; files may change meanwhile.
|
||||
clearstatcache(true, $path);
|
||||
$hash = is_file($path) ? hash_file('sha256', $path) : false;
|
||||
|
||||
return is_string($hash) ? $hash : null;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $result */
|
||||
private function record(\stdClass $document, array $result, \DateTimeInterface $startedAt, string $state): void
|
||||
{
|
||||
DB::transaction(function () use ($document, $result, $startedAt, $state): void {
|
||||
$locked = DB::table('documents')->where('id', $this->documentId)->lockForUpdate()->first();
|
||||
|
||||
@@ -2,7 +2,12 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/** @property string|null $lead_id */
|
||||
use Carbon\CarbonImmutable;
|
||||
|
||||
/**
|
||||
* @property string|null $lead_id
|
||||
* @property CarbonImmutable|null $due_at
|
||||
*/
|
||||
class CrmTask extends CrmRecord
|
||||
{
|
||||
protected $table = 'tasks';
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Concerns\HasUuids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/** @property array<string,mixed> $payload */
|
||||
class InvestmentSubmission extends Model
|
||||
{
|
||||
use HasUuids;
|
||||
|
||||
@@ -5,6 +5,21 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
/**
|
||||
* @property string $id
|
||||
* @property string $code
|
||||
* @property string $name
|
||||
* @property ?string $primary_territory_id
|
||||
* @property ?string $province
|
||||
* @property ?string $district
|
||||
* @property ?string $physical_category
|
||||
* @property ?string $legal_regime
|
||||
* @property ?string $development_status
|
||||
* @property ?string $availability_status
|
||||
* @property ?string $area_value
|
||||
* @property ?string $area_unit
|
||||
* @property array<int,array<string,mixed>>|null $source_evidence
|
||||
*/
|
||||
class InvestmentZone extends DomainRecord
|
||||
{
|
||||
protected $casts = ['version_no' => 'integer', 'published_at' => 'datetime', 'aliases' => 'array', 'activities' => 'array', 'resources' => 'array', 'historical_territory' => 'array', 'area_components' => 'array', 'source_evidence' => 'array', 'area_value' => 'decimal:4', 'data_as_of' => 'date', 'sources_consulted_at' => 'date'];
|
||||
|
||||
@@ -5,10 +5,10 @@ namespace App\Providers;
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\Sector;
|
||||
use App\Models\User;
|
||||
use App\Services\Mne\BusinessRepository;
|
||||
use App\Services\Mne\CompatibleBusinessRepository;
|
||||
use App\Services\Malware\ClamDScanner;
|
||||
use App\Services\Malware\MalwareScanner;
|
||||
use App\Services\Mne\BusinessRepository;
|
||||
use App\Services\Mne\CompatibleBusinessRepository;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -34,6 +34,10 @@ final class EvidenceRepository
|
||||
if (! empty($filters['nature'])) {
|
||||
$builder->where('s.nature', $filters['nature']);
|
||||
}
|
||||
if ($filters['public_only'] ?? false) {
|
||||
$builder->where('d.data_classification', 'public')->where('d.rights_status', '!=', 'restricted')
|
||||
->whereExists(fn ($q) => $q->selectRaw('1')->from('portal_resources as r')->whereColumn('r.document_id', 'd.id')->where('r.published', true)->where('r.rights_status', '!=', 'restricted'));
|
||||
}
|
||||
|
||||
return array_values($builder->orderByDesc('rank')->orderBy('c.id')->limit((int) config('investmoz_ai.retrieval_limit', 5))
|
||||
->get()->map(fn ($row) => (array) $row)->all());
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Ai;
|
||||
|
||||
use App\Models\CrmTask;
|
||||
use App\Models\InvestmentSubmission;
|
||||
use App\Models\SimulationFormDraft;
|
||||
use App\Models\User;
|
||||
use App\Services\InvestmentGeography;
|
||||
use App\Services\VirtualDesk;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
class InvestorGuidance
|
||||
{
|
||||
public function __construct(private VirtualDesk $desk, private InvestorGuidanceRules $rules) {}
|
||||
|
||||
public function installed(): bool
|
||||
{
|
||||
return Schema::hasColumn('tasks', 'desk_case_id');
|
||||
}
|
||||
|
||||
/** Reads each linked module with its own owner rule; case membership does not grant access to a private simulator. */
|
||||
/** @return array<string,mixed> */
|
||||
public function context(User $actor, \stdClass $case): array
|
||||
{
|
||||
$base = $this->desk->decode($case->payload);
|
||||
$facts = Arr::only($base['guidance']['assumptions'] ?? [], InvestorGuidanceRules::FIELDS);
|
||||
$consulted = [];
|
||||
$unavailable = [];
|
||||
$financialDraft = null;
|
||||
$checks = [];
|
||||
$revision = ['case' => (int) $case->version];
|
||||
$submission = $case->submission_id ? InvestmentSubmission::where('id', $case->submission_id)->where('user_id', $actor->id)->first() : null;
|
||||
if ($submission) {
|
||||
$input = $submission->payload;
|
||||
$mapped = ['project_title' => $input['title'] ?? $case->title, 'project_summary' => $input['objective'] ?? '',
|
||||
'activity' => $input['activity_description'] ?? '', 'schedule' => $input['schedule'] ?? ''];
|
||||
$facts = [...$mapped, ...$facts];
|
||||
$consulted[] = ['module' => 'investment_submission', 'id' => $submission->id, 'version' => $submission->version, 'date' => $submission->updated_at?->toIso8601String(), 'status' => 'investor_declaration'];
|
||||
$revision['submission'] = $submission->version;
|
||||
} elseif ($case->submission_id) {
|
||||
$unavailable[] = 'investment_submission_permission_or_availability';
|
||||
}
|
||||
if ($case->simulation_id) {
|
||||
$draft = SimulationFormDraft::where('id', $case->simulation_id)->where('owner_user_id', $actor->id)->whereNull('deleted_at')
|
||||
->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now()))->first();
|
||||
if ($draft) {
|
||||
$economic = SimulationFormDraft::decryptSnapshot($draft->economic_snapshot);
|
||||
$mapped = Arr::only($economic, InvestorGuidanceRules::FIELDS);
|
||||
$mapped['activity'] = $economic['selection_labels']['activity_id'] ?? $economic['products_services'] ?? '';
|
||||
// A market assumption is not evidence of demand.
|
||||
if (! empty($economic['market_source'])) {
|
||||
$mapped['market_evidence'] = $economic['market_source'];
|
||||
}
|
||||
$mapped['location'] = $economic['selection_labels']['province_id'] ?? '';
|
||||
$mapped['location_evidence'] = $economic['location_rationale'] ?? '';
|
||||
if (! empty($economic['skills_needed'])) {
|
||||
$mapped['resource_plan'] = $economic['skills_needed'];
|
||||
}
|
||||
$checks = $this->catalogueChecks($economic);
|
||||
$facts = [...$mapped, ...$facts];
|
||||
// Only economic fields needed by existing deterministic calculators, never personal_snapshot.
|
||||
$financialDraft = Arr::only($economic, ['business_plan_snapshot', 'financial_tools_snapshot', 'currency']);
|
||||
$consulted[] = ['module' => 'simulation_form_draft', 'id' => $draft->id, 'version' => $draft->version_no, 'hash' => $draft->snapshot_sha256,
|
||||
'date' => $draft->updated_at?->toIso8601String(), 'status' => 'declared_assumptions_and_versioned_snapshots'];
|
||||
$revision['simulation'] = [$draft->version_no, $draft->snapshot_sha256];
|
||||
} else {
|
||||
$unavailable[] = 'simulation_draft_permission_or_availability';
|
||||
}
|
||||
}
|
||||
$facts['project_title'] = $facts['project_title'] ?? $case->title;
|
||||
$evidence = app(InvestorGuidanceEvidence::class)->forCase($actor, $case, $this->desk);
|
||||
$fingerprint = hash('sha256', json_encode([InvestorGuidanceRules::VERSION, $revision, Arr::sortRecursive($facts), $checks, $evidence], JSON_THROW_ON_ERROR));
|
||||
|
||||
return ['facts' => $facts, 'consulted' => $consulted, 'unavailable' => $unavailable, 'financial_draft' => $financialDraft, 'fingerprint' => $fingerprint, 'catalogue_checks' => $checks, 'evidence' => $evidence];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $economic
|
||||
* @return list<array<string,mixed>> */
|
||||
private function catalogueChecks(array $economic): array
|
||||
{
|
||||
$checks = [];
|
||||
if (! empty($economic['activity_id'])) {
|
||||
$activity = DB::table('economic_activities as a')->join('reference_catalog_versions as v', 'v.id', '=', 'a.catalog_version_id')
|
||||
->where('a.id', $economic['activity_id'])->where('a.status', 'published')->where('v.status', 'published')
|
||||
->whereDate('a.valid_from', '<=', today())->where(fn ($q) => $q->whereNull('a.valid_to')->orWhereDate('a.valid_to', '>', today()))
|
||||
->first(['a.id', 'a.sector_id', 'a.catalog_version_id']);
|
||||
$classification = ! $activity || empty($economic['sector_id']) ? 'insufficient_information'
|
||||
: ((int) $activity->sector_id === (int) $economic['sector_id'] ? 'catalogue_consistent' : 'documented_incompatibility');
|
||||
$checks[] = ['code' => 'activity_sector', 'classification' => $classification, 'restriction' => false,
|
||||
'explanation' => match ($classification) {
|
||||
'documented_incompatibility' => 'A actividade e o sector seleccionados não correspondem no catálogo publicado consultado.',
|
||||
'catalogue_consistent' => 'Actividade e sector correspondem no catálogo consultado; isto não prova mercado ou elegibilidade.',
|
||||
default => 'Sem referência actual autorizada suficiente para verificar actividade e sector.',
|
||||
}, 'source' => $activity ? ['module' => 'economic_activities', 'id' => $activity->id, 'version_id' => $activity->catalog_version_id] : null];
|
||||
}
|
||||
$parents = ['district_id' => 'province_id', 'admin_post_id' => 'district_id', 'locality_id' => 'admin_post_id'];
|
||||
foreach ($parents as $child => $parent) {
|
||||
if (empty($economic[$child])) {
|
||||
continue;
|
||||
}
|
||||
$location = app(InvestmentGeography::class)->publicReferences()->where('id', $economic[$child])->first(['id', 'parent_id']);
|
||||
$classification = ! $location || empty($economic[$parent]) ? 'insufficient_information'
|
||||
: ($location->parent_id === $economic[$parent] ? 'catalogue_consistent' : 'documented_incompatibility');
|
||||
$checks[] = ['code' => 'territory_'.$child, 'classification' => $classification, 'restriction' => false,
|
||||
'explanation' => match ($classification) {
|
||||
'documented_incompatibility' => 'A hierarquia territorial declarada contradiz a referência publicada consultada.',
|
||||
'catalogue_consistent' => 'Hierarquia territorial coerente na referência consultada; disponibilidade e infraestrutura continuam por confirmar.',
|
||||
default => 'Referência territorial insuficiente ou indisponível para esta verificação.',
|
||||
}, 'source' => $location ? ['module' => 'geo_locations', 'id' => $location->id, 'parent_id' => $location->parent_id] : null];
|
||||
}
|
||||
|
||||
return $checks;
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function view(User $actor, string $id): array
|
||||
{
|
||||
$case = $this->desk->case($actor, $id);
|
||||
abort_unless($this->installed(), 503, 'A extensão do acompanhamento aguarda instalação autenticada.');
|
||||
$context = $this->context($actor, $case);
|
||||
$choices = [];
|
||||
foreach (DB::table('desk_events')->where('case_id', $id)->where('action', 'recommendation_reviewed')->orderBy('event_order')->get() as $event) {
|
||||
$data = $this->desk->decode($event->payload);
|
||||
$choices[$data['code']] = [...$data, 'actor_id' => $event->actor_id, 'date' => $event->created_at, 'stale' => $data['context_hash'] !== $context['fingerprint']];
|
||||
}
|
||||
$actions = ['opportunities' => '/oportunidades', 'locations' => '/oportunidades?view=map', 'partners' => '/parcerias',
|
||||
'documents' => '/documentos', 'plan' => $case->submission_id && (int) $case->user_id === $actor->id ? '/investidor/manifestacoes/'.$case->submission_id : '/balcao-virtual/dossiers/'.$id,
|
||||
'simulator' => '/simulador-fiscal', 'support' => '/balcao-virtual/dossiers/'.$id];
|
||||
// The simulator recovers its current owner-scoped draft; do not pass private payload in URLs.
|
||||
if ($case->simulation_id && $context['financial_draft'] !== null) {
|
||||
$actions['plan'] = '/simulador-fiscal';
|
||||
}
|
||||
$phases = $this->rules->evaluate($context['facts'], $context['evidence']);
|
||||
foreach ($phases as &$phase) {
|
||||
foreach ($phase['recommendations'] as &$recommendation) {
|
||||
$recommendation['review'] = $choices[$recommendation['code']] ?? null;
|
||||
$recommendation['href'] = $actions[$recommendation['action']];
|
||||
}
|
||||
unset($recommendation);
|
||||
}
|
||||
unset($phase);
|
||||
$tasks = DB::table('tasks as t')->leftJoin('users as u', 'u.id', '=', 't.assignee_user_id')->where('t.desk_case_id', $id)->whereNull('t.deleted_at')
|
||||
->when(! $this->desk->owns($actor, $case, false) && ! $actor->can('manage-portal'), fn ($q) => $q->where('t.assignee_user_id', $actor->id))
|
||||
->get(['t.id', 't.title', 't.status', 't.due_at', 't.version_no', 't.desk_request_id', 't.accepted_by', 't.accepted_at', 't.assignee_user_id', 'u.name as assignee_name', 't.external_sync_status']);
|
||||
|
||||
return ['version' => $case->version, 'rules_version' => InvestorGuidanceRules::VERSION, 'computed_at' => now()->toIso8601String(),
|
||||
...$context, 'phases' => $phases, 'tasks' => $tasks, 'actions' => $actions, 'can_write' => $this->desk->owns($actor, $case),
|
||||
'external_ai' => ['status' => 'not_configured', 'model' => null, 'consulted' => []],
|
||||
'calculator_versions' => ['business_financial_plan' => hash_file('sha256', resource_path('js/lib/business-financial-plan.ts')), 'financial_calculators' => hash_file('sha256', resource_path('js/lib/financial-calculators.ts')), 'project_dossier' => hash_file('sha256', resource_path('js/lib/project-dossier.ts'))],
|
||||
'crm' => ['internal' => 'available', 'external' => 'out_of_scope', 'reason' => 'O acompanhamento usa o CRM interno existente. A sincronização externa está fora desta fase; nenhum contacto, mensagem ou documento é transmitido.']];
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $input */
|
||||
public function assumptions(User $actor, string $id, array $input): void
|
||||
{
|
||||
$rules = ['version' => 'required|integer|min:1', 'assumptions' => ['required', 'array:'.implode(',', InvestorGuidanceRules::FIELDS)]];
|
||||
foreach (InvestorGuidanceRules::FIELDS as $field) {
|
||||
$rules['assumptions.'.$field] = in_array($field, ['expected_start_date', 'ready_date', 'funding_ready_date', 'completion_date'], true) ? 'nullable|date_format:Y-m-d' : 'nullable|string|max:3000';
|
||||
}
|
||||
$data = Validator::make($input, $rules)->validate();
|
||||
DB::transaction(function () use ($actor, $id, $data) {
|
||||
$case = $this->desk->case($actor, $id, true, true);
|
||||
abort_if($case->closed_at, 409);
|
||||
abort_unless((int) $case->version === (int) $data['version'], 409, 'Dossier alterado; recarregue antes de corrigir.');
|
||||
$base = $this->desk->decode($case->payload);
|
||||
$base['guidance']['assumptions'] = $data['assumptions'];
|
||||
DB::table('desk_cases')->where('id', $id)->update(['payload' => $this->desk->encode($base), 'version' => $case->version + 1, 'updated_at' => now()]);
|
||||
$this->desk->event(DB::table('desk_cases')->where('id', $id)->first(), $actor, 'assumptions_corrected', ['assumptions' => $data['assumptions'], 'origin' => 'investor_correction_not_official_data']);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $input */
|
||||
public function feedback(User $actor, string $id, array $input): void
|
||||
{
|
||||
$data = Validator::make($input, ['code' => 'required|string|max:100', 'context_hash' => 'required|string|size:64',
|
||||
'decision' => ['required', Rule::in(['accepted', 'rejected', 'corrected'])], 'reason' => 'required|string|min:3|max:3000'])->validate();
|
||||
DB::transaction(function () use ($actor, $id, $data) {
|
||||
$case = $this->desk->case($actor, $id, true, true);
|
||||
abort_if($case->closed_at, 409);
|
||||
$view = $this->view($actor, $id);
|
||||
abort_unless(hash_equals($view['fingerprint'], $data['context_hash']), 409, 'Pressupostos alterados; reveja a recomendação actual.');
|
||||
$recommendations = array_merge(...array_column($view['phases'], 'recommendations'));
|
||||
abort_unless(in_array($data['code'], array_column($recommendations, 'code'), true), 422);
|
||||
$this->desk->event($case, $actor, 'recommendation_reviewed', [...$data, 'rules_version' => InvestorGuidanceRules::VERSION, 'creates_task' => false]);
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $input */
|
||||
public function acceptTask(User $actor, string $id, array $input): CrmTask
|
||||
{
|
||||
$data = Validator::make($input, ['code' => 'required|string|max:100', 'context_hash' => 'required|string|size:64', 'consent' => 'accepted',
|
||||
'due_at' => 'required|date|after:now', 'request_id' => 'nullable|uuid'])->validate();
|
||||
|
||||
return DB::transaction(function () use ($actor, $id, $data) {
|
||||
$case = $this->desk->case($actor, $id, true, true);
|
||||
abort_if($case->closed_at, 409);
|
||||
$view = $this->view($actor, $id);
|
||||
abort_unless(hash_equals($view['fingerprint'], $data['context_hash']), 409);
|
||||
$recommendations = array_merge(...array_column($view['phases'], 'recommendations'));
|
||||
$suggestion = collect($recommendations)->firstWhere('code', $data['code']);
|
||||
abort_unless($suggestion && ($suggestion['review']['decision'] ?? null) === 'accepted' && ! $suggestion['review']['stale'], 422, 'Aceite a recomendação actual antes de assumir a tarefa.');
|
||||
if ($data['request_id'] ?? null) {
|
||||
$request = $this->desk->request($actor, $data['request_id'], true);
|
||||
abort_unless($request->case_id === $id, 422);
|
||||
}
|
||||
// One accepted action per suggestion, even after corrections or retries.
|
||||
$existing = CrmTask::where('desk_case_id', $id)->where('guidance_key', $data['code'])->first();
|
||||
if ($existing) {
|
||||
abort_unless($existing->getAttribute('desk_request_id') === ($data['request_id'] ?? null)
|
||||
&& (int) $existing->getAttribute('accepted_by') === $actor->id && $existing->due_at !== null && $existing->due_at->equalTo(Carbon::parse($data['due_at'])), 409, 'A tarefa já foi assumida com outros dados; reveja o registo existente.');
|
||||
|
||||
return $existing;
|
||||
}
|
||||
$task = CrmTask::create(['desk_case_id' => $id, 'desk_request_id' => $data['request_id'] ?? null, 'guidance_key' => $data['code'],
|
||||
'title' => $suggestion['explanation'], 'status' => 'open', 'due_at' => $data['due_at'], 'assignee_user_id' => $actor->id,
|
||||
'accepted_by' => $actor->id, 'accepted_at' => now(), 'external_sync_status' => 'pending_authorization',
|
||||
'source_system' => 'investor-guidance', 'data_classification' => 'confidential']);
|
||||
$this->desk->event($case, $actor, 'guidance_task_accepted', ['task_id' => $task->id, 'code' => $data['code'], 'context_hash' => $data['context_hash'], 'due_at' => $data['due_at'], 'scope' => 'internal_crm']);
|
||||
|
||||
return $task;
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $input */
|
||||
public function updateTask(User $actor, string $id, string $taskId, array $input): void
|
||||
{
|
||||
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'status' => ['required', Rule::in(['open', 'in_progress', 'done', 'cancelled'])],
|
||||
'reason' => 'required|string|min:3|max:3000', 'assignee_user_id' => 'nullable|integer|exists:users,id', 'due_at' => 'nullable|date|after:now'])->validate();
|
||||
DB::transaction(function () use ($actor, $id, $taskId, $data) {
|
||||
$case = $this->desk->case($actor, $id);
|
||||
$task = CrmTask::where('desk_case_id', $id)->whereKey($taskId)->lockForUpdate()->firstOrFail();
|
||||
abort_unless($actor->can('manage-portal') || (int) $task->getAttribute('assignee_user_id') === $actor->id, 403);
|
||||
abort_unless((int) $task->getAttribute('version_no') === (int) $data['version'], 409);
|
||||
$updates = ['status' => $data['status'], 'version_no' => $data['version'] + 1];
|
||||
if ($data['assignee_user_id'] ?? null) {
|
||||
abort_unless($actor->can('manage-portal'), 403);
|
||||
$assignee = User::where('id', $data['assignee_user_id'])->firstOrFail();
|
||||
abort_unless($assignee->hasVerifiedEmail(), 422, 'Responsável sem conta verificada.');
|
||||
abort_unless($assignee->can('manage-portal') || DB::table('desk_requests')->where('case_id', $id)->where('analyst_id', $assignee->id)->exists() || $this->desk->owns($assignee, $case), 422, 'Responsável sem acesso ao dossier.');
|
||||
$updates['assignee_user_id'] = $assignee->id;
|
||||
}
|
||||
if ($data['due_at'] ?? null) {
|
||||
$updates['due_at'] = $data['due_at'];
|
||||
}
|
||||
$task->update($updates);
|
||||
$this->desk->event($case, $actor, 'guidance_task_updated', ['task_id' => $task->id, ...$data, 'kind' => $actor->can('manage-portal') ? 'responsible_response' : 'investor_action']);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Ai;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\VirtualDesk;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class InvestorGuidanceEvidence
|
||||
{
|
||||
public const FIELDS = ['market_evidence', 'location_evidence', 'resource_evidence', 'funding_evidence'];
|
||||
|
||||
/** @return array<string,array<string,mixed>> */
|
||||
public function forCase(User $actor, \stdClass $case, VirtualDesk $desk): array
|
||||
{
|
||||
$privateScope = ! $desk->owns($actor, $case, false) && ! $actor->can('manage-portal');
|
||||
$documents = DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $case->id)->whereNull('d.deleted_at')
|
||||
->when($privateScope, fn ($q) => $q->where(fn ($p) => $p->whereNull('l.request_id')->orWhereIn('l.request_id', DB::table('desk_requests')->where('analyst_id', $actor->id)->select('id'))))
|
||||
->get(['l.id as link_id', 'l.document_id', 'l.category', 'l.valid_until', 'd.file_name', 'd.version_no', 'd.sha256', 'd.status', 'd.malware_scan_status', 'd.malware_scanned_sha256', 'd.storage_key']);
|
||||
$reviews = DB::table('desk_events as e')->join('users as u', 'u.id', '=', 'e.actor_id')->where('e.case_id', $case->id)->where('e.action', 'document_reviewed')
|
||||
->when($privateScope, fn ($q) => $q->where(fn ($p) => $p->whereNull('e.request_id')->orWhereIn('e.request_id', DB::table('desk_requests')->where('analyst_id', $actor->id)->select('id'))))
|
||||
->orderByDesc('e.event_order')->get(['e.id', 'e.payload', 'e.created_at', 'e.actor_id', 'u.name']);
|
||||
$result = [];
|
||||
foreach (self::FIELDS as $field) {
|
||||
$records = [];
|
||||
foreach ($documents as $doc) {
|
||||
$latest = null;
|
||||
foreach ($reviews as $review) {
|
||||
$payload = $desk->decode($review->payload);
|
||||
if (($payload['link_id'] ?? null) === $doc->link_id) {
|
||||
$latest = ['id' => $review->id, 'author_id' => $review->actor_id, 'author' => $review->name, 'date' => $review->created_at, 'payload' => $payload];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($doc->category !== $field && ! ($latest && in_array($field, $latest['payload']['supports'] ?? [], true))) {
|
||||
continue;
|
||||
}
|
||||
// A later review replaces the earlier scope. Merely choosing a
|
||||
// document category does not verify a preparation assumption.
|
||||
if ($latest && ! in_array($field, $latest['payload']['supports'] ?? [], true)) {
|
||||
$latest = null;
|
||||
}
|
||||
$state = 'attached_unchecked';
|
||||
if ($doc->valid_until && $doc->valid_until < now()->toDateString()) {
|
||||
$state = 'expired';
|
||||
} elseif ($latest && (($latest['payload']['sha256'] ?? null) !== trim($doc->sha256) || ($latest['payload']['document_version'] ?? null) !== (int) $doc->version_no)) {
|
||||
$state = 'superseded';
|
||||
} elseif ($latest) {
|
||||
$path = realpath(Storage::disk('local')->path($doc->storage_key));
|
||||
$root = realpath(Storage::disk('local')->path(''));
|
||||
if (! $path || ! $root || ! str_starts_with($path, $root.DIRECTORY_SEPARATOR) || ! is_file($path) || ! is_string($hash = hash_file('sha256', $path)) || ! hash_equals(trim($doc->sha256), $hash)) {
|
||||
$state = 'superseded';
|
||||
} elseif ($doc->status !== 'active' || $doc->malware_scan_status !== 'clean' || trim($doc->sha256) !== $doc->malware_scanned_sha256) {
|
||||
$state = 'technical_unavailable';
|
||||
} else {
|
||||
$state = match ($latest['payload']['outcome'] ?? '') {
|
||||
'internal_verified' => 'internally_verified',
|
||||
'contradiction_identified' => 'contradiction',
|
||||
default => 'issue_identified',
|
||||
};
|
||||
}
|
||||
}
|
||||
$records[] = ['document_id' => $doc->document_id, 'link_id' => $doc->link_id, 'name' => $doc->file_name, 'state' => $state, 'version' => (int) $doc->version_no, 'hash' => trim($doc->sha256), 'valid_until' => $doc->valid_until,
|
||||
'review' => $latest ? ['event_id' => $latest['id'], 'author_id' => $latest['author_id'], 'author' => $latest['author'], 'date' => $latest['date'], 'what_checked' => $latest['payload']['what_checked'], 'evidence' => $latest['payload']['evidence'], 'scope' => $latest['payload']['scope']] : null];
|
||||
}
|
||||
if ($records) {
|
||||
// A specific current contradiction takes precedence; other valid evidence
|
||||
// remains visible instead of erasing expired or replaced originals.
|
||||
$states = array_column($records, 'state');
|
||||
$state = 'attached_unchecked';
|
||||
foreach (['contradiction', 'internally_verified', 'issue_identified', 'superseded', 'expired', 'technical_unavailable'] as $candidate) {
|
||||
if (in_array($candidate, $states, true)) {
|
||||
$state = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$result[$field] = ['state' => $state, 'documents' => $records];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Ai;
|
||||
|
||||
/** Internal preparation rules; never calculate tax, returns or eligibility. */
|
||||
class InvestorGuidanceRules
|
||||
{
|
||||
public const VERSION = 'investor-guidance-2026-10-08-v3';
|
||||
|
||||
public const FIELDS = ['project_title', 'project_summary', 'activity', 'target_customers', 'market_evidence', 'location',
|
||||
'location_evidence', 'resource_plan', 'resource_evidence', 'funding_evidence', 'expected_start_date', 'ready_date', 'schedule',
|
||||
'demand_quantity', 'capacity_quantity', 'quantity_unit', 'quantity_period', 'resources_required', 'resources_available', 'resource_unit',
|
||||
'funding_required', 'funding_committed', 'funding_currency', 'funding_ready_date', 'completion_date'];
|
||||
|
||||
/** @param array<string,mixed> $facts
|
||||
* @param array<string,array<string,mixed>> $evidence
|
||||
* @return list<array<string,mixed>> */
|
||||
public function evaluate(array $facts, array $evidence = []): array
|
||||
{
|
||||
$phases = [
|
||||
'idea' => ['Definir a actividade e o objectivo', ['project_title', 'project_summary', 'activity'], 'Confirme o que pretende produzir ou prestar.', 'plan'],
|
||||
'market' => ['Documentar clientes e hipóteses de mercado', ['target_customers', 'market_evidence'], 'Reúna evidência de clientes; uma hipótese não demonstra procura.', 'opportunities'],
|
||||
'location' => ['Comparar locais e requisitos da actividade', ['location', 'location_evidence'], 'Confirme terreno, acesso, infraestrutura e licenciamento para cada alternativa.', 'locations'],
|
||||
'resources' => ['Preparar recursos e equipa', ['resource_plan', 'resource_evidence'], 'Distinga recursos declarados de disponibilidade comprovada.', 'partners'],
|
||||
'finance' => ['Explorar financiamento e cenários', ['funding_evidence'], 'Reveja os pressupostos e os cenários no simulador determinístico.', 'simulator'],
|
||||
'execution' => ['Reconciliar calendário e dependências', ['expected_start_date', 'ready_date', 'schedule'], 'Confirme datas, dependências e responsáveis antes de assumir marcos.', 'plan'],
|
||||
'support' => ['Preparar o pedido e acompanhar a resposta', [], 'Prepare um rascunho no Balcão; reveja e submeta pessoalmente.', 'support'],
|
||||
];
|
||||
$result = [];
|
||||
foreach ($phases as $code => [$objective, $fields, $explanation, $action]) {
|
||||
$known = [];
|
||||
$unknown = [];
|
||||
$phaseEvidence = [];
|
||||
$risks = [];
|
||||
foreach ($fields as $field) {
|
||||
$value = trim((string) ($facts[$field] ?? ''));
|
||||
$record = $evidence[$field] ?? null;
|
||||
$state = $record['state'] ?? ($value !== '' ? 'investor_declared_not_verified' : 'absent');
|
||||
if (in_array($field, InvestorGuidanceEvidence::FIELDS, true)) {
|
||||
$phaseEvidence[] = ['field' => $field, 'state' => $state, 'documents' => $record['documents'] ?? []];
|
||||
if ($state !== 'internally_verified') {
|
||||
$message = match ($state) {
|
||||
'absent' => 'Ainda não existe informação ou documento para este pressuposto.',
|
||||
'investor_declared_not_verified' => 'A declaração do investidor ainda não foi conferida.',
|
||||
'attached_unchecked' => 'Existe um documento associado, sem conferência interna deste pressuposto.',
|
||||
'expired' => 'A validade documental registada terminou; actualize a evidência.',
|
||||
'superseded' => 'O ficheiro ou a versão mudou depois da conferência; é necessária nova análise.',
|
||||
'contradiction' => 'Uma conferência interna identificou contradição no âmbito descrito.',
|
||||
'issue_identified' => 'Uma conferência interna identificou um problema no âmbito descrito.',
|
||||
default => 'A evidência não está disponível para utilização; consulte o motivo documental.',
|
||||
};
|
||||
$risks[] = ['code' => $field.'_'.$state, 'classification' => $state === 'contradiction' ? 'documented_contradiction' : 'insufficient_information', 'explanation' => $message, 'field' => $field, 'restriction' => false];
|
||||
}
|
||||
}
|
||||
if ($value !== '') {
|
||||
$known[] = ['field' => $field, 'value' => $value, 'status' => 'investor_declared_not_verified'];
|
||||
} elseif (! $record) {
|
||||
$unknown[] = $field;
|
||||
}
|
||||
}
|
||||
$result[] = ['id' => $code, 'objective' => $objective, 'known' => $known, 'questions' => $unknown,
|
||||
'recommendations' => [[
|
||||
'code' => $code.'_preparation', 'kind' => 'automatic_recommendation', 'classification' => $unknown ? 'insufficient_information' : 'verification_required',
|
||||
'explanation' => $explanation, 'action' => $action, 'assumptions' => $known,
|
||||
'source' => ['identifier' => self::VERSION, 'reference' => 'app/Services/Ai/InvestorGuidanceRules.php#'.$code, 'date' => '2026-10-08', 'nature' => 'internal_preparation_rule'],
|
||||
'limitations' => ['Uma conferência interna só sustenta o âmbito descrito pelo verificador; não aprova procura, infraestrutura, financiamento, registo ou licença institucional.', 'Sem previsão de sucesso, taxa fiscal ou elegibilidade inferida.'],
|
||||
]], 'evidence' => $phaseEvidence, 'risks' => $risks];
|
||||
}
|
||||
if (($facts['expected_start_date'] ?? '') && ($facts['ready_date'] ?? '') && $facts['expected_start_date'] < $facts['ready_date']) {
|
||||
$result[5]['risks'][] = ['code' => 'schedule_contradiction', 'classification' => 'declared_contradiction',
|
||||
'explanation' => 'O início indicado antecede a disponibilidade declarada dos recursos.', 'basis' => ['expected_start_date' => $facts['expected_start_date'], 'ready_date' => $facts['ready_date']],
|
||||
'restriction' => false];
|
||||
}
|
||||
$result[4]['risks'][] = ['code' => 'tax_eligibility_not_inferred', 'classification' => 'insufficient_information', 'explanation' => 'A evidência do financiamento não determina taxas fiscais ou elegibilidade. Consulte os pressupostos, fontes e versões no simulador.', 'restriction' => false];
|
||||
foreach ([
|
||||
[1, 'demand_quantity', 'capacity_quantity', ['quantity_unit', 'quantity_period'], 'A procura declarada excede a capacidade declarada para a mesma unidade e período.'],
|
||||
[3, 'resources_required', 'resources_available', ['resource_unit'], 'Os recursos disponíveis declarados são inferiores aos necessários declarados.'],
|
||||
[4, 'funding_required', 'funding_committed', ['funding_currency'], 'O financiamento comprometido declarado é inferior ao necessário declarado na mesma moeda.'],
|
||||
] as [$phase, $required, $available, $scope, $explanation]) {
|
||||
$values = array_intersect_key($facts, array_flip([$required, $available, ...$scope]));
|
||||
if (! array_filter($values, fn ($value) => $value !== null && $value !== '')) {
|
||||
continue;
|
||||
}
|
||||
$complete = true;
|
||||
foreach ([$required, $available, ...$scope] as $field) {
|
||||
if (! isset($facts[$field]) || trim((string) $facts[$field]) === '') {
|
||||
$complete = false;
|
||||
}
|
||||
}
|
||||
// Compare declared magnitudes only: this is no forecast, valuation or financial calculation.
|
||||
$left = $facts[$required] ?? null;
|
||||
$right = $facts[$available] ?? null;
|
||||
$valid = is_scalar($left) && is_scalar($right)
|
||||
&& preg_match('/^\d{1,12}(\.\d{1,2})?$/', (string) $left)
|
||||
&& preg_match('/^\d{1,12}(\.\d{1,2})?$/', (string) $right);
|
||||
if (! $complete || ! $valid) {
|
||||
$result[$phase]['risks'][] = ['code' => $required.'_comparison_incomplete', 'classification' => 'insufficient_information',
|
||||
'explanation' => 'Faltam valores comparáveis, unidade, período ou moeda para esta comparação.', 'basis' => $values, 'restriction' => false];
|
||||
} elseif ($this->declaredMagnitude((string) $left) > $this->declaredMagnitude((string) $right)) {
|
||||
$result[$phase]['risks'][] = ['code' => $required.'_declared_gap', 'classification' => 'declared_contradiction',
|
||||
'explanation' => $explanation.' Estes valores são declarações, não disponibilidade comprovada.', 'basis' => $values, 'restriction' => false];
|
||||
}
|
||||
}
|
||||
foreach ([['completion_date', 'expected_start_date', 'A conclusão declarada antecede o início.'],
|
||||
['expected_start_date', 'funding_ready_date', 'O início declarado antecede a disponibilidade declarada do financiamento.']] as [$first, $second, $explanation]) {
|
||||
if (! empty($facts[$first]) && ! empty($facts[$second]) && $facts[$first] < $facts[$second]) {
|
||||
$result[5]['risks'][] = ['code' => $first.'_'.$second.'_contradiction', 'classification' => 'declared_contradiction',
|
||||
'explanation' => $explanation, 'basis' => [$first => $facts[$first], $second => $facts[$second]], 'restriction' => false];
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function declaredMagnitude(string $value): int
|
||||
{
|
||||
$parts = explode('.', $value);
|
||||
|
||||
return (int) $parts[0] * 100 + (int) str_pad($parts[1] ?? '', 2, '0');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Illuminate\Support\Carbon;
|
||||
|
||||
/** Availability evidence is distinct from legal validity or institutional endorsement. */
|
||||
class InstitutionalLinkMonitor
|
||||
{
|
||||
/** @return array{available:bool,reason:string,contact:string} */
|
||||
public function status(\stdClass $link): array
|
||||
{
|
||||
$valid = filter_var($link->url, FILTER_VALIDATE_URL) && parse_url($link->url, PHP_URL_SCHEME) === 'https';
|
||||
$fresh = $link->verified_at && Carbon::parse($link->verified_at)->between(now()->subDays(7), now());
|
||||
|
||||
return ['available' => (bool) ($valid && $fresh && $link->status === 'active'),
|
||||
'reason' => ! $valid ? 'invalid_url' : (! $fresh ? 'verification_missing_or_expired' : ($link->status === 'active' ? 'recent_internal_check' : 'reported_unavailable')),
|
||||
'contact' => $link->contact ?: (config('portal.support.email') ?: 'Utilize o pedido de apoio no mesmo dossier.')];
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,8 @@ class InvestmentZones
|
||||
});
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $filters */
|
||||
/** @param array<string, mixed> $filters
|
||||
* @return Builder<InvestmentZone> */
|
||||
public function search(array $filters, bool $management = false): Builder
|
||||
{
|
||||
$query = $management ? InvestmentZone::query() : $this->visible();
|
||||
@@ -118,7 +119,7 @@ class InvestmentZones
|
||||
$zone = InvestmentZone::whereKey($id)->lockForUpdate()->firstOrFail();
|
||||
abort_if((int) $zone->version_no !== $version, 409, 'Versão alterada. Recarregue antes de aprovar.');
|
||||
$source = DB::table('investment_legal_sources')->where('id', $zone->legal_source_id)->lockForUpdate()->first();
|
||||
$evidence = is_array($zone->source_evidence) ? $zone->source_evidence : [];
|
||||
$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'
|
||||
@@ -166,7 +167,7 @@ class InvestmentZones
|
||||
{
|
||||
return DB::transaction(function () use ($id, $version, $actor) {
|
||||
$location = DB::table('investment_zone_locations')->where('id', $id)->lockForUpdate()->first();
|
||||
abort_unless($location, 404);
|
||||
abort_unless($location !== null, 404);
|
||||
abort_if((int) $location->version_no !== $version, 409, 'Version changed.');
|
||||
abort_if((int) $location->editor_id === $actor, 403, 'Independent review required.');
|
||||
abort_unless($location->validation_status === 'pending', 422, 'Territorial association already reviewed.');
|
||||
|
||||
@@ -36,7 +36,7 @@ final class ArchiveLimitInspector
|
||||
$expandedBytes = 0;
|
||||
for ($index = 0; $index < $archive->numFiles; $index++) {
|
||||
$entry = $archive->statIndex($index);
|
||||
if (! is_array($entry) || ! isset($entry['size']) || ! is_int($entry['size']) || $entry['size'] < 0) {
|
||||
if (! is_array($entry) || $entry['size'] < 0) {
|
||||
return 'archive_inspection_failed';
|
||||
}
|
||||
|
||||
|
||||
@@ -197,7 +197,7 @@ final class ClamDScanner implements MalwareScanner
|
||||
}
|
||||
$offset += $written;
|
||||
$meta = stream_get_meta_data($socket);
|
||||
if ($meta['timed_out'] ?? false) {
|
||||
if ($meta['timed_out']) {
|
||||
throw new RuntimeException('ClamD scan timed out while sending data.');
|
||||
}
|
||||
}
|
||||
@@ -208,19 +208,21 @@ final class ClamDScanner implements MalwareScanner
|
||||
{
|
||||
$reply = stream_get_line($socket, 65536, "\0");
|
||||
$meta = stream_get_meta_data($socket);
|
||||
if ($reply === false || ($meta['timed_out'] ?? false)) {
|
||||
throw new RuntimeException(($meta['timed_out'] ?? false) ? 'ClamD scan timed out.' : 'ClamD returned no scan result.');
|
||||
if ($reply === false || $meta['timed_out']) {
|
||||
throw new RuntimeException($meta['timed_out'] ? 'ClamD scan timed out.' : 'ClamD returned no scan result.');
|
||||
}
|
||||
|
||||
return trim($reply);
|
||||
}
|
||||
|
||||
/** @return array{available:bool,engine:string,engine_version:?string,signatures_at:?DateTimeImmutable,signature_age_hours:?float,reason_code:?string} */
|
||||
private function unavailable(string $reason): array
|
||||
{
|
||||
return ['available' => false, 'engine' => 'ClamAV', 'engine_version' => null, 'signatures_at' => null,
|
||||
'signature_age_hours' => null, 'reason_code' => $reason];
|
||||
}
|
||||
|
||||
/** @return array{result:string,reason_code:?string,engine:string,engine_version:string,signatures_at:?DateTimeImmutable,duration_ms:int,detail:?string} */
|
||||
private function result(string $result, ?string $reason, int $startedAt, ?string $detail): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace App\Services\Malware;
|
||||
|
||||
interface MalwareScanner
|
||||
{
|
||||
/** @return array{result:string,reason_code:?string,engine:string,engine_version:string,signatures_at:\DateTimeImmutable,duration_ms:int,detail:?string} */
|
||||
/** @return array{result:string,reason_code:?string,engine:string,engine_version:string,signatures_at:?\DateTimeImmutable,duration_ms:int,detail:?string} */
|
||||
public function scan(string $path, int $fileSizeBytes, string $storageRoot): array;
|
||||
|
||||
/** @return array{available:bool,engine:string,engine_version:?string,signatures_at:?\DateTimeImmutable,signature_age_hours:?float,reason_code:?string} */
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\SimulationFormDraft;
|
||||
use App\Models\ExistingBusiness;
|
||||
use App\Models\SimulationFormDraft;
|
||||
use App\Models\User;
|
||||
use App\Services\Mne\BusinessRepository;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
@@ -164,6 +164,7 @@ final class SimulationFormDrafts
|
||||
|
||||
return DB::transaction(function () use ($actorId, $tokenHash): ?SimulationFormDraft {
|
||||
$this->setContext($actorId, $tokenHash);
|
||||
|
||||
return $this->ownedQuery($actorId, $tokenHash)->orderByDesc('updated_at')->first();
|
||||
});
|
||||
}
|
||||
|
||||
+214
-58
@@ -2,26 +2,33 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\InvestmentSubmission;
|
||||
use App\Models\Investor;
|
||||
use App\Models\InvestorJourney;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
class VirtualDesk
|
||||
{
|
||||
public function installed(): bool
|
||||
{
|
||||
return \Illuminate\Support\Facades\Schema::hasTable('desk_cases');
|
||||
return Schema::hasTable('desk_cases') && Schema::hasColumn('desk_events', 'event_order') && Schema::hasColumn('tasks', 'desk_case_id');
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $data */
|
||||
public function encode(array $data): string
|
||||
{
|
||||
return Crypt::encryptString(json_encode($data, JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function decode(?string $data): array
|
||||
{
|
||||
return $data ? json_decode(Crypt::decryptString($data), true, 512, JSON_THROW_ON_ERROR) : [];
|
||||
@@ -33,32 +40,36 @@ class VirtualDesk
|
||||
->when($write, fn ($q) => $q->whereIn('role', ['admin', 'manager', 'officer']))->exists();
|
||||
}
|
||||
|
||||
public function owns(User $actor, object $case, bool $write = true): bool
|
||||
public function owns(User $actor, \stdClass $case, bool $write = true): bool
|
||||
{
|
||||
return $case->organization_id ? $this->represents($actor, $case->organization_id, $write) : (int) $case->user_id === $actor->id;
|
||||
}
|
||||
|
||||
public function case(User $actor, string $id, bool $write = false, bool $lock = false): object
|
||||
public function case(User $actor, string $id, bool $write = false, bool $lock = false): \stdClass
|
||||
{
|
||||
$case = DB::table('desk_cases')->where('id', $id)->when($lock, fn ($q) => $q->lockForUpdate())->first();
|
||||
abort_unless($case, 404);
|
||||
abort_unless($case !== null, 404);
|
||||
abort_unless($this->owns($actor, $case, $write) || (! $write && ($actor->can('manage-portal') || DB::table('desk_requests')->where('case_id', $id)->where('analyst_id', $actor->id)->exists())), 403);
|
||||
|
||||
return $case;
|
||||
}
|
||||
|
||||
public function request(User $actor, string $id, bool $ownerOnly = false, bool $lock = false): object
|
||||
public function request(User $actor, string $id, bool $ownerOnly = false, bool $lock = false): \stdClass
|
||||
{
|
||||
$request = DB::table('desk_requests')->where('id', $id)->when($lock, fn ($q) => $q->lockForUpdate())->first();
|
||||
abort_unless($request, 404);
|
||||
abort_unless($request !== null, 404);
|
||||
$case = $this->case($actor, $request->case_id, $ownerOnly);
|
||||
abort_unless($this->owns($actor, $case, $ownerOnly) || (! $ownerOnly && ($actor->can('manage-portal') || (int) $request->analyst_id === $actor->id)), 403);
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
public function createCase(User $actor, array $input): object
|
||||
/** @param array<string,mixed> $input */
|
||||
public function createCase(User $actor, array $input): \stdClass
|
||||
{
|
||||
$data = Validator::make($input, ['title' => 'required|string|max:200', 'client_key' => 'required|uuid', 'reuse_consent' => 'accepted',
|
||||
'organization_id' => 'nullable|uuid', 'company_id' => 'nullable|uuid', 'submission_id' => 'nullable|uuid', 'simulation_id' => 'nullable|uuid'])->validate();
|
||||
'organization_id' => 'nullable|uuid', 'company_id' => 'nullable|uuid', 'submission_id' => 'nullable|uuid', 'simulation_id' => 'nullable|uuid', 'journey_id' => 'nullable|uuid', 'interest_request_id' => 'nullable|integer|min:1'])->validate();
|
||||
|
||||
return DB::transaction(function () use ($actor, $data) {
|
||||
DB::table('users')->where('id', $actor->id)->lockForUpdate()->first();
|
||||
$existing = DB::table('desk_cases')->where('user_id', $actor->id)->where('client_key', $data['client_key'])->first();
|
||||
@@ -66,58 +77,132 @@ class VirtualDesk
|
||||
if ($existing) {
|
||||
abort_unless(($this->decode($existing->payload)['_creation_hash'] ?? '') === $inputHash, 409, 'Chave de criação usada com outro conteúdo.');
|
||||
abort_unless($this->owns($actor, $existing), 403);
|
||||
|
||||
return $existing;
|
||||
}
|
||||
abort_if(($data['organization_id'] ?? null) && ! $this->represents($actor, $data['organization_id']), 403);
|
||||
$interest = null;
|
||||
if ($data['interest_request_id'] ?? null) {
|
||||
$interest = DB::table('interest_requests')->where('id', $data['interest_request_id'])->where('metadata->portal_user_id', $actor->id)->whereNull('deleted_at')->firstOrFail();
|
||||
$linkedInterest = DB::table('desk_cases')->where('interest_request_id', $data['interest_request_id'])->where('user_id', $actor->id)->first();
|
||||
if ($linkedInterest) {
|
||||
abort_unless($this->owns($actor, $linkedInterest), 403);
|
||||
foreach (['organization_id', 'submission_id', 'simulation_id', 'company_id', 'journey_id'] as $anchor) {
|
||||
abort_if(($data[$anchor] ?? null) && ($linkedInterest->{$anchor} ?? null) !== $data[$anchor], 409, 'Pedido existente já associado a um dossier; continue nesse contexto.');
|
||||
}
|
||||
|
||||
return $linkedInterest;
|
||||
}
|
||||
}
|
||||
if (($data['journey_id'] ?? null) && Schema::hasColumn('desk_cases', 'journey_id')) {
|
||||
InvestorJourney::whereKey($data['journey_id'])->where('user_id', $actor->id)->firstOrFail();
|
||||
$linkedJourney = DB::table('desk_cases')->where('journey_id', $data['journey_id'])->where('user_id', $actor->id)->first();
|
||||
if ($linkedJourney) {
|
||||
abort_unless($this->owns($actor, $linkedJourney), 403);
|
||||
foreach (['organization_id', 'submission_id', 'simulation_id', 'company_id'] as $anchor) {
|
||||
abort_if(($data[$anchor] ?? null) && $linkedJourney->{$anchor} !== $data[$anchor], 409, 'Este percurso já tem um dossier; associe novos elementos no contexto existente.');
|
||||
}
|
||||
|
||||
return $linkedJourney;
|
||||
}
|
||||
}
|
||||
$payload = ['profile' => app(InvestmentWorkspace::class)->account($actor->id)['payload'], 'name' => $actor->name, 'email' => $actor->email, '_creation_hash' => $inputHash, 'reuse_consent' => ['actor_id' => $actor->id, 'date' => now()->toIso8601String()]];
|
||||
if ($data['company_id'] ?? null) {
|
||||
$company = DB::table('investment_company_declarations')->where('id', $data['company_id'])->where('user_id', $actor->id)->first();
|
||||
abort_unless($company, 404);
|
||||
$payload['company'] = $this->decode($company->payload); $payload['company_version'] = $company->version;
|
||||
abort_unless($company !== null, 404);
|
||||
$payload['company'] = $this->decode($company->payload);
|
||||
$payload['company_version'] = $company->version;
|
||||
}
|
||||
if ($data['submission_id'] ?? null) {
|
||||
$submission = \App\Models\InvestmentSubmission::where('id', $data['submission_id'])->where('user_id', $actor->id)->firstOrFail();
|
||||
$payload['investment'] = $submission->payload; $payload['investment_version'] = $submission->version;
|
||||
$submission = InvestmentSubmission::where('id', $data['submission_id'])->where('user_id', $actor->id)->firstOrFail();
|
||||
$payload['investment'] = $submission->payload;
|
||||
$payload['investment_version'] = $submission->version;
|
||||
}
|
||||
if ($data['simulation_id'] ?? null) {
|
||||
app(PartnershipSources::class)->resolve('simulation', $data['simulation_id'], $actor);
|
||||
}
|
||||
if ($data['simulation_id'] ?? null) app(PartnershipSources::class)->resolve('simulation', $data['simulation_id'], $actor);
|
||||
// One case per explicitly reused investment; retries never create parallel dossiers.
|
||||
if ($data['submission_id'] ?? null) {
|
||||
$linked = DB::table('desk_cases')->where('submission_id', $data['submission_id'])->where('user_id', $actor->id)->first();
|
||||
if ($linked) { abort_unless($this->owns($actor, $linked), 403); return $linked; }
|
||||
if ($linked) {
|
||||
abort_unless($this->owns($actor, $linked), 403);
|
||||
foreach (['organization_id', 'simulation_id', 'company_id', 'journey_id'] as $anchor) {
|
||||
abort_if(($data[$anchor] ?? null) && ($linked->{$anchor} ?? null) !== $data[$anchor], 409, 'Contexto já associado a outro dossier; não foi criado um dossier paralelo.');
|
||||
}
|
||||
|
||||
return $linked;
|
||||
}
|
||||
}
|
||||
if ($data['simulation_id'] ?? null) {
|
||||
$linked = DB::table('desk_cases')->where('simulation_id', $data['simulation_id'])->where('user_id', $actor->id)->first();
|
||||
if ($linked) {
|
||||
abort_unless($this->owns($actor, $linked), 403);
|
||||
foreach (['organization_id', 'submission_id', 'company_id', 'journey_id'] as $anchor) {
|
||||
abort_if(($data[$anchor] ?? null) && ($linked->{$anchor} ?? null) !== $data[$anchor], 409, 'Contexto já associado a outro dossier; não foi criado um dossier paralelo.');
|
||||
}
|
||||
|
||||
return $linked;
|
||||
}
|
||||
}
|
||||
$identity = [];
|
||||
if (Schema::hasColumn('desk_cases', 'investor_id')) {
|
||||
$journey = ($data['journey_id'] ?? null) ? InvestorJourney::whereKey($data['journey_id'])->where('user_id', $actor->id)->firstOrFail() : null;
|
||||
$account = DB::table('investor_accounts')->where('user_id', $actor->id)->first();
|
||||
if (! $account) {
|
||||
$investor = ($interest->investor_id ?? null) ? Investor::whereKey($interest->investor_id)->firstOrFail() : Investor::create(['legal_name' => $actor->name, 'investor_type' => 'individual', 'status' => 'prospect', 'source_system' => 'portal-account', 'data_classification' => 'confidential']);
|
||||
DB::table('investor_accounts')->insert(['user_id' => $actor->id, 'investor_id' => $investor->id, 'created_at' => now(), 'updated_at' => now()]);
|
||||
$account = DB::table('investor_accounts')->where('user_id', $actor->id)->first();
|
||||
}
|
||||
$dossierId = $journey->dossier_id ?? ($submission->dossier_id ?? null);
|
||||
$projectId = $dossierId ? DB::table('investment_dossiers')->where('id', $dossierId)->whereNull('deleted_at')->value('project_id') : null;
|
||||
$identity = ['investor_id' => $account->investor_id, 'journey_id' => $journey?->id, 'project_id' => $projectId, 'interest_request_id' => $data['interest_request_id'] ?? null];
|
||||
}
|
||||
$id = (string) Str::uuid();
|
||||
DB::table('desk_cases')->insert(['id' => $id, 'user_id' => $actor->id, 'organization_id' => $data['organization_id'] ?? null, 'company_id' => $data['company_id'] ?? null,
|
||||
'submission_id' => $data['submission_id'] ?? null, 'simulation_id' => $data['simulation_id'] ?? null, 'title' => $data['title'], 'client_key' => $data['client_key'],
|
||||
'payload' => $this->encode($payload), 'workflow' => json_encode(array_map(fn ($s) => [...$s, 'status' => 'not_started', 'evidence' => null], config('virtual-desk.steps'))), 'created_at' => now(), 'updated_at' => now()]);
|
||||
$case = DB::table('desk_cases')->where('id', $id)->first();
|
||||
'payload' => $this->encode($payload), 'workflow' => json_encode(array_map(fn ($s) => [...$s, 'status' => 'not_started', 'evidence' => null], config('virtual-desk.steps'))), 'created_at' => now(), 'updated_at' => now(), ...$identity]);
|
||||
$case = DB::table('desk_cases')->where('id', $id)->firstOrFail();
|
||||
$this->event($case, $actor, 'case_created', ['reuse_consent' => true]);
|
||||
|
||||
return $case;
|
||||
});
|
||||
}
|
||||
|
||||
public function createRequest(User $actor, string $caseId, string $serviceId, string $clientKey): object
|
||||
public function createRequest(User $actor, string $caseId, string $serviceId, string $clientKey): \stdClass
|
||||
{
|
||||
abort_unless(Str::isUuid($serviceId) && Str::isUuid($clientKey), 422);
|
||||
|
||||
return DB::transaction(function () use ($actor, $caseId, $serviceId, $clientKey) {
|
||||
$case = $this->case($actor, $caseId, true, true);
|
||||
abort_if($case->closed_at, 409, 'Dossier encerrado.');
|
||||
$service = DB::table('desk_service_versions')->where('id', $serviceId)->where('status', 'published')->first();
|
||||
abort_unless($service, 422, 'Versão do serviço indisponível para novos pedidos.');
|
||||
abort_unless($service !== null, 422, 'Versão do serviço indisponível para novos pedidos.');
|
||||
$existing = DB::table('desk_requests')->where('case_id', $caseId)->where('client_key', $clientKey)->first();
|
||||
if ($existing) { abort_unless($existing->service_version_id === $serviceId, 409); return $existing; }
|
||||
$id = (string) Str::uuid(); $base = $this->decode($case->payload);
|
||||
if ($existing) {
|
||||
abort_unless($existing->service_version_id === $serviceId, 409);
|
||||
|
||||
return $existing;
|
||||
}
|
||||
$id = (string) Str::uuid();
|
||||
$base = $this->decode($case->payload);
|
||||
$payload = ['title' => $case->title, 'description' => $base['investment']['objective'] ?? '', 'regime' => 'unknown'];
|
||||
DB::table('desk_requests')->insert(['id' => $id, 'case_id' => $caseId, 'service_version_id' => $serviceId, 'client_key' => $clientKey, 'payload' => $this->encode($payload), 'created_at' => now(), 'updated_at' => now()]);
|
||||
$request = DB::table('desk_requests')->where('id', $id)->first(); $this->event($case, $actor, 'draft_created', [], $request);
|
||||
$request = DB::table('desk_requests')->where('id', $id)->firstOrFail();
|
||||
$this->event($case, $actor, 'draft_created', [], $request);
|
||||
|
||||
return $request;
|
||||
});
|
||||
}
|
||||
|
||||
public function definition(object $request): array
|
||||
/** @return array<string,mixed> */
|
||||
public function definition(\stdClass $request): array
|
||||
{
|
||||
return json_decode(DB::table('desk_service_versions')->where('id', $request->service_version_id)->value('definition'), true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $payload
|
||||
* @param array<string,mixed> $definition
|
||||
* @return array<string,mixed> */
|
||||
public function validateFields(array $payload, array $definition, bool $submitting): array
|
||||
{
|
||||
$rules = ['payload' => ['required', 'array:'.implode(',', array_keys($definition['fields']))]];
|
||||
@@ -126,8 +211,8 @@ class VirtualDesk
|
||||
$required = $submitting && ($field['required'] ?? false) && $active;
|
||||
$rule = [$required ? 'required' : 'nullable'];
|
||||
$rule = [...$rule, ...match ($field['type']) {
|
||||
'number' => ['numeric', 'min:'.($field['min'] ?? 0), 'max:999999999999'], 'date' => ['date_format:Y-m-d'],
|
||||
'select' => [\Illuminate\Validation\Rule::in(array_keys($field['options']))], 'territory' => ['uuid'],
|
||||
'number' => ['numeric', 'decimal:0,2', 'min:'.($field['min'] ?? 0), 'max:999999999999'], 'date' => ['date_format:Y-m-d'],
|
||||
'select' => [Rule::in(array_keys($field['options']))], 'territory' => ['uuid'],
|
||||
default => ['string', 'max:'.($field['type'] === 'textarea' ? 15000 : 1000)],
|
||||
}];
|
||||
$rules['payload.'.$key] = $rule;
|
||||
@@ -135,83 +220,127 @@ class VirtualDesk
|
||||
$validated = Validator::make(['payload' => $payload], $rules)->validate()['payload'];
|
||||
if ($validated['territory_id'] ?? null) {
|
||||
abort_unless(app(InvestmentGeography::class)->publicReferences()->where('id', $validated['territory_id'])->exists(), 422, 'Localização indisponível.');
|
||||
if ($submitting && trim($validated['location_source'] ?? '') === '') throw ValidationException::withMessages(['payload.location_source' => 'Indique a fonte da localização.']);
|
||||
if ($submitting && trim($validated['location_source'] ?? '') === '') {
|
||||
throw ValidationException::withMessages(['payload.location_source' => 'Indique a fonte da localização.']);
|
||||
}
|
||||
}
|
||||
|
||||
return $validated;
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $payload
|
||||
* @return list<array<string,mixed>> */
|
||||
public function findings(array $payload): array
|
||||
{
|
||||
$findings = [];
|
||||
if (isset($payload['investment_amount'], $payload['own_finance'], $payload['other_finance']) && abs((float) $payload['investment_amount'] - (float) $payload['own_finance'] - (float) $payload['other_finance']) > 0.01) {
|
||||
$amounts = array_map(function ($key) use ($payload) {
|
||||
$value = (string) ($payload[$key] ?? '');
|
||||
if (! preg_match('/^\d{1,12}(?:\.\d{1,2})?$/', $value)) {
|
||||
return null;
|
||||
}
|
||||
[$whole, $fraction] = array_pad(explode('.', $value), 2, '');
|
||||
|
||||
return (int) $whole * 100 + (int) str_pad($fraction, 2, '0');
|
||||
}, ['investment_amount', 'own_finance', 'other_finance']);
|
||||
if (! in_array(null, $amounts, true) && $amounts[0] !== $amounts[1] + $amounts[2]) {
|
||||
$findings[] = ['code' => 'finance_difference', 'severity' => 'error', 'field' => 'investment_amount', 'explanation' => 'A soma do financiamento difere do valor do investimento.', 'recommendation' => 'Confirme valores e moeda.', 'method' => 'deterministic', 'confidence' => null];
|
||||
}
|
||||
if (! ($payload['territory_id'] ?? null)) $findings[] = ['code' => 'location_unknown', 'severity' => 'warning', 'field' => 'territory_id', 'explanation' => 'Localização ainda não documentada.', 'recommendation' => 'Complete quando houver fonte.', 'method' => 'deterministic', 'confidence' => null];
|
||||
if (in_array(null, $amounts, true)) {
|
||||
$findings[] = ['code' => 'finance_unknown', 'severity' => 'warning', 'field' => 'investment_amount', 'explanation' => 'A coerência do financiamento permanece indeterminada sem os três valores válidos na mesma moeda.', 'recommendation' => 'Complete as fontes de financiamento; valores ausentes não são zero.', 'method' => 'deterministic', 'confidence' => null];
|
||||
}
|
||||
if (! ($payload['territory_id'] ?? null)) {
|
||||
$findings[] = ['code' => 'location_unknown', 'severity' => 'warning', 'field' => 'territory_id', 'explanation' => 'Localização ainda não documentada.', 'recommendation' => 'Complete quando houver fonte.', 'method' => 'deterministic', 'confidence' => null];
|
||||
}
|
||||
$findings[] = ['code' => 'official_form_pending', 'severity' => 'warning', 'field' => null, 'explanation' => 'Adaptação digital do plano; não foi identificada uma versão oficial homologada do formulário APIEX no acervo.', 'recommendation' => 'A preparação e análise interna continuam disponíveis; confirme a versão exigida pela entidade para o ato oficial.', 'method' => 'documentary', 'confidence' => null];
|
||||
|
||||
return $findings;
|
||||
}
|
||||
|
||||
public function save(User $actor, string $id, array $input, bool $submit = false): object
|
||||
/** @param array<string,mixed> $input */
|
||||
public function save(User $actor, string $id, array $input, bool $submit = false): \stdClass
|
||||
{
|
||||
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'payload' => 'required|array', 'consent' => $submit ? 'accepted' : 'nullable|boolean', 'reason' => 'nullable|string|max:3000'])->validate();
|
||||
|
||||
return DB::transaction(function () use ($actor, $id, $data, $submit) {
|
||||
$request = $this->request($actor, $id, true, true);
|
||||
if ($submit && $request->status === 'submitted' && $request->version === (int) $data['version'] + 1 && hash_equals(hash('sha256', json_encode(Arr::sortRecursive($this->decode($request->payload)))), hash('sha256', json_encode(Arr::sortRecursive($data['payload']))))) return $request;
|
||||
if ($submit && $request->status === 'submitted' && $request->version === (int) $data['version'] + 1 && hash_equals(hash('sha256', json_encode(Arr::sortRecursive($this->decode($request->payload)), JSON_THROW_ON_ERROR)), hash('sha256', json_encode(Arr::sortRecursive($this->validateFields($data['payload'], $this->definition($request), false)), JSON_THROW_ON_ERROR)))) {
|
||||
return $request;
|
||||
}
|
||||
abort_unless(in_array($request->status, ['draft', 'returned'], true) && $request->version === (int) $data['version'], 409, 'Pedido alterado ou já submetido.');
|
||||
$payload = $this->validateFields($data['payload'], $this->definition($request), $submit);
|
||||
if ($request->status === 'returned') {
|
||||
$editable = json_decode($request->editable_fields ?? '[]', true); $previous = $this->decode($request->payload);
|
||||
$editable = json_decode($request->editable_fields ?? '[]', true);
|
||||
$previous = $this->decode($request->payload);
|
||||
foreach (array_unique([...array_keys($previous), ...array_keys($payload)]) as $key) {
|
||||
if (! in_array($key, $editable, true) && ($previous[$key] ?? null) !== ($payload[$key] ?? null)) throw ValidationException::withMessages(['payload.'.$key => 'Campo não autorizado para esta correção.']);
|
||||
if (! in_array($key, $editable, true) && ($previous[$key] ?? null) !== ($payload[$key] ?? null)) {
|
||||
throw ValidationException::withMessages(['payload.'.$key => 'Campo não autorizado para esta correção.']);
|
||||
}
|
||||
}
|
||||
abort_if($submit && trim($data['reason'] ?? '') === '', 422, 'Justifique a correção.');
|
||||
}
|
||||
$findings = $this->findings($payload);
|
||||
if ($submit && array_filter($findings, fn ($f) => $f['severity'] === 'error')) throw ValidationException::withMessages(['payload.investment_amount' => $findings[0]['explanation']]);
|
||||
if ($submit && array_filter($findings, fn ($f) => $f['severity'] === 'error')) {
|
||||
throw ValidationException::withMessages(['payload.investment_amount' => $findings[0]['explanation']]);
|
||||
}
|
||||
$case = $this->case($actor, $request->case_id, true);
|
||||
$snapshot = ['payload' => $payload, 'service_version_id' => $request->service_version_id, 'documents' => DB::table('desk_documents as link')->join('documents as d', 'd.id', '=', 'link.document_id')->where('link.case_id', $case->id)->where(fn ($q) => $q->whereNull('link.request_id')->orWhere('link.request_id', $id))->get(['link.document_id', 'd.sha256', 'd.version_no', 'link.source_reference'])->all(), 'findings' => $findings, 'consent' => $submit, 'source' => config('virtual-desk.source')];
|
||||
DB::table('desk_revisions')->insert(['request_id' => $id, 'version' => $request->version, 'snapshot' => $this->encode(['payload' => $this->decode($request->payload), 'status' => $request->status, 'submitted_snapshot' => $request->submitted_snapshot ? $this->decode($request->submitted_snapshot) : null])]);
|
||||
$updates = ['payload' => $this->encode($payload), 'version' => $request->version + 1, 'updated_at' => now()];
|
||||
if ($submit) $updates = [...$updates, 'status' => 'submitted', 'protocol' => $request->protocol ?? 'BV-'.date('Y').'-'.strtoupper(str_replace('-', '', $id)), 'submitted_at' => now(), 'submitted_snapshot' => $this->encode($snapshot)];
|
||||
DB::table('desk_requests')->where('id', $id)->update($updates); $request = DB::table('desk_requests')->where('id', $id)->first();
|
||||
if ($submit) {
|
||||
$updates = [...$updates, 'status' => 'submitted', 'protocol' => $request->protocol ?? 'BV-'.date('Y').'-'.strtoupper(str_replace('-', '', $id)), 'submitted_at' => now(), 'submitted_snapshot' => $this->encode($snapshot)];
|
||||
}
|
||||
DB::table('desk_requests')->where('id', $id)->update($updates);
|
||||
$request = DB::table('desk_requests')->where('id', $id)->firstOrFail();
|
||||
$this->event($case, $actor, $submit ? 'submitted' : 'draft_saved', ['reason' => $data['reason'] ?? '', 'findings' => $findings], $request);
|
||||
|
||||
return $request;
|
||||
});
|
||||
}
|
||||
|
||||
public function transition(User $actor, string $id, array $data): object
|
||||
/** @param array<string,mixed> $data */
|
||||
public function transition(User $actor, string $id, array $data): \stdClass
|
||||
{
|
||||
$data = Validator::make($data, ['version' => 'required|integer|min:1', 'status' => ['required', \Illuminate\Validation\Rule::in(array_keys(config('virtual-desk.request_states')))], 'reason' => 'required|string|min:3|max:5000', 'editable_fields' => 'nullable|array', 'editable_fields.*' => 'string|max:100'])->validate();
|
||||
$data = Validator::make($data, ['version' => 'required|integer|min:1', 'status' => ['required', Rule::in(array_keys(config('virtual-desk.request_states')))], 'reason' => 'required|string|min:3|max:5000', 'editable_fields' => 'nullable|array', 'editable_fields.*' => 'string|max:100'])->validate();
|
||||
|
||||
return DB::transaction(function () use ($actor, $id, $data) {
|
||||
$request = $this->request($actor, $id, false, true); $case = $this->case($actor, $request->case_id);
|
||||
$request = $this->request($actor, $id, false, true);
|
||||
$case = $this->case($actor, $request->case_id);
|
||||
abort_unless($request->version === (int) $data['version'], 409);
|
||||
$cancel = $data['status'] === 'cancelled';
|
||||
if ($cancel) { abort_unless($this->owns($actor, $case) && in_array($request->status, ['draft', 'returned'], true), 403); }
|
||||
else {
|
||||
if ($cancel) {
|
||||
abort_unless($this->owns($actor, $case) && in_array($request->status, ['draft', 'returned'], true), 403);
|
||||
} else {
|
||||
abort_if($this->owns($actor, $case, false), 403, 'O proponente não pode analisar ou decidir o seu próprio pedido.');
|
||||
abort_unless($actor->can('manage-portal') || (int) $request->analyst_id === $actor->id, 403);
|
||||
if (in_array($data['status'], ['accepted', 'rejected', 'closed'], true)) abort_unless($actor->can('manage-portal'), 403);
|
||||
if (in_array($data['status'], ['accepted', 'rejected', 'closed'], true)) {
|
||||
abort_unless($actor->can('manage-portal'), 403);
|
||||
}
|
||||
abort_unless(in_array($data['status'], config('virtual-desk.transitions.'.$request->status, []), true), 409, 'Transição inválida.');
|
||||
}
|
||||
$editable = $data['editable_fields'] ?? [];
|
||||
abort_if($data['status'] === 'returned' && ! $editable, 422, 'Indique os campos autorizados para correção.');
|
||||
abort_if(array_diff($editable, array_keys($this->definition($request)['fields'])), 422);
|
||||
abort_if(array_diff($editable, array_keys($this->definition($request)['fields'])) !== [], 422);
|
||||
DB::table('desk_requests')->where('id', $id)->update(['status' => $data['status'], 'version' => $request->version + 1, 'editable_fields' => json_encode($editable), 'closed_at' => in_array($data['status'], ['closed', 'cancelled'], true) ? now() : null, 'updated_at' => now()]);
|
||||
$request = DB::table('desk_requests')->where('id', $id)->first(); $this->event($case, $actor, 'request_'.$data['status'], ['reason' => $data['reason'], 'editable_fields' => $editable], $request);
|
||||
$request = DB::table('desk_requests')->where('id', $id)->firstOrFail();
|
||||
$this->event($case, $actor, 'request_'.$data['status'], ['reason' => $data['reason'], 'editable_fields' => $editable], $request);
|
||||
|
||||
return $request;
|
||||
});
|
||||
}
|
||||
|
||||
public function stage(User $actor, string $id, array $data): object
|
||||
/** @param array<string,mixed> $data */
|
||||
public function stage(User $actor, string $id, array $data): \stdClass
|
||||
{
|
||||
$data = Validator::make($data, ['version' => 'required|integer|min:1', 'stage' => ['required', \Illuminate\Validation\Rule::in(array_keys(config('virtual-desk.steps')))],
|
||||
'status' => ['required', \Illuminate\Validation\Rule::in(array_keys(config('virtual-desk.step_states')))], 'reason' => 'required|string|min:3|max:3000',
|
||||
$data = Validator::make($data, ['version' => 'required|integer|min:1', 'stage' => ['required', Rule::in(array_keys(config('virtual-desk.steps')))],
|
||||
'status' => ['required', Rule::in(array_keys(config('virtual-desk.step_states')))], 'reason' => 'required|string|min:3|max:3000',
|
||||
'evidence' => 'nullable|string|max:5000', 'document_id' => 'nullable|uuid', 'valid_until' => 'nullable|date_format:Y-m-d', 'exception' => 'nullable|boolean'])->validate();
|
||||
|
||||
return DB::transaction(function () use ($actor, $id, $data) {
|
||||
$case = $this->case($actor, $id, false, true);
|
||||
abort_unless($this->owns($actor, $case) || $actor->can('manage-portal'), 403);
|
||||
abort_unless((int) $case->version === (int) $data['version'], 409);
|
||||
$workflow = json_decode($case->workflow, true, 512, JSON_THROW_ON_ERROR); $current = $workflow[$data['stage']];
|
||||
$workflow = json_decode($case->workflow, true, 512, JSON_THROW_ON_ERROR);
|
||||
$current = $workflow[$data['stage']];
|
||||
$decision = in_array($data['status'], ['completed', 'not_applicable'], true);
|
||||
if ($decision) {
|
||||
abort_unless($actor->can('manage-portal') && ! $this->owns($actor, $case, false), 403, 'Conclusão documental exige revisão interna por outra pessoa autorizada.');
|
||||
@@ -219,36 +348,63 @@ class VirtualDesk
|
||||
foreach ($current['dependencies'] as $dependency) {
|
||||
abort_unless(in_array($workflow[$dependency]['status'], ['completed', 'not_applicable'], true) || (($data['exception'] ?? false) && ($data['document_id'] ?? null)), 422, 'Dependência '.$dependency.' pendente; uma exceção exige evidência própria.');
|
||||
}
|
||||
if ($data['stage'] !== 'E0') abort_unless($data['document_id'] ?? null, 422, 'Ligue o documento de suporte ou de não aplicabilidade.');
|
||||
if ($data['stage'] !== 'E0') {
|
||||
abort_unless($data['document_id'] ?? null, 422, 'Ligue o documento de suporte ou de não aplicabilidade.');
|
||||
}
|
||||
}
|
||||
if ($data['document_id'] ?? null) {
|
||||
$document = DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $id)->where('d.id', $data['document_id'])->whereNull('d.deleted_at')->first(['d.status', 'd.malware_scan_status', 'd.sha256', 'd.malware_scanned_sha256', 'l.valid_until']);
|
||||
abort_unless($document, 422);
|
||||
if ($decision) abort_unless($document->status === 'active' && $document->malware_scan_status === 'clean' && trim($document->sha256) === $document->malware_scanned_sha256 && (! $document->valid_until || $document->valid_until >= now()->toDateString()), 422, 'Documento ainda não consultável ou expirado.');
|
||||
abort_unless($document !== null, 422);
|
||||
if ($decision) {
|
||||
abort_unless($document->status === 'active' && $document->malware_scan_status === 'clean' && trim($document->sha256) === $document->malware_scanned_sha256 && (! $document->valid_until || $document->valid_until >= now()->toDateString()), 422, 'Documento ainda não consultável ou expirado.');
|
||||
}
|
||||
}
|
||||
if (in_array($data['status'], ['referred', 'waiting'], true)) {
|
||||
abort_unless(trim($data['evidence'] ?? '') !== '', 422, 'Registe referência e canal do encaminhamento.');
|
||||
}
|
||||
if (in_array($data['status'], ['referred', 'waiting'], true)) abort_unless(trim($data['evidence'] ?? '') !== '', 422, 'Registe referência e canal do encaminhamento.');
|
||||
// Reopening an upstream step must never leave dependent conclusions falsely valid.
|
||||
if (! in_array($data['status'], ['completed', 'not_applicable'], true)) {
|
||||
$affected = [$data['stage']];
|
||||
do { $changed = false; foreach ($workflow as $code => &$step) { if (! in_array($code, $affected, true) && array_intersect($step['dependencies'], $affected)) { $affected[] = $code; $changed = true; if ($step['status'] === 'completed') $step['status'] = 'pending'; } } unset($step); } while ($changed);
|
||||
do {
|
||||
$changed = false;
|
||||
foreach ($workflow as $code => &$step) {
|
||||
if (! in_array($code, $affected, true) && array_intersect($step['dependencies'], $affected)) {
|
||||
$affected[] = $code;
|
||||
$changed = true;
|
||||
if (in_array($step['status'], ['completed', 'not_applicable'], true)) {
|
||||
$step['status'] = 'pending';
|
||||
}
|
||||
}
|
||||
} unset($step);
|
||||
} while ($changed);
|
||||
}
|
||||
$workflow[$data['stage']] = [...$current, 'status' => $data['status'], 'evidence' => $data['evidence'] ?? null, 'document_id' => $data['document_id'] ?? null,
|
||||
'valid_until' => $data['valid_until'] ?? null, 'reason' => $data['reason'], 'reviewed_by' => $actor->id, 'updated_at' => now()->toIso8601String()];
|
||||
DB::table('desk_cases')->where('id', $id)->update(['workflow' => json_encode($workflow), 'version' => $case->version + 1, 'updated_at' => now()]);
|
||||
$case = DB::table('desk_cases')->where('id', $id)->first(); $this->event($case, $actor, 'stage_updated', $data);
|
||||
$case = DB::table('desk_cases')->where('id', $id)->firstOrFail();
|
||||
$this->event($case, $actor, 'stage_updated', $data);
|
||||
|
||||
return $case;
|
||||
});
|
||||
}
|
||||
|
||||
public function event(object $case, User $actor, string $action, array $payload, ?object $request = null): void
|
||||
/** @param array<string,mixed> $payload */
|
||||
public function event(\stdClass $case, User $actor, string $action, array $payload, ?\stdClass $request = null): void
|
||||
{
|
||||
$id = (string) Str::uuid(); $version = $request->version ?? $case->version;
|
||||
$id = (string) Str::uuid();
|
||||
$version = $request->version ?? $case->version;
|
||||
DB::table('desk_events')->insert(['id' => $id, 'case_id' => $case->id, 'request_id' => $request->id ?? null, 'actor_id' => $actor->id, 'action' => $action, 'version' => $version, 'payload' => $this->encode($payload)]);
|
||||
DB::table('audit_events')->insert(['source_system' => 'virtual-desk', 'entity_type' => $request ? 'desk_requests' : 'desk_cases', 'entity_id' => $request->id ?? $case->id, 'actor_user_id' => $actor->id, 'action' => $action,
|
||||
'after_data' => json_encode(['event_id' => $id, 'event_version' => 1, 'version' => $version, 'case_id' => $case->id, 'source_hash' => config('virtual-desk.source.sha256')])]);
|
||||
$recipients = [(int) $case->user_id];
|
||||
if ($case->organization_id) $recipients = [...$recipients, ...DB::table('memberships')->where('organization_id', $case->organization_id)->whereNull('deleted_at')->pluck('user_id')->all()];
|
||||
if ($request?->analyst_id) $recipients[] = (int) $request->analyst_id;
|
||||
foreach (array_unique($recipients) as $user) DB::table('desk_notifications')->insert(['event_id' => $id, 'user_id' => $user, 'title' => config('virtual-desk.request_states.'.$request?->status, 'Dossier atualizado'), 'href' => $request ? '/balcao-virtual/pedidos/'.$request->id : '/balcao-virtual/dossiers/'.$case->id]);
|
||||
if ($case->organization_id) {
|
||||
$recipients = [...DB::table('memberships')->where('organization_id', $case->organization_id)->whereNull('deleted_at')->pluck('user_id')->all()];
|
||||
}
|
||||
if ($request?->analyst_id) {
|
||||
$recipients[] = (int) $request->analyst_id;
|
||||
}
|
||||
foreach (array_unique($recipients) as $user) {
|
||||
DB::table('desk_notifications')->insert(['event_id' => $id, 'user_id' => $user, 'title' => config('virtual-desk.request_states.'.$request?->status, 'Dossier atualizado'), 'href' => $request ? '/balcao-virtual/pedidos/'.$request->id : '/balcao-virtual/dossiers/'.$case->id]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\Malware\MalwareScanner;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class VirtualDeskInstallation
|
||||
{
|
||||
public const MIGRATIONS = ['2026_10_07_190000_create_internal_virtual_desk', '2026_10_08_120000_extend_desk_investor_guidance'];
|
||||
|
||||
public function fingerprint(): string
|
||||
{
|
||||
$files = [...(glob(app_path('Services/Ai/InvestorGuidance*.php')) ?: []), app_path('Services/VirtualDesk.php'), __FILE__,
|
||||
app_path('Http/Controllers/VirtualDeskController.php'), app_path('Http/Controllers/VirtualDeskOperationsController.php'), app_path('Http/Controllers/InvestorGuidanceController.php'),
|
||||
app_path('Http/Controllers/VirtualDeskInstallationController.php'), app_path('Http/Middleware/VirtualDeskInstalled.php'), app_path('Services/InstitutionalLinkMonitor.php'),
|
||||
app_path('Services/Ai/EvidenceRepository.php'), base_path('routes/virtual-desk.php'), config_path('virtual-desk.php'), app_path('Http/Controllers/PortalServicesController.php'), app_path('Http/Middleware/HandleInertiaRequests.php'), resource_path('js/app.tsx'),
|
||||
resource_path('js/lib/business-financial-plan.ts'), resource_path('js/lib/financial-calculators.ts'), resource_path('js/lib/project-dossier.ts'),
|
||||
...array_map(fn ($name) => database_path('migrations/'.$name.'.php'), self::MIGRATIONS),
|
||||
...(glob(resource_path('js/pages/desk/*.tsx')) ?: []), resource_path('js/components/investor-guidance.tsx'), resource_path('js/components/journey-companion.tsx'), resource_path('js/components/desk-internal-operations.tsx'), resource_path('js/layouts/portal-navigation.tsx'), resource_path('js/layouts/app/app-sidebar-layout.tsx'), resource_path('js/pages/investment/shared.tsx'), resource_path('js/lib/investor-guidance-finance.ts')];
|
||||
sort($files);
|
||||
|
||||
return hash('sha256', json_encode(array_map(fn ($file) => hash_file('sha256', $file), $files), JSON_THROW_ON_ERROR));
|
||||
}
|
||||
|
||||
/** @return array<string,mixed> */
|
||||
public function preview(): array
|
||||
{
|
||||
$identity = DB::selectOne('select current_database() as database, inet_server_port() as port');
|
||||
$directory = config('virtual-desk.proof_directory', storage_path('app/private/virtual-desk'));
|
||||
$read = function ($file) use ($directory) {
|
||||
if (! is_file($directory.'/'.$file)) {
|
||||
return [];
|
||||
}
|
||||
$contents = file_get_contents($directory.'/'.$file);
|
||||
abort_unless(is_string($contents), 503, 'Evidência privada indisponível.');
|
||||
|
||||
return json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
|
||||
};
|
||||
$backup = $read('backup-proof.json');
|
||||
$qa = $read('guidance-qa-evidence.json');
|
||||
$compatibility = $read('activation-compatibility.json');
|
||||
$code = $this->fingerprint();
|
||||
$backupVerified = ($backup['restore_verified'] ?? false) && ($backup['target']['database'] ?? null) === $identity->database
|
||||
&& (int) ($backup['target']['port'] ?? 0) === (int) $identity->port && is_file($backup['dump_path'] ?? '')
|
||||
&& hash_file('sha256', $backup['dump_path']) === ($backup['sha256'] ?? null)
|
||||
&& ($backup['files_verified'] ?? false) && is_file($backup['files_path'] ?? '')
|
||||
&& hash_file('sha256', $backup['files_path']) === ($backup['files_sha256'] ?? null)
|
||||
&& strtotime($backup['verified_at'] ?? '') >= time() - 86400;
|
||||
$qaVerified = ($qa['passed'] ?? false) && ($qa['code_hash'] ?? null) === $code && str_ends_with($qa['database'] ?? '', '_test') && (int) ($qa['port'] ?? 0) === 55441;
|
||||
$compatibilityVerified = $backupVerified && ($compatibility['passed'] ?? false) && ($compatibility['code_hash'] ?? null) === $code
|
||||
&& ($compatibility['dump_sha256'] ?? null) === ($backup['sha256'] ?? null)
|
||||
&& ($compatibility['empty_rollback_verified'] ?? false) && (int) ($compatibility['port'] ?? 0) === 55441;
|
||||
$counts = [];
|
||||
foreach (['desk_cases', 'desk_requests', 'desk_documents', 'desk_messages', 'tasks', 'investor_accounts', 'investment_submissions'] as $table) {
|
||||
$counts[$table] = Schema::hasTable($table) ? DB::table($table)->count() : null;
|
||||
}
|
||||
$installed = app(VirtualDesk::class)->installed();
|
||||
$privileges = DB::selectOne("select has_schema_privilege(current_user, 'public', 'CREATE') as can_create,
|
||||
pg_has_role(current_user, (select tableowner from pg_tables where schemaname='public' and tablename='tasks'), 'USAGE') as can_alter_tasks");
|
||||
$databaseReady = (bool) ($privileges->can_create && $privileges->can_alter_tasks);
|
||||
|
||||
return ['hash' => hash('sha256', json_encode([$identity, $code, $counts, $backupVerified, $qaVerified, $compatibilityVerified, $databaseReady], JSON_THROW_ON_ERROR)), 'code_hash' => $code,
|
||||
'installed' => $installed, 'target' => $identity, 'counts' => $counts, 'backupVerified' => $backupVerified, 'qaVerified' => $qaVerified,
|
||||
'compatibilityVerified' => $compatibilityVerified, 'backupDate' => $backup['verified_at'] ?? null,
|
||||
'databaseReady' => $databaseReady,
|
||||
'scanner' => app(MalwareScanner::class)->health(),
|
||||
'pendingMigrations' => array_values(array_filter(self::MIGRATIONS, fn ($name) => ! DB::table('migrations')->where('migration', $name)->exists())),
|
||||
'rollback' => 'Reversão vazia ensaiada em cópia isolada. Com dados novos, preservar/exportar pedidos e tarefas; o down recusa eliminar dados. Restauro integral só numa janela autorizada, após reconciliar alterações posteriores ao backup.',
|
||||
'changes' => ['Completar o Balcão Virtual existente, preservando pedidos anteriores.', 'Acrescentar associações opcionais ao investidor, projecto e percurso existentes.',
|
||||
'Reutilizar tasks do CRM para acções expressamente assumidas; sem envio externo.', 'Nenhuma publicação de serviço, documento ou candidatura automática.']];
|
||||
}
|
||||
|
||||
public function apply(string $hash, User $actor): void
|
||||
{
|
||||
abort_unless($actor->can('manage-portal'), 403);
|
||||
DB::transaction(function () use ($hash, $actor) {
|
||||
DB::select('select pg_advisory_xact_lock(261008, 1)');
|
||||
$preview = $this->preview();
|
||||
abort_unless(hash_equals($preview['hash'], $hash), 409, 'Pré-visualização alterada.');
|
||||
if ($preview['installed']) {
|
||||
return;
|
||||
}
|
||||
abort_unless($preview['backupVerified'] && $preview['qaVerified'] && $preview['compatibilityVerified'] && $preview['databaseReady'], 422, 'Backup/restauro atualizado, compatibilidade/reversão, permissões da base e QA desta versão continuam necessários.');
|
||||
config(['virtual-desk.installation_authorized' => true]);
|
||||
try {
|
||||
foreach (self::MIGRATIONS as $name) {
|
||||
if (! DB::table('migrations')->where('migration', $name)->exists()) {
|
||||
(require database_path('migrations/'.$name.'.php'))->up();
|
||||
DB::table('migrations')->insert(['migration' => $name, 'batch' => (DB::table('migrations')->max('batch') ?? 0) + 1]);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
config(['virtual-desk.installation_authorized' => false]);
|
||||
}
|
||||
DB::table('audit_events')->insert(['source_system' => 'virtual-desk', 'entity_type' => 'desk_cases', 'actor_user_id' => $actor->id,
|
||||
'action' => 'desk_installed', 'after_data' => json_encode(['preview_hash' => $hash, 'code_hash' => $preview['code_hash'], 'prior_counts' => $preview['counts']])]);
|
||||
});
|
||||
}
|
||||
}
|
||||
+3
-4
@@ -1,14 +1,13 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Middleware\EnsureSimulationRecoveryCookie;
|
||||
use App\Http\Middleware\HandleAppearance;
|
||||
use App\Http\Middleware\HandleInertiaRequests;
|
||||
use App\Http\Middleware\PreserveFormFeedback;
|
||||
use App\Http\Middleware\EnsureSimulationRecoveryCookie;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Database\ConnectionException;
|
||||
use Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -44,7 +43,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
);
|
||||
}
|
||||
|
||||
if (($exception instanceof QueryException || $exception instanceof ConnectionException || $exception instanceof \PDOException)
|
||||
if (($exception instanceof QueryException || $exception instanceof PDOException)
|
||||
&& ! $request->is('api/v1/simulation-drafts*')) {
|
||||
$reference = (string) ($response->headers->get('X-Correlation-ID')
|
||||
?? $request->attributes->get('correlation_id')
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'operator_name' => env('PORTAL_OPERATOR_NAME'),
|
||||
'support' => [
|
||||
'email' => env('PORTAL_SUPPORT_EMAIL'),
|
||||
'phone' => env('PORTAL_SUPPORT_PHONE'),
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
$source = ['identifier' => 'Plano de Incorporação e Optimização do Módulo Balcão Virtual — Portal do Investidor',
|
||||
'sha256' => 'f167d9156c2572c0253949a6f0ed88aafc28450cce902e8f445e3d979119aa37', 'edition' => 'Plano fornecido em 7 outubro 2026',
|
||||
'legal_status' => 'not_a_legal_instrument', 'form_status' => 'digital_preparation', 'rule_use' => 'internal_workflow_only'];
|
||||
'legal_status' => 'not_a_legal_instrument', 'form_status' => 'digital_preparation', 'rule_use' => 'internal_workflow_only',
|
||||
'legal_claims_status' => 'source_assertions_not_independently_verified', 'official_rates_deadlines_eligibility' => 'indeterminate'];
|
||||
$steps = [
|
||||
'E0' => ['Perfil e preparação', [], 'Portal do Investidor', 'Dados mínimos do promotor e do projeto; reutilização consentida.'],
|
||||
'E1' => ['Reserva do nome', ['E0'], 'SARN', 'Nome aprovado, identificação do pedido, comprovativo e validade.'],
|
||||
@@ -21,6 +22,7 @@ $field = fn ($label, $type = 'text', $required = false, $extra = []) => ['label'
|
||||
$common = ['title' => $field('Nome do projeto', 'text', true), 'description' => $field('Descrição e objetivo', 'textarea', true),
|
||||
'territory_id' => $field('Localização documentada', 'territory'), 'location_source' => $field('Fonte da localização'),
|
||||
'regime' => $field('Regime declarado', 'select', false, ['options' => ['unknown' => 'Por determinar', 'registration' => 'Mero registo declarado', 'authorization' => 'Autorização declarada', 'special' => 'Regime especial declarado']])];
|
||||
|
||||
return [
|
||||
'installation_authorized' => false,
|
||||
'source' => $source,
|
||||
|
||||
@@ -1,18 +1,291 @@
|
||||
{
|
||||
"source_document_attached": true,
|
||||
"source_document_reconciled": true,
|
||||
"source_document_title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_document_note": "Documento do utilizador reconciliado com fontes institucionais e primárias acessíveis. Alegações promocionais não são tratadas como actividade autorizada, operação ou disponibilidade.",
|
||||
"consulted_at": "2026-10-04",
|
||||
"records": [
|
||||
{"code":"MOZ-TOPUITO-PARK","name":"Parque Industrial de Topuito","category":"industrial_park","physical_category":"industrial_park","legal_regime":"pending","development_status":"in_development","availability_status":"reported_unverified","territory_note":"The source names Topuito in Larde; the master dataset has no district row for Larde. This candidate is linked only at Nampula province until the territorial mapping is reviewed.","territories":[{"name":"NAMPULA","type":"province","scope_type":"administrative"}],"source":{"title":"Construction begins on SME Village at Topuito Industrial Park","url":"https://mozparks.co.mz/construction-begins-on-sme-village-at-topuito-industrial-park/","source_type":"promoter","supports":"Project and development activity, not operational availability."},"areas":[{"type":"total","value":215,"unit":"ha","status":"disputed","source_reference":"MozParks brochure 2023, source scope to review"},{"type":"total","value":200,"unit":"ha","status":"disputed","source_reference":"MozParks brochure 2025, source scope to review"},{"type":"developed","value":30,"unit":"ha","status":"reported","source_reference":"MozParks brochure 2025; not a land availability statement"}]},
|
||||
{"code":"MOZ-TOPUITO-ZFI","name":"Zona Franca Industrial de Topuito","category":"zfi","physical_category":"industrial_free_zone","legal_regime":"zfi","development_status":"planned","availability_status":"not_informed","territory_note":"The decree names Topuito in Larde; the master dataset has no district row for Larde. This candidate is linked only at Nampula province until the territorial mapping is reviewed.","territories":[{"name":"NAMPULA","type":"province","scope_type":"administrative"}],"source":{"title":"Decreto n.º 57/2023 — criação da ZFI de Topuito","url":"https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-190-de-031023-boletim-da-rep%C3%BAblica-i-s%C3%A9rie-p%C3%A1g-2349","source_type":"government","supports":"Legal creation and reported legal area; primary review remains required."},"areas":[{"type":"legal","value":65,"unit":"ha","status":"reported","source_reference":"Decreto n.º 57/2023, artigo 1.º — confirm against retained official text"}]},
|
||||
{"code":"MOZ-MUNHAVA-PARK","name":"Parque Industrial e Logístico de Munhava","category":"industrial_park","physical_category":"industrial_logistics_park","legal_regime":"pending","development_status":"in_development","availability_status":"not_informed","territories":[{"name":"CIDADE DA BEIRA","type":"district","scope_type":"administrative"}],"source":{"title":"Parque Industrial e Logístico — SDUBeira","url":"https://sdubeira.co.mz/project/parque-industrial-logistico/","source_type":"promoter","supports":"Project existence, location and reported development status."},"areas":[{"type":"total","value":900,"unit":"ha","status":"disputed","source_reference":"SDUBeira project page, total area scope needs confirmation"},{"type":"total","value":990,"unit":"ha","status":"disputed","source_reference":"SDUBeira project headline, conflicts with page summary"},{"type":"pilot","value":50,"unit":"ha","status":"reported","source_reference":"SDUBeira project page; not available-land confirmation"}]},
|
||||
{"code":"MOZ-CDP-MONTEPUEZ","name":"Parque Industrial de Montepuez","category":"industrial_park","physical_category":"industrial_park","legal_regime":"pending","development_status":"planned","availability_status":"not_informed","territories":[{"name":"MONTEPUEZ","type":"district","scope_type":"administrative"}],"source":{"title":"MozParks — Cabo Delgado parks initiative","url":"https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/","source_type":"promoter","supports":"District association within a provincial parks initiative; not an operating facility claim."},"areas":[]},
|
||||
{"code":"MOZ-CDP-BALAMA","name":"Parque Industrial de Balama","category":"industrial_park","physical_category":"industrial_park","legal_regime":"pending","development_status":"planned","availability_status":"not_informed","territories":[{"name":"BALAMA","type":"district","scope_type":"administrative"}],"source":{"title":"MozParks — Cabo Delgado parks initiative","url":"https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/","source_type":"promoter","supports":"District association within a provincial parks initiative; not an operating facility claim."},"areas":[]},
|
||||
{"code":"MOZ-CDP-ANCUABE","name":"Parque Industrial de Ancuabe","category":"industrial_park","physical_category":"industrial_park","legal_regime":"pending","development_status":"planned","availability_status":"not_informed","territories":[{"name":"ANCUABE","type":"district","scope_type":"administrative"}],"source":{"title":"MozParks — Cabo Delgado parks initiative","url":"https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/","source_type":"promoter","supports":"District association within a provincial parks initiative; not an operating facility claim."},"areas":[]},
|
||||
{"code":"MOZ-CDP-PALMA","name":"Parque Industrial de Palma","category":"industrial_park","physical_category":"industrial_park","legal_regime":"pending","development_status":"planned","availability_status":"not_informed","territories":[{"name":"PALMA","type":"district","scope_type":"administrative"}],"source":{"title":"MozParks — Cabo Delgado parks initiative","url":"https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/","source_type":"promoter","supports":"District association within a provincial parks initiative; not an operating facility claim."},"areas":[]},
|
||||
{"code":"MOZ-REVUBUE-ZFI","name":"Zona Franca Industrial de Revúbuè","category":"zfi","physical_category":"industrial_free_zone","legal_regime":"zfi","development_status":"planned","availability_status":"not_informed","territories":[{"name":"CHIUTA","type":"district","scope_type":"legal_scope"},{"name":"MOATIZE","type":"district","scope_type":"legal_scope"}],"source":{"title":"Revúbuè Free Industrial Zone — APIEX","url":"https://apiex.gov.mz/revubue-free-industrial-zone/","source_type":"institutional","supports":"APIEX-reported identity, district scope and area; primary legal text pending."},"areas":[{"type":"legal","value":4456,"unit":"ha","status":"reported","source_reference":"APIEX page; confirm perimeter against Decreto 41/2016"}]},
|
||||
{"code":"MOZ-MINHEWENE-ZFI","name":"Zona Franca Industrial de Minhewene","category":"zfi","physical_category":"industrial_free_zone","legal_regime":"zfi","development_status":"planned","availability_status":"not_informed","territories":[{"name":"NAMPULA","type":"province","scope_type":"administrative"}],"source":{"title":"Zona Franca Industrial de Minhewene — APIEX","url":"https://apiex.gov.mz/zona-franca-industrial-de-minhewene/","source_type":"institutional","supports":"APIEX-reported identity, province and area; primary legal text and site precision pending."},"areas":[{"type":"legal","value":350,"unit":"ha","status":"reported","source_reference":"APIEX page; confirm perimeter against Decreto 51/2011"}]}
|
||||
]
|
||||
"source_document_attached": true,
|
||||
"source_document_reconciled": true,
|
||||
"source_document_title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_document_note": "Documento do utilizador reconciliado com fontes institucionais e primárias acessíveis. Alegações promocionais não são tratadas como actividade autorizada, operação ou disponibilidade.",
|
||||
"consulted_at": "2026-10-04",
|
||||
"records": [
|
||||
{
|
||||
"code": "MOZ-TOPUITO-PARK",
|
||||
"name": "Parque Industrial de Topuito",
|
||||
"category": "industrial_park",
|
||||
"physical_category": "industrial_park",
|
||||
"legal_regime": "pending",
|
||||
"development_status": "in_development",
|
||||
"availability_status": "reported_unverified",
|
||||
"territory_note": "The source names Topuito in Larde; the master dataset has no district row for Larde. This candidate is linked only at Nampula province until the territorial mapping is reviewed.",
|
||||
"territories": [
|
||||
{
|
||||
"name": "NAMPULA",
|
||||
"type": "province",
|
||||
"scope_type": "administrative"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"title": "Construction begins on SME Village at Topuito Industrial Park",
|
||||
"url": "https://mozparks.co.mz/construction-begins-on-sme-village-at-topuito-industrial-park/",
|
||||
"source_type": "promoter",
|
||||
"supports": "Project and development activity, not operational availability."
|
||||
},
|
||||
"areas": [
|
||||
{
|
||||
"type": "total",
|
||||
"value": 215,
|
||||
"unit": "ha",
|
||||
"status": "disputed",
|
||||
"source_reference": "MozParks brochure 2023, source scope to review"
|
||||
},
|
||||
{
|
||||
"type": "total",
|
||||
"value": 200,
|
||||
"unit": "ha",
|
||||
"status": "disputed",
|
||||
"source_reference": "MozParks brochure 2025, source scope to review"
|
||||
},
|
||||
{
|
||||
"type": "developed",
|
||||
"value": 30,
|
||||
"unit": "ha",
|
||||
"status": "reported",
|
||||
"source_reference": "MozParks brochure 2025; not a land availability statement"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-TOPUITO-ZFI",
|
||||
"name": "Zona Franca Industrial de Topuito",
|
||||
"category": "zfi",
|
||||
"physical_category": "industrial_free_zone",
|
||||
"legal_regime": "zfi",
|
||||
"development_status": "planned",
|
||||
"availability_status": "not_informed",
|
||||
"territory_note": "The decree names Topuito in Larde; the master dataset has no district row for Larde. This candidate is linked only at Nampula province until the territorial mapping is reviewed.",
|
||||
"territories": [
|
||||
{
|
||||
"name": "NAMPULA",
|
||||
"type": "province",
|
||||
"scope_type": "administrative"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"title": "Decreto n.º 57/2023 — criação da ZFI de Topuito",
|
||||
"url": "https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-190-de-031023-boletim-da-rep%C3%BAblica-i-s%C3%A9rie-p%C3%A1g-2349",
|
||||
"source_type": "government",
|
||||
"supports": "Legal creation and reported legal area; primary review remains required."
|
||||
},
|
||||
"areas": [
|
||||
{
|
||||
"type": "legal",
|
||||
"value": 65,
|
||||
"unit": "ha",
|
||||
"status": "reported",
|
||||
"source_reference": "Decreto n.º 57/2023, artigo 1.º — confirm against retained official text"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-MUNHAVA-PARK",
|
||||
"name": "Parque Industrial e Logístico de Munhava",
|
||||
"category": "industrial_park",
|
||||
"physical_category": "industrial_logistics_park",
|
||||
"legal_regime": "pending",
|
||||
"development_status": "in_development",
|
||||
"availability_status": "not_informed",
|
||||
"territories": [
|
||||
{
|
||||
"name": "CIDADE DA BEIRA",
|
||||
"type": "district",
|
||||
"scope_type": "administrative"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"title": "Parque Industrial e Logístico — SDUBeira",
|
||||
"url": "https://sdubeira.co.mz/project/parque-industrial-logistico/",
|
||||
"source_type": "promoter",
|
||||
"supports": "Project existence, location and reported development status."
|
||||
},
|
||||
"areas": [
|
||||
{
|
||||
"type": "total",
|
||||
"value": 900,
|
||||
"unit": "ha",
|
||||
"status": "disputed",
|
||||
"source_reference": "SDUBeira project page, total area scope needs confirmation"
|
||||
},
|
||||
{
|
||||
"type": "total",
|
||||
"value": 990,
|
||||
"unit": "ha",
|
||||
"status": "disputed",
|
||||
"source_reference": "SDUBeira project headline, conflicts with page summary"
|
||||
},
|
||||
{
|
||||
"type": "pilot",
|
||||
"value": 50,
|
||||
"unit": "ha",
|
||||
"status": "reported",
|
||||
"source_reference": "SDUBeira project page; not available-land confirmation"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-CDP-MONTEPUEZ",
|
||||
"name": "Parque Industrial de Montepuez",
|
||||
"category": "industrial_park",
|
||||
"physical_category": "industrial_park",
|
||||
"legal_regime": "pending",
|
||||
"development_status": "planned",
|
||||
"availability_status": "not_informed",
|
||||
"territories": [
|
||||
{
|
||||
"name": "MONTEPUEZ",
|
||||
"type": "district",
|
||||
"scope_type": "administrative"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"title": "MozParks — Cabo Delgado parks initiative",
|
||||
"url": "https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/",
|
||||
"source_type": "promoter",
|
||||
"supports": "District association within a provincial parks initiative; not an operating facility claim."
|
||||
},
|
||||
"areas": []
|
||||
},
|
||||
{
|
||||
"code": "MOZ-CDP-BALAMA",
|
||||
"name": "Parque Industrial de Balama",
|
||||
"category": "industrial_park",
|
||||
"physical_category": "industrial_park",
|
||||
"legal_regime": "pending",
|
||||
"development_status": "planned",
|
||||
"availability_status": "not_informed",
|
||||
"territories": [
|
||||
{
|
||||
"name": "BALAMA",
|
||||
"type": "district",
|
||||
"scope_type": "administrative"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"title": "MozParks — Cabo Delgado parks initiative",
|
||||
"url": "https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/",
|
||||
"source_type": "promoter",
|
||||
"supports": "District association within a provincial parks initiative; not an operating facility claim."
|
||||
},
|
||||
"areas": []
|
||||
},
|
||||
{
|
||||
"code": "MOZ-CDP-ANCUABE",
|
||||
"name": "Parque Industrial de Ancuabe",
|
||||
"category": "industrial_park",
|
||||
"physical_category": "industrial_park",
|
||||
"legal_regime": "pending",
|
||||
"development_status": "planned",
|
||||
"availability_status": "not_informed",
|
||||
"territories": [
|
||||
{
|
||||
"name": "ANCUABE",
|
||||
"type": "district",
|
||||
"scope_type": "administrative"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"title": "MozParks — Cabo Delgado parks initiative",
|
||||
"url": "https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/",
|
||||
"source_type": "promoter",
|
||||
"supports": "District association within a provincial parks initiative; not an operating facility claim."
|
||||
},
|
||||
"areas": []
|
||||
},
|
||||
{
|
||||
"code": "MOZ-CDP-PALMA",
|
||||
"name": "Parque Industrial de Palma",
|
||||
"category": "industrial_park",
|
||||
"physical_category": "industrial_park",
|
||||
"legal_regime": "pending",
|
||||
"development_status": "planned",
|
||||
"availability_status": "not_informed",
|
||||
"territories": [
|
||||
{
|
||||
"name": "PALMA",
|
||||
"type": "district",
|
||||
"scope_type": "administrative"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"title": "MozParks — Cabo Delgado parks initiative",
|
||||
"url": "https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/",
|
||||
"source_type": "promoter",
|
||||
"supports": "District association within a provincial parks initiative; not an operating facility claim."
|
||||
},
|
||||
"areas": []
|
||||
},
|
||||
{
|
||||
"code": "MOZ-REVUBUE-ZFI",
|
||||
"name": "Zona Franca Industrial de Revúbuè",
|
||||
"category": "zfi",
|
||||
"physical_category": "industrial_free_zone",
|
||||
"legal_regime": "zfi",
|
||||
"development_status": "planned",
|
||||
"availability_status": "not_informed",
|
||||
"territories": [
|
||||
{
|
||||
"name": "CHIUTA",
|
||||
"type": "district",
|
||||
"scope_type": "legal_scope"
|
||||
},
|
||||
{
|
||||
"name": "MOATIZE",
|
||||
"type": "district",
|
||||
"scope_type": "legal_scope"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"title": "Revúbuè Free Industrial Zone — APIEX",
|
||||
"url": "https://apiex.gov.mz/revubue-free-industrial-zone/",
|
||||
"source_type": "institutional",
|
||||
"supports": "APIEX-reported identity, district scope and area; primary legal text pending."
|
||||
},
|
||||
"areas": [
|
||||
{
|
||||
"type": "legal",
|
||||
"value": 4456,
|
||||
"unit": "ha",
|
||||
"status": "reported",
|
||||
"source_reference": "APIEX page; confirm perimeter against Decreto 41/2016"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-MINHEWENE-ZFI",
|
||||
"name": "Zona Franca Industrial de Minhewene",
|
||||
"category": "zfi",
|
||||
"physical_category": "industrial_free_zone",
|
||||
"legal_regime": "zfi",
|
||||
"development_status": "planned",
|
||||
"availability_status": "not_informed",
|
||||
"territories": [
|
||||
{
|
||||
"name": "NAMPULA",
|
||||
"type": "province",
|
||||
"scope_type": "administrative"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"title": "Zona Franca Industrial de Minhewene — APIEX",
|
||||
"url": "https://apiex.gov.mz/zona-franca-industrial-de-minhewene/",
|
||||
"source_type": "institutional",
|
||||
"supports": "APIEX-reported identity, province and area; primary legal text and site precision pending."
|
||||
},
|
||||
"areas": [
|
||||
{
|
||||
"type": "legal",
|
||||
"value": 350,
|
||||
"unit": "ha",
|
||||
"status": "reported",
|
||||
"source_reference": "APIEX page; confirm perimeter against Decreto 51/2011"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,271 +1,248 @@
|
||||
{
|
||||
"source_document": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"provided": true,
|
||||
"received_at": "2026-10-04",
|
||||
"classification": "documento fornecido pelo utilizador; referncias citadas sem URLs/ficheiros primrios anexos; claims no so por si s prova oficial",
|
||||
"note": "As afirmaes do documento foram comparadas com pginas institucionais/promotoras e referncias oficiais acessveis. Manter alegaes promocionais separadas de existncia jurdica, autorizao de actividade, operao e disponibilidade."
|
||||
},
|
||||
"consulted_at": "2026-10-04",
|
||||
"candidates": [
|
||||
{
|
||||
"name": "Beluluane",
|
||||
"outcome": "reconcile_existing",
|
||||
"prepared_codes": [
|
||||
"MOZ-BELULUANE-PARK",
|
||||
"MOZ-BELULUANE-ZFI"
|
||||
],
|
||||
"source": "https://apiex.gov.mz/beluluane-industrial-park-free-zone/",
|
||||
"known": "MozParks apresenta 700 ha prontos para arrendamento; APIEX apresenta complexo de cerca de 780 ha: parque 600 ha, Mozal 180 ha e ZFI 55 ha. So mbitos que no podem ser somados nem equiparados a disponibilidade.",
|
||||
"pending": "Confirmar limites e sobreposio cartogrfica/documental, sobretudo relao entre 700 ha e parque de 600 ha. No publicar actividades como autorizadas nem declarar lotes disponveis.",
|
||||
"user_document_claims": {
|
||||
"status": "operacional e em expanso (alegao documental/promotora)",
|
||||
"area": "700 ha (alegao MozParks no portal actual); actividades/enfoque: manufactura, alumnio, cabos, agro-processamento, tecnologia, logstica e servios",
|
||||
"assessment": "MozParks apresenta 700 ha prontos para arrendamento; APIEX descreve um complexo de cerca de 780 ha composto por parque de 600 ha e Mozal 180 ha, e uma ZFI de 55 ha. Escopos no reconciliados; nenhum nmero prova disponibilidade actual."
|
||||
},
|
||||
"claim_source": {
|
||||
"source_document": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
"provided": true,
|
||||
"received_at": "2026-10-04",
|
||||
"classification": "documento fornecido pelo utilizador; referncias citadas sem URLs/ficheiros primrios anexos; claims no so por si s prova oficial",
|
||||
"note": "As afirmaes do documento foram comparadas com pginas institucionais/promotoras e referncias oficiais acessveis. Manter alegaes promocionais separadas de existncia jurdica, autorizao de actividade, operao e disponibilidade."
|
||||
},
|
||||
{
|
||||
"name": "Topuito",
|
||||
"outcome": "prepare_separate_park_and_zfi",
|
||||
"prepared_codes": [
|
||||
"MOZ-TOPUITO-PARK",
|
||||
"MOZ-TOPUITO-ZFI"
|
||||
],
|
||||
"source": "https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-190-de-031023-boletim-da-rep%C3%BAblica-i-s%C3%A9rie-p%C3%A1g-2349",
|
||||
"known": "MozParks apresenta Topuito como parque e oferta de 215 ha; notcia de Agosto de 2026 confirma incio da construo do SME Village. Decreto 57/2023 cria separadamente ZFI com rea legal de 65 ha.",
|
||||
"pending": "O parque, a obra do SME Village e a ZFI so escopos distintos. Verificar limites, operao e disponibilidade actual. Base territorial no contm distrito Larde; manter associao apenas provincial at reviso.",
|
||||
"user_document_claims": {
|
||||
"status": "operacional/em expanso (alegao documental/promotora)",
|
||||
"area": "215 ha (documento e perfil institucional MozParks)",
|
||||
"assessment": "MozParks identifica Topuito Industrial Park e notcia de Agosto de 2026 informa incio de construo do SME Village. A ZFI legal de 65 ha entidade/permetro distinto. No inferir que toda a rea esteja pronta ou disponvel."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
"consulted_at": "2026-10-04",
|
||||
"candidates": [
|
||||
{
|
||||
"name": "Beluluane",
|
||||
"outcome": "reconcile_existing",
|
||||
"prepared_codes": ["MOZ-BELULUANE-PARK", "MOZ-BELULUANE-ZFI"],
|
||||
"source": "https://apiex.gov.mz/beluluane-industrial-park-free-zone/",
|
||||
"known": "MozParks apresenta 700 ha prontos para arrendamento; APIEX apresenta complexo de cerca de 780 ha: parque 600 ha, Mozal 180 ha e ZFI 55 ha. So mbitos que no podem ser somados nem equiparados a disponibilidade.",
|
||||
"pending": "Confirmar limites e sobreposio cartogrfica/documental, sobretudo relao entre 700 ha e parque de 600 ha. No publicar actividades como autorizadas nem declarar lotes disponveis.",
|
||||
"user_document_claims": {
|
||||
"status": "operacional e em expanso (alegao documental/promotora)",
|
||||
"area": "700 ha (alegao MozParks no portal actual); actividades/enfoque: manufactura, alumnio, cabos, agro-processamento, tecnologia, logstica e servios",
|
||||
"assessment": "MozParks apresenta 700 ha prontos para arrendamento; APIEX descreve um complexo de cerca de 780 ha composto por parque de 600 ha e Mozal 180 ha, e uma ZFI de 55 ha. Escopos no reconciliados; nenhum nmero prova disponibilidade actual."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Topuito",
|
||||
"outcome": "prepare_separate_park_and_zfi",
|
||||
"prepared_codes": ["MOZ-TOPUITO-PARK", "MOZ-TOPUITO-ZFI"],
|
||||
"source": "https://www.inm.gov.mz/pt-br/content/conselho-de-ministros-br-n%C2%BA-190-de-031023-boletim-da-rep%C3%BAblica-i-s%C3%A9rie-p%C3%A1g-2349",
|
||||
"known": "MozParks apresenta Topuito como parque e oferta de 215 ha; notcia de Agosto de 2026 confirma incio da construo do SME Village. Decreto 57/2023 cria separadamente ZFI com rea legal de 65 ha.",
|
||||
"pending": "O parque, a obra do SME Village e a ZFI so escopos distintos. Verificar limites, operao e disponibilidade actual. Base territorial no contm distrito Larde; manter associao apenas provincial at reviso.",
|
||||
"user_document_claims": {
|
||||
"status": "operacional/em expanso (alegao documental/promotora)",
|
||||
"area": "215 ha (documento e perfil institucional MozParks)",
|
||||
"assessment": "MozParks identifica Topuito Industrial Park e notcia de Agosto de 2026 informa incio de construo do SME Village. A ZFI legal de 65 ha entidade/permetro distinto. No inferir que toda a rea esteja pronta ou disponvel."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Munhava",
|
||||
"outcome": "prepare_informational_candidate",
|
||||
"prepared_codes": ["MOZ-MUNHAVA-PARK"],
|
||||
"source": "https://sdubeira.co.mz/project/parque-industrial-logistico/",
|
||||
"known": "SDUBeira descreve o projecto em fases, rea total de 900 ha no resumo e piloto de 50 ha; o destaque de 990 ha na mesma pgina cria divergncia.",
|
||||
"pending": "Documento de entrada acrescenta faixa aproximada de 44-60 ha para piloto; no corresponde a metragem confirmada. Obter permetro/plano primrio e no declarar terreno disponvel.",
|
||||
"user_document_claims": {
|
||||
"status": "desenvolvimento faseado",
|
||||
"area": "900 ha total e piloto de cerca de 44-60 ha (alegao documental); actividades/enfoque: logstica porturia, armazenagem, manufactura, agro-indstria e pescas",
|
||||
"assessment": "SDUBeira descreve 900 ha no resumo e 50 ha piloto; a mesma pgina contm destaque de 990 ha. Conservar divergncia editorial e no tratar piloto como terreno livre."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Montepuez",
|
||||
"outcome": "prepare_district_candidate",
|
||||
"prepared_codes": ["MOZ-CDP-MONTEPUEZ"],
|
||||
"source": "https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/",
|
||||
"known": "A iniciativa MozParks menciona parque/rede de Cabo Delgado no distrito de Montepuez. MozParks confirma a rede/parques em desenvolvimento; actividades do documento so apenas enfoque promocional.",
|
||||
"pending": "Regime jurdico, reas, actividades efectivamente autorizadas/instaladas e disponibilidade permanecem por confirmar.",
|
||||
"user_document_claims": {
|
||||
"status": "planeado/em desenvolvimento",
|
||||
"activities": "fornecedores mineiros, transformao, servios e PME (enfoque alegado no documento)",
|
||||
"assessment": "MozParks confirma quatro parques em desenvolvimento nos distritos; actividades so foco de promoo, no licena ou operao confirmada."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Balama",
|
||||
"outcome": "prepare_district_candidate",
|
||||
"prepared_codes": ["MOZ-CDP-BALAMA"],
|
||||
"source": "https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/",
|
||||
"known": "A iniciativa MozParks menciona parque/rede de Cabo Delgado no distrito de Balama. MozParks confirma a rede/parques em desenvolvimento; actividades do documento so apenas enfoque promocional.",
|
||||
"pending": "Regime jurdico, reas, actividades efectivamente autorizadas/instaladas e disponibilidade permanecem por confirmar.",
|
||||
"user_document_claims": {
|
||||
"status": "planeado/em desenvolvimento",
|
||||
"activities": "minerao, servios industriais, fornecedores e PME (enfoque alegado no documento)",
|
||||
"assessment": "MozParks confirma a iniciativa/distrito; actividades permanecem promocionais e no so autorizaes."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Ancuabe",
|
||||
"outcome": "prepare_district_candidate",
|
||||
"prepared_codes": ["MOZ-CDP-ANCUABE"],
|
||||
"source": "https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/",
|
||||
"known": "A iniciativa MozParks menciona parque/rede de Cabo Delgado no distrito de Ancuabe. MozParks confirma a rede/parques em desenvolvimento; actividades do documento so apenas enfoque promocional.",
|
||||
"pending": "Regime jurdico, reas, actividades efectivamente autorizadas/instaladas e disponibilidade permanecem por confirmar.",
|
||||
"user_document_claims": {
|
||||
"status": "planeado/em desenvolvimento",
|
||||
"activities": "transformao, contedo local, servios industriais e PME (enfoque alegado no documento)",
|
||||
"assessment": "MozParks confirma a iniciativa/distrito; actividades permanecem promocionais e no so autorizaes."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Palma",
|
||||
"outcome": "prepare_district_candidate",
|
||||
"prepared_codes": ["MOZ-CDP-PALMA"],
|
||||
"source": "https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/",
|
||||
"known": "A iniciativa MozParks menciona parque/rede de Cabo Delgado no distrito de Palma. MozParks confirma a rede/parques em desenvolvimento; actividades do documento so apenas enfoque promocional.",
|
||||
"pending": "Regime jurdico, reas, actividades efectivamente autorizadas/instaladas e disponibilidade permanecem por confirmar.",
|
||||
"user_document_claims": {
|
||||
"status": "planeado/em desenvolvimento",
|
||||
"activities": "petrleo e gs, energia, logstica e cadeias locais (enfoque alegado no documento)",
|
||||
"assessment": "MozParks confirma a iniciativa/distrito; actividades permanecem promocionais e no so autorizaes."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Selva/Manica Parks",
|
||||
"outcome": "keep_unresolved_program_and_site_distinct",
|
||||
"prepared_codes": [],
|
||||
"source": "https://mozparks.co.mz/mozparks-launches-manica-parks-a-new-industrial-platform-for-manica-province/",
|
||||
"known": "MozParks confirma Manica Parks como iniciativa provincial lanada por memorando em Setembro de 2026. O documento fornecido associa a iniciativa a Selva/Vanduzi, mas no confirma instalao individual.",
|
||||
"pending": "No criar parque individual nem relao territorial de Selva sem fonte especfica. Actividades listadas so enfoque declarado, no autorizao.",
|
||||
"user_document_claims": {
|
||||
"status": "Manica Parks lanado por memorando em Setembro de 2026; alegao de localizao em Selva/Vanduzi permanece por confirmar",
|
||||
"activities": "agro-processamento, metalomecnica, combustveis, hotelaria e restaurao (enfoque alegado no documento)",
|
||||
"assessment": "MozParks confirma iniciativa provincial e memorando, no uma instalao individual em Selva nem incio de construo/operao."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Mocuba ZFI",
|
||||
"outcome": "reconcile_existing",
|
||||
"prepared_codes": ["MOZ-MOCUBA-ZFI", "MOZ-MOCUBA-ZEE"],
|
||||
"source": "https://apiex.gov.mz/zif/mocuba-free-industrial-zone/",
|
||||
"known": "APIEX confirma a ZFI de 50,58 ha e fase de mobilizao de recursos e operador. A lista de actividades do documento enfoque declarado, no autorizao.",
|
||||
"pending": "Confirmar diploma primrio, limites e actividades; manter ZFI e ZEE de Mocuba separadas.",
|
||||
"user_document_claims": {
|
||||
"status": "mobilizao de recursos e operador",
|
||||
"area": "50,58 ha segundo APIEX",
|
||||
"activities": "agro-florestal, pescas, txteis, calcrio, pedra e minerais (enfoque alegado no documento)",
|
||||
"assessment": "APIEX confirma 50,58 ha e mobilizao; confirmar actividades com fonte prpria antes de atribuir ao registo. Manter ZFI distinta da ZEE."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Revúbuè ZFI",
|
||||
"outcome": "prepare_one_record_two_districts",
|
||||
"prepared_codes": ["MOZ-REVUBUE-ZFI"],
|
||||
"source": "https://apiex.gov.mz/revubue-free-industrial-zone/",
|
||||
"known": "APIEX reporta a ZFI abrangendo Chiuta e Moatize, 4.456 ha, sem operador identificado.",
|
||||
"pending": "Obter e validar texto primário do Decreto 41/2016 e as relações territoriais. Um único registo, duas localizações.",
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Mocambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
},
|
||||
"user_document_claims": {
|
||||
"status": "sem operador; um projecto aprovado segundo APIEX",
|
||||
"area": "4 456 ha segundo APIEX",
|
||||
"activities": "industria ligada a recursos e transformacao (enfoque alegado no documento)",
|
||||
"assessment": "APIEX informa um projecto aprovado e potenciais sectores; isto nao transforma o projecto em oportunidade publicada nem comprova actividades autorizadas."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Minhewene ZFI",
|
||||
"outcome": "prepare_province_precision_candidate",
|
||||
"prepared_codes": ["MOZ-MINHEWENE-ZFI"],
|
||||
"source": "https://apiex.gov.mz/zona-franca-industrial-de-minhewene/",
|
||||
"known": "Pgina APIEX actualmente acessvel reporta Decreto 51/2011, Nampula/Cidade de Nacala, 350 ha e mobilizao de recursos/operador. O documento fornecido diz que a ficha no estava acessvel; esta afirmao diverge da consulta actual.",
|
||||
"pending": "Validar Decreto 51/2011 e localizao cadastral exacta; pgina institucional no substitui fonte legal primria nem comprova operacionalizao actual.",
|
||||
"user_document_claims": {
|
||||
"status": "mobilizao de recursos e operador segundo pgina APIEX acessvel na verificao actual",
|
||||
"area": "350 ha segundo APIEX",
|
||||
"assessment": "A alegao do documento de que no havia ficha acessvel diverge da pgina institucional APIEX actualmente acessvel, que reporta Decreto 51/2011, Nampula/Cidade de Nacala, 350 ha e mobilizao de recursos/operador. Acessibilidade da pgina no substitui validao do diploma primrio ou do estado actual."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"legislative_review": {
|
||||
"decree_57_2023": "Texto oficial localizado; criação da ZFI de Topuito e 65 ha confirmados. Não implica que operador, actividade operacional ou terrenos disponíveis estejam confirmados.",
|
||||
"decree_12_2026": "A página oficial consultada apresenta o sumário de aprovação do regulamento ZEE/ZFI e revogação do Decreto 75/99. O texto integral e regras transitórias/entrada em vigor ainda não foram revistos; não parametrizar simulador fiscal a partir do sumário."
|
||||
},
|
||||
{
|
||||
"name": "Munhava",
|
||||
"outcome": "prepare_informational_candidate",
|
||||
"prepared_codes": [
|
||||
"MOZ-MUNHAVA-PARK"
|
||||
],
|
||||
"source": "https://sdubeira.co.mz/project/parque-industrial-logistico/",
|
||||
"known": "SDUBeira descreve o projecto em fases, rea total de 900 ha no resumo e piloto de 50 ha; o destaque de 990 ha na mesma pgina cria divergncia.",
|
||||
"pending": "Documento de entrada acrescenta faixa aproximada de 44-60 ha para piloto; no corresponde a metragem confirmada. Obter permetro/plano primrio e no declarar terreno disponvel.",
|
||||
"user_document_claims": {
|
||||
"status": "desenvolvimento faseado",
|
||||
"area": "900 ha total e piloto de cerca de 44-60 ha (alegao documental); actividades/enfoque: logstica porturia, armazenagem, manufactura, agro-indstria e pescas",
|
||||
"assessment": "SDUBeira descreve 900 ha no resumo e 50 ha piloto; a mesma pgina contm destaque de 990 ha. Conservar divergncia editorial e no tratar piloto como terreno livre."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Montepuez",
|
||||
"outcome": "prepare_district_candidate",
|
||||
"prepared_codes": [
|
||||
"MOZ-CDP-MONTEPUEZ"
|
||||
],
|
||||
"source": "https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/",
|
||||
"known": "A iniciativa MozParks menciona parque/rede de Cabo Delgado no distrito de Montepuez. MozParks confirma a rede/parques em desenvolvimento; actividades do documento so apenas enfoque promocional.",
|
||||
"pending": "Regime jurdico, reas, actividades efectivamente autorizadas/instaladas e disponibilidade permanecem por confirmar.",
|
||||
"user_document_claims": {
|
||||
"status": "planeado/em desenvolvimento",
|
||||
"activities": "fornecedores mineiros, transformao, servios e PME (enfoque alegado no documento)",
|
||||
"assessment": "MozParks confirma quatro parques em desenvolvimento nos distritos; actividades so foco de promoo, no licena ou operao confirmada."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Balama",
|
||||
"outcome": "prepare_district_candidate",
|
||||
"prepared_codes": [
|
||||
"MOZ-CDP-BALAMA"
|
||||
],
|
||||
"source": "https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/",
|
||||
"known": "A iniciativa MozParks menciona parque/rede de Cabo Delgado no distrito de Balama. MozParks confirma a rede/parques em desenvolvimento; actividades do documento so apenas enfoque promocional.",
|
||||
"pending": "Regime jurdico, reas, actividades efectivamente autorizadas/instaladas e disponibilidade permanecem por confirmar.",
|
||||
"user_document_claims": {
|
||||
"status": "planeado/em desenvolvimento",
|
||||
"activities": "minerao, servios industriais, fornecedores e PME (enfoque alegado no documento)",
|
||||
"assessment": "MozParks confirma a iniciativa/distrito; actividades permanecem promocionais e no so autorizaes."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Ancuabe",
|
||||
"outcome": "prepare_district_candidate",
|
||||
"prepared_codes": [
|
||||
"MOZ-CDP-ANCUABE"
|
||||
],
|
||||
"source": "https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/",
|
||||
"known": "A iniciativa MozParks menciona parque/rede de Cabo Delgado no distrito de Ancuabe. MozParks confirma a rede/parques em desenvolvimento; actividades do documento so apenas enfoque promocional.",
|
||||
"pending": "Regime jurdico, reas, actividades efectivamente autorizadas/instaladas e disponibilidade permanecem por confirmar.",
|
||||
"user_document_claims": {
|
||||
"status": "planeado/em desenvolvimento",
|
||||
"activities": "transformao, contedo local, servios industriais e PME (enfoque alegado no documento)",
|
||||
"assessment": "MozParks confirma a iniciativa/distrito; actividades permanecem promocionais e no so autorizaes."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Palma",
|
||||
"outcome": "prepare_district_candidate",
|
||||
"prepared_codes": [
|
||||
"MOZ-CDP-PALMA"
|
||||
],
|
||||
"source": "https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/",
|
||||
"known": "A iniciativa MozParks menciona parque/rede de Cabo Delgado no distrito de Palma. MozParks confirma a rede/parques em desenvolvimento; actividades do documento so apenas enfoque promocional.",
|
||||
"pending": "Regime jurdico, reas, actividades efectivamente autorizadas/instaladas e disponibilidade permanecem por confirmar.",
|
||||
"user_document_claims": {
|
||||
"status": "planeado/em desenvolvimento",
|
||||
"activities": "petrleo e gs, energia, logstica e cadeias locais (enfoque alegado no documento)",
|
||||
"assessment": "MozParks confirma a iniciativa/distrito; actividades permanecem promocionais e no so autorizaes."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Selva/Manica Parks",
|
||||
"outcome": "keep_unresolved_program_and_site_distinct",
|
||||
"prepared_codes": [],
|
||||
"source": "https://mozparks.co.mz/mozparks-launches-manica-parks-a-new-industrial-platform-for-manica-province/",
|
||||
"known": "MozParks confirma Manica Parks como iniciativa provincial lanada por memorando em Setembro de 2026. O documento fornecido associa a iniciativa a Selva/Vanduzi, mas no confirma instalao individual.",
|
||||
"pending": "No criar parque individual nem relao territorial de Selva sem fonte especfica. Actividades listadas so enfoque declarado, no autorizao.",
|
||||
"user_document_claims": {
|
||||
"status": "Manica Parks lanado por memorando em Setembro de 2026; alegao de localizao em Selva/Vanduzi permanece por confirmar",
|
||||
"activities": "agro-processamento, metalomecnica, combustveis, hotelaria e restaurao (enfoque alegado no documento)",
|
||||
"assessment": "MozParks confirma iniciativa provincial e memorando, no uma instalao individual em Selva nem incio de construo/operao."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Mocuba ZFI",
|
||||
"outcome": "reconcile_existing",
|
||||
"prepared_codes": [
|
||||
"MOZ-MOCUBA-ZFI",
|
||||
"MOZ-MOCUBA-ZEE"
|
||||
],
|
||||
"source": "https://apiex.gov.mz/zif/mocuba-free-industrial-zone/",
|
||||
"known": "APIEX confirma a ZFI de 50,58 ha e fase de mobilizao de recursos e operador. A lista de actividades do documento enfoque declarado, no autorizao.",
|
||||
"pending": "Confirmar diploma primrio, limites e actividades; manter ZFI e ZEE de Mocuba separadas.",
|
||||
"user_document_claims": {
|
||||
"status": "mobilizao de recursos e operador",
|
||||
"area": "50,58 ha segundo APIEX",
|
||||
"activities": "agro-florestal, pescas, txteis, calcrio, pedra e minerais (enfoque alegado no documento)",
|
||||
"assessment": "APIEX confirma 50,58 ha e mobilizao; confirmar actividades com fonte prpria antes de atribuir ao registo. Manter ZFI distinta da ZEE."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Revúbuè ZFI",
|
||||
"outcome": "prepare_one_record_two_districts",
|
||||
"prepared_codes": [
|
||||
"MOZ-REVUBUE-ZFI"
|
||||
],
|
||||
"source": "https://apiex.gov.mz/revubue-free-industrial-zone/",
|
||||
"known": "APIEX reporta a ZFI abrangendo Chiuta e Moatize, 4.456 ha, sem operador identificado.",
|
||||
"pending": "Obter e validar texto primário do Decreto 41/2016 e as relações territoriais. Um único registo, duas localizações.",
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Mocambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
},
|
||||
"user_document_claims": {
|
||||
"status": "sem operador; um projecto aprovado segundo APIEX",
|
||||
"area": "4 456 ha segundo APIEX",
|
||||
"activities": "industria ligada a recursos e transformacao (enfoque alegado no documento)",
|
||||
"assessment": "APIEX informa um projecto aprovado e potenciais sectores; isto nao transforma o projecto em oportunidade publicada nem comprova actividades autorizadas."
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Minhewene ZFI",
|
||||
"outcome": "prepare_province_precision_candidate",
|
||||
"prepared_codes": [
|
||||
"MOZ-MINHEWENE-ZFI"
|
||||
],
|
||||
"source": "https://apiex.gov.mz/zona-franca-industrial-de-minhewene/",
|
||||
"known": "Pgina APIEX actualmente acessvel reporta Decreto 51/2011, Nampula/Cidade de Nacala, 350 ha e mobilizao de recursos/operador. O documento fornecido diz que a ficha no estava acessvel; esta afirmao diverge da consulta actual.",
|
||||
"pending": "Validar Decreto 51/2011 e localizao cadastral exacta; pgina institucional no substitui fonte legal primria nem comprova operacionalizao actual.",
|
||||
"user_document_claims": {
|
||||
"status": "mobilizao de recursos e operador segundo pgina APIEX acessvel na verificao actual",
|
||||
"area": "350 ha segundo APIEX",
|
||||
"assessment": "A alegao do documento de que no havia ficha acessvel diverge da pgina institucional APIEX actualmente acessvel, que reporta Decreto 51/2011, Nampula/Cidade de Nacala, 350 ha e mobilizao de recursos/operador. Acessibilidade da pgina no substitui validao do diploma primrio ou do estado actual."
|
||||
},
|
||||
"claim_source": {
|
||||
"title": "Parques Industriais e Zonas Francas Industriais em Moçambique",
|
||||
"source_type": "user_supplied_secondary_document",
|
||||
"verified_as_primary": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"legislative_review": {
|
||||
"decree_57_2023": "Texto oficial localizado; criação da ZFI de Topuito e 65 ha confirmados. Não implica que operador, actividade operacional ou terrenos disponíveis estejam confirmados.",
|
||||
"decree_12_2026": "A página oficial consultada apresenta o sumário de aprovação do regulamento ZEE/ZFI e revogação do Decreto 75/99. O texto integral e regras transitórias/entrada em vigor ainda não foram revistos; não parametrizar simulador fiscal a partir do sumário."
|
||||
},
|
||||
"official_checks": [
|
||||
{
|
||||
"topic": "Beluluane",
|
||||
"source": "https://mozparks.co.mz/",
|
||||
"finding": "MozParks apresenta 700 ha prontos para arrendamento; APIEX reporta complexo de cerca de 780 ha, parque 600 ha, Mozal 180 ha, ZFI 55 ha. Escopos divergentes."
|
||||
},
|
||||
{
|
||||
"topic": "Topuito",
|
||||
"source": "https://mozparks.co.mz/",
|
||||
"finding": "MozParks apresenta 215 ha; notcia de 19-08-2026 diz SME Village comeou construo. No declara a totalidade da rea operacional/disponvel."
|
||||
},
|
||||
{
|
||||
"topic": "Munhava",
|
||||
"source": "https://sdubeira.co.mz/project/parque-industrial-logistico/",
|
||||
"finding": "A pgina da SDUBeira contm total 900 ha no resumo e 990 ha em destaque; piloto 50 ha. A faixa 44-60 ha no foi corroborada."
|
||||
},
|
||||
{
|
||||
"topic": "Manica Parks",
|
||||
"source": "https://mozparks.co.mz/mozparks-launches-manica-parks-a-new-industrial-platform-for-manica-province/",
|
||||
"finding": "Memorando e iniciativa provincial confirmados; Selva/Vanduzi no aparece como instalao individual confirmada."
|
||||
},
|
||||
{
|
||||
"topic": "Cabo Delgado Parks",
|
||||
"source": "https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/",
|
||||
"finding": "MozParks identifica parques em desenvolvimento nos quatro distritos. Sectores do documento permanecem promocionais."
|
||||
},
|
||||
{
|
||||
"topic": "Minhewene",
|
||||
"source": "https://apiex.gov.mz/zona-franca-industrial-de-minhewene/",
|
||||
"finding": "Pgina APIEX est acessvel nesta consulta e reporta Decreto 51/2011, Nacala, 350 ha e mobilizao de recursos/operador; a alegao documental de indisponibilidade da ficha no corresponde ao acesso actual."
|
||||
}
|
||||
]
|
||||
"official_checks": [
|
||||
{
|
||||
"topic": "Beluluane",
|
||||
"source": "https://mozparks.co.mz/",
|
||||
"finding": "MozParks apresenta 700 ha prontos para arrendamento; APIEX reporta complexo de cerca de 780 ha, parque 600 ha, Mozal 180 ha, ZFI 55 ha. Escopos divergentes."
|
||||
},
|
||||
{
|
||||
"topic": "Topuito",
|
||||
"source": "https://mozparks.co.mz/",
|
||||
"finding": "MozParks apresenta 215 ha; notcia de 19-08-2026 diz SME Village comeou construo. No declara a totalidade da rea operacional/disponvel."
|
||||
},
|
||||
{
|
||||
"topic": "Munhava",
|
||||
"source": "https://sdubeira.co.mz/project/parque-industrial-logistico/",
|
||||
"finding": "A pgina da SDUBeira contm total 900 ha no resumo e 990 ha em destaque; piloto 50 ha. A faixa 44-60 ha no foi corroborada."
|
||||
},
|
||||
{
|
||||
"topic": "Manica Parks",
|
||||
"source": "https://mozparks.co.mz/mozparks-launches-manica-parks-a-new-industrial-platform-for-manica-province/",
|
||||
"finding": "Memorando e iniciativa provincial confirmados; Selva/Vanduzi no aparece como instalao individual confirmada."
|
||||
},
|
||||
{
|
||||
"topic": "Cabo Delgado Parks",
|
||||
"source": "https://mozparks.co.mz/mozparks-launches-cabo-delgado-parks/",
|
||||
"finding": "MozParks identifica parques em desenvolvimento nos quatro distritos. Sectores do documento permanecem promocionais."
|
||||
},
|
||||
{
|
||||
"topic": "Minhewene",
|
||||
"source": "https://apiex.gov.mz/zona-franca-industrial-de-minhewene/",
|
||||
"finding": "Pgina APIEX est acessvel nesta consulta e reporta Decreto 51/2011, Nacala, 350 ha e mobilizao de recursos/operador; a alegao documental de indisponibilidade da ficha no corresponde ao acesso actual."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,16 +1,125 @@
|
||||
{
|
||||
"need_category": {"labour_required":"Mão de obra necessária","labour_available":"Mão de obra disponível","infrastructure_existing":"Infraestruturas existentes","infrastructure_required":"Infraestruturas necessárias","education":"Educação e formação","support":"Apoios"},
|
||||
"infrastructure": {"energy":"Energia","water":"Água","sanitation":"Saneamento","telecom":"Telecomunicações","internet":"Internet","road":"Estrada","rail":"Ferrovia","port":"Porto","airport":"Aeroporto","storage":"Armazenagem","cold_chain":"Cadeia de frio","housing":"Habitação","health":"Saúde","education":"Educação","security":"Segurança","industrial":"Instalações industriais","office":"Escritórios","land":"Terrenos","logistics":"Serviços logísticos"},
|
||||
"education": {"preschool":"Educação pré-escolar","primary":"Ensino primário","secondary":"Ensino secundário","vocational":"Formação técnico-profissional","higher":"Ensino superior","research":"Investigação","innovation":"Centros de inovação","incubation":"Incubadoras","laboratory":"Laboratórios","edtech":"Tecnologia educativa","digital":"Plataformas digitais","connectivity":"Conectividade","buildings":"Infraestruturas escolares","equipment":"Equipamentos","library":"Bibliotecas","residence":"Alojamento","meals":"Alimentação escolar","transport":"Transporte","content":"Produção de conteúdos","teachers":"Formação de professores","skills":"Desenvolvimento de competências","continuing":"Formação contínua"},
|
||||
"labour": {"technical":"Pessoal técnico","professional":"Profissionais especializados","operational":"Pessoal operacional","management":"Gestão","training":"Formadores"},
|
||||
"qualification": {"primary":"Ensino primário","secondary":"Ensino secundário","technical":"Qualificação técnica","higher":"Ensino superior","postgraduate":"Pós-graduação"},
|
||||
"support": {"licence":"Licenças","incentives":"Incentivos","finance":"Financiamento","land":"Terra","logistics":"Logística","partners":"Parceiros","suppliers":"Fornecedores","recruitment":"Contratação","imports":"Importação","exports":"Exportação","installation":"Instalação","utilities":"Ligação a serviços públicos"},
|
||||
"priority": {"low":"Baixa","normal":"Normal","high":"Alta"},
|
||||
"unit": {"people":"Pessoas","units":"Unidades","mw":"MW","m3_day":"m³/dia","km":"km","hectares":"Hectares"},
|
||||
"participation": {"invest":"Investir","finance":"Financiar","supply":"Fornecer","operate":"Operar","partner":"Estabelecer parceria"},
|
||||
"partnership": {"direct":"Investimento directo","joint_venture":"Joint venture","ppp":"Parceria público-privada","supply":"Fornecimento","technical":"Cooperação técnica"},
|
||||
"investor_profile": {"individual":"Individual","company":"Empresa","fund":"Fundo","public":"Entidade pública"},
|
||||
"impact": {"employment":"Emprego","training":"Competências","local_supply":"Fornecimento local","environment":"Ambiente","exports":"Exportações"},
|
||||
"subsector": {},
|
||||
"need_status": {"declared":"Declarada","available":"Disponível segundo declaração","required":"Necessária"}
|
||||
"need_category": {
|
||||
"labour_required": "Mão de obra necessária",
|
||||
"labour_available": "Mão de obra disponível",
|
||||
"infrastructure_existing": "Infraestruturas existentes",
|
||||
"infrastructure_required": "Infraestruturas necessárias",
|
||||
"education": "Educação e formação",
|
||||
"support": "Apoios"
|
||||
},
|
||||
"infrastructure": {
|
||||
"energy": "Energia",
|
||||
"water": "Água",
|
||||
"sanitation": "Saneamento",
|
||||
"telecom": "Telecomunicações",
|
||||
"internet": "Internet",
|
||||
"road": "Estrada",
|
||||
"rail": "Ferrovia",
|
||||
"port": "Porto",
|
||||
"airport": "Aeroporto",
|
||||
"storage": "Armazenagem",
|
||||
"cold_chain": "Cadeia de frio",
|
||||
"housing": "Habitação",
|
||||
"health": "Saúde",
|
||||
"education": "Educação",
|
||||
"security": "Segurança",
|
||||
"industrial": "Instalações industriais",
|
||||
"office": "Escritórios",
|
||||
"land": "Terrenos",
|
||||
"logistics": "Serviços logísticos"
|
||||
},
|
||||
"education": {
|
||||
"preschool": "Educação pré-escolar",
|
||||
"primary": "Ensino primário",
|
||||
"secondary": "Ensino secundário",
|
||||
"vocational": "Formação técnico-profissional",
|
||||
"higher": "Ensino superior",
|
||||
"research": "Investigação",
|
||||
"innovation": "Centros de inovação",
|
||||
"incubation": "Incubadoras",
|
||||
"laboratory": "Laboratórios",
|
||||
"edtech": "Tecnologia educativa",
|
||||
"digital": "Plataformas digitais",
|
||||
"connectivity": "Conectividade",
|
||||
"buildings": "Infraestruturas escolares",
|
||||
"equipment": "Equipamentos",
|
||||
"library": "Bibliotecas",
|
||||
"residence": "Alojamento",
|
||||
"meals": "Alimentação escolar",
|
||||
"transport": "Transporte",
|
||||
"content": "Produção de conteúdos",
|
||||
"teachers": "Formação de professores",
|
||||
"skills": "Desenvolvimento de competências",
|
||||
"continuing": "Formação contínua"
|
||||
},
|
||||
"labour": {
|
||||
"technical": "Pessoal técnico",
|
||||
"professional": "Profissionais especializados",
|
||||
"operational": "Pessoal operacional",
|
||||
"management": "Gestão",
|
||||
"training": "Formadores"
|
||||
},
|
||||
"qualification": {
|
||||
"primary": "Ensino primário",
|
||||
"secondary": "Ensino secundário",
|
||||
"technical": "Qualificação técnica",
|
||||
"higher": "Ensino superior",
|
||||
"postgraduate": "Pós-graduação"
|
||||
},
|
||||
"support": {
|
||||
"licence": "Licenças",
|
||||
"incentives": "Incentivos",
|
||||
"finance": "Financiamento",
|
||||
"land": "Terra",
|
||||
"logistics": "Logística",
|
||||
"partners": "Parceiros",
|
||||
"suppliers": "Fornecedores",
|
||||
"recruitment": "Contratação",
|
||||
"imports": "Importação",
|
||||
"exports": "Exportação",
|
||||
"installation": "Instalação",
|
||||
"utilities": "Ligação a serviços públicos"
|
||||
},
|
||||
"priority": { "low": "Baixa", "normal": "Normal", "high": "Alta" },
|
||||
"unit": {
|
||||
"people": "Pessoas",
|
||||
"units": "Unidades",
|
||||
"mw": "MW",
|
||||
"m3_day": "m³/dia",
|
||||
"km": "km",
|
||||
"hectares": "Hectares"
|
||||
},
|
||||
"participation": {
|
||||
"invest": "Investir",
|
||||
"finance": "Financiar",
|
||||
"supply": "Fornecer",
|
||||
"operate": "Operar",
|
||||
"partner": "Estabelecer parceria"
|
||||
},
|
||||
"partnership": {
|
||||
"direct": "Investimento directo",
|
||||
"joint_venture": "Joint venture",
|
||||
"ppp": "Parceria público-privada",
|
||||
"supply": "Fornecimento",
|
||||
"technical": "Cooperação técnica"
|
||||
},
|
||||
"investor_profile": {
|
||||
"individual": "Individual",
|
||||
"company": "Empresa",
|
||||
"fund": "Fundo",
|
||||
"public": "Entidade pública"
|
||||
},
|
||||
"impact": {
|
||||
"employment": "Emprego",
|
||||
"training": "Competências",
|
||||
"local_supply": "Fornecimento local",
|
||||
"environment": "Ambiente",
|
||||
"exports": "Exportações"
|
||||
},
|
||||
"subsector": {},
|
||||
"need_status": {
|
||||
"declared": "Declarada",
|
||||
"available": "Disponível segundo declaração",
|
||||
"required": "Necessária"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,150 +1,321 @@
|
||||
{
|
||||
"source_prompt": "docs/investmoz/source-staging/zone-integration-2026-10-04/Pasted text.txt",
|
||||
"consulted_at": "2026-10-04",
|
||||
"records": [
|
||||
{
|
||||
"code": "MOZ-BELULUANE-PARK",
|
||||
"name": "Parque Industrial de Beluluane",
|
||||
"name_en": "Beluluane Industrial Park",
|
||||
"category": "industrial_park",
|
||||
"physical_category": "industrial_park",
|
||||
"legal_regime": "general",
|
||||
"operational_status": "unknown",
|
||||
"area_value": null,
|
||||
"area_unit": null,
|
||||
"area_components": [
|
||||
{"type":"total","value":780,"unit":"ha","status":"disputed","source_reference":"https://apiex.gov.mz/beluluane-industrial-park-free-zone/ — APIEX reports the complex; scope and overlap need confirmation","source_date":null},
|
||||
{"type":"total","value":700,"unit":"ha","status":"disputed","source_reference":"Document cited in the integration prompt was not attached; claim retained as unresolved, not imported as verified data","source_date":null},
|
||||
{"type":"developed","value":600,"unit":"ha","status":"reported","source_reference":"https://apiex.gov.mz/beluluane-industrial-park-free-zone/ — APIEX describes the industrial park component","source_date":null},
|
||||
{"type":"developed","value":180,"unit":"ha","status":"reported","source_reference":"https://apiex.gov.mz/beluluane-industrial-park-free-zone/ — APIEX describes the Mozal site component; do not add as available land","source_date":null},
|
||||
{"type":"legal","value":55,"unit":"ha","status":"reported","source_reference":"https://apiex.gov.mz/beluluane-industrial-park-free-zone/ — separate ZFI component; do not merge with park","source_date":null}
|
||||
],
|
||||
"description": "A APIEX apresenta aproximadamente 780 ha para o conjunto referido como Parque Industrial de Beluluane–Zona Franca, incluindo componentes com perímetros distintos. O valor não representa área livre ou disponível. A fonte identifica historicamente Matola-Rio, Boane, Província de Maputo. Situação operacional e área disponível não confirmadas por esta referência.",
|
||||
"description_en": "APIEX reports approximately 780 ha for the Beluluane Industrial Park–Free Zone complex, including distinct components. This is not an available-land figure. The source historically identifies Matola-Rio, Boane, Maputo Province. Operational status and available area are not confirmed by this reference.",
|
||||
"aliases": ["PIB-ZF", "Parque Industrial de Beluluane–Zona Franca"],
|
||||
"activities": [],
|
||||
"historical_territory": {"name": "Matola-Rio", "district": "Boane", "province": "Maputo", "source_reference": "APIEX page; current territorial equivalence requires review"},
|
||||
"source": {"title": "Beluluane Industrial Park – Free Zone", "url": "https://apiex.gov.mz/beluluane-industrial-park-free-zone/", "diploma": "Resolução Interna do Conselho de Ministros n.º 15/99, de 12 de Outubro", "article": "Act of creation; article and current legal effect pending review", "valid_from": "1999-10-12", "competent_entity": "Conselho de Ministros / APIEX source page"},
|
||||
"territories": [{"name": "MATOLA RIO", "type": "admin_post", "scope_type": "administrative"}]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-BELULUANE-ZFI",
|
||||
"name": "Zona Franca Industrial de Beluluane",
|
||||
"name_en": "Beluluane Industrial Free Zone",
|
||||
"category": "zfi",
|
||||
"physical_category": "industrial_free_zone",
|
||||
"legal_regime": "zfi",
|
||||
"operational_status": "unknown",
|
||||
"area_value": 55,
|
||||
"area_unit": "ha",
|
||||
"description": "Componente ZFI distinta dentro do complexo Beluluane. A APIEX indica cerca de 55 ha. Este registo não atribui elegibilidade ou incentivos a qualquer empresa.",
|
||||
"description_en": "A distinct IFZ component within the Beluluane complex. APIEX reports approximately 55 ha. This record does not establish eligibility or incentives for any company.",
|
||||
"aliases": ["Beluluane IFZ"],
|
||||
"activities": [],
|
||||
"parent_code": "MOZ-BELULUANE-PARK",
|
||||
"historical_territory": {"name": "Matola-Rio", "district": "Boane", "province": "Maputo", "source_reference": "APIEX page; current territorial equivalence requires review"},
|
||||
"source": {"title": "Beluluane Industrial Park – Free Zone", "url": "https://apiex.gov.mz/beluluane-industrial-park-free-zone/", "diploma": "Resolução Interna do Conselho de Ministros n.º 15/99, de 12 de Outubro", "article": "Act of creation; article and current legal effect pending review", "valid_from": "1999-10-12", "competent_entity": "Conselho de Ministros / APIEX source page"},
|
||||
"territories": [{"name": "MATOLA RIO", "type": "admin_post", "scope_type": "administrative"}]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-CRUSSE-JAMALI-ZEE",
|
||||
"name": "Zona de Estância de Turismo Integrado de Crusse e Jamali",
|
||||
"name_en": "Crusse and Jamali Integrated Tourism Resort Area",
|
||||
"category": "zee",
|
||||
"physical_category": "integrated_tourism_area",
|
||||
"legal_regime": "zee",
|
||||
"operational_status": "unknown",
|
||||
"area_value": 1750,
|
||||
"area_unit": "ha",
|
||||
"description": "Criada em regime de ZEE pelo Decreto n.º 47/2013, de 30 de Agosto. A fonte oficial indica 1.750 ha no Posto Administrativo de Matibane, Distrito de Mossuril, Nampula. A criação da zona não confirma que infra-estruturas ou oportunidades previstas estejam actualmente disponíveis.",
|
||||
"description_en": "Created under the SEZ regime by Decree no. 47/2013 of 30 August. The official source states 1,750 ha in Matibane Administrative Post, Mossuril District, Nampula. Establishment of the zone does not confirm that planned infrastructure or opportunities are currently available.",
|
||||
"aliases": ["Crusse-Jamali", "Crusse & Jamali"],
|
||||
"activities": [{"name": "Turismo integrado", "status": "potential", "source_reference": "Decreto n.º 47/2013; tourism scope in the creation description"}],
|
||||
"source": {"title": "Conselho de Ministros — BR n.º 70 de 30.08.13", "url": "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", "diploma": "Decreto n.º 47/2013, de 30 de Agosto", "article": "Act of creation; article reference pending review", "valid_from": "2013-08-30", "competent_entity": "Conselho de Ministros / Imprensa Nacional de Moçambique"},
|
||||
"territories": [{"name": "MATIBANE", "type": "admin_post", "scope_type": "legal_scope"}]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-MANGA-MUNGASSA-ZEE",
|
||||
"name": "Zona Económica Especial de Manga-Mungassa",
|
||||
"name_en": "Manga-Mungassa Special Economic Zone",
|
||||
"category": "zee",
|
||||
"physical_category": "economic_zone",
|
||||
"legal_regime": "zee",
|
||||
"operational_status": "unknown",
|
||||
"area_value": 217,
|
||||
"area_unit": "ha",
|
||||
"description": "A APIEX indica criação pelo Decreto n.º 22/2012, de 6 de Junho, localização na Cidade da Beira, Sofala, e área de 217 ha. A fonte refere sectores de potencial atracção; esses sectores não são tratados como actividades autorizadas nem como oportunidades cadastradas.",
|
||||
"description_en": "APIEX reports establishment by Decree no. 22/2012 of 6 June, location in Beira City, Sofala, and an area of 217 ha. Sectors described as investment potential are not treated as authorized activities or registered opportunities.",
|
||||
"aliases": ["Manga Mungassa ZEE"],
|
||||
"activities": [
|
||||
{"name": "Indústria", "status": "potential", "source_reference": "APIEX Manga-Mungassa page; potential sectors"},
|
||||
{"name": "Logística e transporte", "status": "potential", "source_reference": "APIEX Manga-Mungassa page; potential sectors"},
|
||||
{"name": "Hotelaria e serviços", "status": "potential", "source_reference": "APIEX Manga-Mungassa page; potential sectors"}
|
||||
],
|
||||
"source": {"title": "Manga-Mungassa Special Economic Zone – APIEX", "url": "https://apiex.gov.mz/zee/manga-mungassa-special-economic-zone/", "diploma": "Decreto n.º 22/2012, de 6 de Junho", "article": "Act of creation; article reference pending review", "valid_from": "2012-06-06", "competent_entity": "Conselho de Ministros / APIEX"},
|
||||
"territories": [{"name": "CIDADE DA BEIRA", "type": "district", "scope_type": "administrative"}]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-MOCUBA-ZEE",
|
||||
"name": "Zona Económica Especial de Mocuba",
|
||||
"name_en": "Mocuba Special Economic Zone",
|
||||
"category": "zee",
|
||||
"physical_category": "economic_zone",
|
||||
"legal_regime": "zee",
|
||||
"operational_status": "unknown",
|
||||
"area_value": 10727,
|
||||
"area_unit": "km2",
|
||||
"description": "Criada pelo Decreto n.º 28/2014, de 6 de Junho. O INM e a APIEX indicam que abrange todo o Distrito de Mocuba e o Posto Administrativo de Munhamade, no Distrito de Lugela, Zambézia. A APIEX indica 10.727 km²; a unidade e a abrangência são preservadas sem as reduzir à cidade de Mocuba.",
|
||||
"description_en": "Created by Decree no. 28/2014 of 6 June. INM and APIEX state that it covers all of Mocuba District and Munhamade Administrative Post in Lugela District, Zambézia. APIEX reports 10,727 km²; the unit and scope are preserved without reducing it to Mocuba City.",
|
||||
"aliases": ["Mocuba ZEE"],
|
||||
"activities": [
|
||||
{"name": "Produção agrícola, pecuária, florestal e processamento", "status": "potential", "source_reference": "APIEX Mocuba SEZ page; stated attraction objectives"}
|
||||
],
|
||||
"source": {"title": "BR n.º 46 de 06.06.14 — Decreto n.º 28/2014", "url": "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", "diploma": "Decreto n.º 28/2014, de 6 de Junho", "article": "Act of creation; article reference pending review", "valid_from": "2014-06-06", "competent_entity": "Conselho de Ministros / Imprensa Nacional de Moçambique"},
|
||||
"territories": [
|
||||
{"name": "MOCUBA", "type": "district", "scope_type": "legal_scope"},
|
||||
{"name": "MUNHAMADE", "type": "admin_post", "scope_type": "legal_scope"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-MOCUBA-ZFI",
|
||||
"name": "Zona Franca Industrial de Mocuba",
|
||||
"name_en": "Mocuba Industrial Free Zone",
|
||||
"category": "zfi",
|
||||
"physical_category": "industrial_free_zone",
|
||||
"legal_regime": "zfi",
|
||||
"operational_status": "unknown",
|
||||
"area_value": 50.58,
|
||||
"area_unit": "ha",
|
||||
"description": "Criada pelo Decreto n.º 29/2014, de 6 de Junho, e localizada na Cidade de Mocuba. A APIEX descreve esta ZFI como estando na fase de mobilização de recursos e do operador. É distinta da ZEE de Mocuba e não implica elegibilidade fiscal de empresas.",
|
||||
"description_en": "Created by Decree no. 29/2014 of 6 June and located in Mocuba City. APIEX describes this IFZ as mobilizing resources and an operator. It is distinct from Mocuba SEZ and does not imply tax eligibility for companies.",
|
||||
"aliases": ["Mocuba IFZ", "ZFI de Mocuba"],
|
||||
"activities": [],
|
||||
"source": {"title": "Mocuba IFZ – APIEX", "url": "https://apiex.gov.mz/zif/mocuba-free-industrial-zone/", "diploma": "Decreto n.º 29/2014, de 6 de Junho", "article": "Act of creation; article reference pending review", "valid_from": "2014-06-06", "competent_entity": "Conselho de Ministros / APIEX"},
|
||||
"territories": [{"name": "CIDADE DE MOCUBA", "type": "admin_post", "scope_type": "administrative"}]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-NACALA-ZEE",
|
||||
"name": "Zona Económica Especial de Nacala",
|
||||
"name_en": "Nacala Special Economic Zone",
|
||||
"category": "zee",
|
||||
"physical_category": "economic_zone",
|
||||
"legal_regime": "zee",
|
||||
"operational_status": "unknown",
|
||||
"area_value": 1539,
|
||||
"area_unit": "km2",
|
||||
"description": "A APIEX indica criação pelo Decreto n.º 76/2007, de 18 de Dezembro, abrangência dos distritos de Nacala e Nacala-à-Velha e área de 1.539 km². Não se confunde com o Corredor de Nacala, porto ou outras zonas francas. Os números de projectos e empregos publicados na página não são importados como actuais por não terem data de referência confirmada.",
|
||||
"description_en": "APIEX reports establishment by Decree no. 76/2007 of 18 December, covering Nacala and Nacala-à-Velha districts, with an area of 1,539 km². It is distinct from the Nacala Corridor, port, and other free zones. Project and employment counts on the page are not imported as current because their reference date is not confirmed.",
|
||||
"aliases": ["ZEEN", "Nacala SEZ"],
|
||||
"activities": [
|
||||
{"name": "Logística e indústria", "status": "potential", "source_reference": "APIEX Nacala SEZ page; sectors identified as potential"},
|
||||
{"name": "Agro-processamento e indústria química", "status": "potential", "source_reference": "APIEX Nacala SEZ page; sectors identified as potential"}
|
||||
],
|
||||
"source": {"title": "Nacala Special Economic Zone – APIEX", "url": "https://apiex.gov.mz/nacala-special-economic-zone/", "diploma": "Decreto n.º 76/2007, de 18 de Dezembro", "article": "Act of creation; article reference pending review", "valid_from": "2007-12-18", "competent_entity": "Conselho de Ministros / APIEX"},
|
||||
"territories": [
|
||||
{"name": "NACALA-PORTO", "type": "district", "scope_type": "legal_scope"},
|
||||
{"name": "NACALA-VELHA", "source_name": "Nacala-à-Velha", "type": "district", "scope_type": "legal_scope"}
|
||||
]
|
||||
}
|
||||
]
|
||||
"source_prompt": "docs/investmoz/source-staging/zone-integration-2026-10-04/Pasted text.txt",
|
||||
"consulted_at": "2026-10-04",
|
||||
"records": [
|
||||
{
|
||||
"code": "MOZ-BELULUANE-PARK",
|
||||
"name": "Parque Industrial de Beluluane",
|
||||
"name_en": "Beluluane Industrial Park",
|
||||
"category": "industrial_park",
|
||||
"physical_category": "industrial_park",
|
||||
"legal_regime": "general",
|
||||
"operational_status": "unknown",
|
||||
"area_value": null,
|
||||
"area_unit": null,
|
||||
"area_components": [
|
||||
{
|
||||
"type": "total",
|
||||
"value": 780,
|
||||
"unit": "ha",
|
||||
"status": "disputed",
|
||||
"source_reference": "https://apiex.gov.mz/beluluane-industrial-park-free-zone/ — APIEX reports the complex; scope and overlap need confirmation",
|
||||
"source_date": null
|
||||
},
|
||||
{
|
||||
"type": "total",
|
||||
"value": 700,
|
||||
"unit": "ha",
|
||||
"status": "disputed",
|
||||
"source_reference": "Document cited in the integration prompt was not attached; claim retained as unresolved, not imported as verified data",
|
||||
"source_date": null
|
||||
},
|
||||
{
|
||||
"type": "developed",
|
||||
"value": 600,
|
||||
"unit": "ha",
|
||||
"status": "reported",
|
||||
"source_reference": "https://apiex.gov.mz/beluluane-industrial-park-free-zone/ — APIEX describes the industrial park component",
|
||||
"source_date": null
|
||||
},
|
||||
{
|
||||
"type": "developed",
|
||||
"value": 180,
|
||||
"unit": "ha",
|
||||
"status": "reported",
|
||||
"source_reference": "https://apiex.gov.mz/beluluane-industrial-park-free-zone/ — APIEX describes the Mozal site component; do not add as available land",
|
||||
"source_date": null
|
||||
},
|
||||
{
|
||||
"type": "legal",
|
||||
"value": 55,
|
||||
"unit": "ha",
|
||||
"status": "reported",
|
||||
"source_reference": "https://apiex.gov.mz/beluluane-industrial-park-free-zone/ — separate ZFI component; do not merge with park",
|
||||
"source_date": null
|
||||
}
|
||||
],
|
||||
"description": "A APIEX apresenta aproximadamente 780 ha para o conjunto referido como Parque Industrial de Beluluane–Zona Franca, incluindo componentes com perímetros distintos. O valor não representa área livre ou disponível. A fonte identifica historicamente Matola-Rio, Boane, Província de Maputo. Situação operacional e área disponível não confirmadas por esta referência.",
|
||||
"description_en": "APIEX reports approximately 780 ha for the Beluluane Industrial Park–Free Zone complex, including distinct components. This is not an available-land figure. The source historically identifies Matola-Rio, Boane, Maputo Province. Operational status and available area are not confirmed by this reference.",
|
||||
"aliases": ["PIB-ZF", "Parque Industrial de Beluluane–Zona Franca"],
|
||||
"activities": [],
|
||||
"historical_territory": {
|
||||
"name": "Matola-Rio",
|
||||
"district": "Boane",
|
||||
"province": "Maputo",
|
||||
"source_reference": "APIEX page; current territorial equivalence requires review"
|
||||
},
|
||||
"source": {
|
||||
"title": "Beluluane Industrial Park – Free Zone",
|
||||
"url": "https://apiex.gov.mz/beluluane-industrial-park-free-zone/",
|
||||
"diploma": "Resolução Interna do Conselho de Ministros n.º 15/99, de 12 de Outubro",
|
||||
"article": "Act of creation; article and current legal effect pending review",
|
||||
"valid_from": "1999-10-12",
|
||||
"competent_entity": "Conselho de Ministros / APIEX source page"
|
||||
},
|
||||
"territories": [
|
||||
{
|
||||
"name": "MATOLA RIO",
|
||||
"type": "admin_post",
|
||||
"scope_type": "administrative"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-BELULUANE-ZFI",
|
||||
"name": "Zona Franca Industrial de Beluluane",
|
||||
"name_en": "Beluluane Industrial Free Zone",
|
||||
"category": "zfi",
|
||||
"physical_category": "industrial_free_zone",
|
||||
"legal_regime": "zfi",
|
||||
"operational_status": "unknown",
|
||||
"area_value": 55,
|
||||
"area_unit": "ha",
|
||||
"description": "Componente ZFI distinta dentro do complexo Beluluane. A APIEX indica cerca de 55 ha. Este registo não atribui elegibilidade ou incentivos a qualquer empresa.",
|
||||
"description_en": "A distinct IFZ component within the Beluluane complex. APIEX reports approximately 55 ha. This record does not establish eligibility or incentives for any company.",
|
||||
"aliases": ["Beluluane IFZ"],
|
||||
"activities": [],
|
||||
"parent_code": "MOZ-BELULUANE-PARK",
|
||||
"historical_territory": {
|
||||
"name": "Matola-Rio",
|
||||
"district": "Boane",
|
||||
"province": "Maputo",
|
||||
"source_reference": "APIEX page; current territorial equivalence requires review"
|
||||
},
|
||||
"source": {
|
||||
"title": "Beluluane Industrial Park – Free Zone",
|
||||
"url": "https://apiex.gov.mz/beluluane-industrial-park-free-zone/",
|
||||
"diploma": "Resolução Interna do Conselho de Ministros n.º 15/99, de 12 de Outubro",
|
||||
"article": "Act of creation; article and current legal effect pending review",
|
||||
"valid_from": "1999-10-12",
|
||||
"competent_entity": "Conselho de Ministros / APIEX source page"
|
||||
},
|
||||
"territories": [
|
||||
{
|
||||
"name": "MATOLA RIO",
|
||||
"type": "admin_post",
|
||||
"scope_type": "administrative"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-CRUSSE-JAMALI-ZEE",
|
||||
"name": "Zona de Estância de Turismo Integrado de Crusse e Jamali",
|
||||
"name_en": "Crusse and Jamali Integrated Tourism Resort Area",
|
||||
"category": "zee",
|
||||
"physical_category": "integrated_tourism_area",
|
||||
"legal_regime": "zee",
|
||||
"operational_status": "unknown",
|
||||
"area_value": 1750,
|
||||
"area_unit": "ha",
|
||||
"description": "Criada em regime de ZEE pelo Decreto n.º 47/2013, de 30 de Agosto. A fonte oficial indica 1.750 ha no Posto Administrativo de Matibane, Distrito de Mossuril, Nampula. A criação da zona não confirma que infra-estruturas ou oportunidades previstas estejam actualmente disponíveis.",
|
||||
"description_en": "Created under the SEZ regime by Decree no. 47/2013 of 30 August. The official source states 1,750 ha in Matibane Administrative Post, Mossuril District, Nampula. Establishment of the zone does not confirm that planned infrastructure or opportunities are currently available.",
|
||||
"aliases": ["Crusse-Jamali", "Crusse & Jamali"],
|
||||
"activities": [
|
||||
{
|
||||
"name": "Turismo integrado",
|
||||
"status": "potential",
|
||||
"source_reference": "Decreto n.º 47/2013; tourism scope in the creation description"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"title": "Conselho de Ministros — BR n.º 70 de 30.08.13",
|
||||
"url": "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",
|
||||
"diploma": "Decreto n.º 47/2013, de 30 de Agosto",
|
||||
"article": "Act of creation; article reference pending review",
|
||||
"valid_from": "2013-08-30",
|
||||
"competent_entity": "Conselho de Ministros / Imprensa Nacional de Moçambique"
|
||||
},
|
||||
"territories": [
|
||||
{
|
||||
"name": "MATIBANE",
|
||||
"type": "admin_post",
|
||||
"scope_type": "legal_scope"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-MANGA-MUNGASSA-ZEE",
|
||||
"name": "Zona Económica Especial de Manga-Mungassa",
|
||||
"name_en": "Manga-Mungassa Special Economic Zone",
|
||||
"category": "zee",
|
||||
"physical_category": "economic_zone",
|
||||
"legal_regime": "zee",
|
||||
"operational_status": "unknown",
|
||||
"area_value": 217,
|
||||
"area_unit": "ha",
|
||||
"description": "A APIEX indica criação pelo Decreto n.º 22/2012, de 6 de Junho, localização na Cidade da Beira, Sofala, e área de 217 ha. A fonte refere sectores de potencial atracção; esses sectores não são tratados como actividades autorizadas nem como oportunidades cadastradas.",
|
||||
"description_en": "APIEX reports establishment by Decree no. 22/2012 of 6 June, location in Beira City, Sofala, and an area of 217 ha. Sectors described as investment potential are not treated as authorized activities or registered opportunities.",
|
||||
"aliases": ["Manga Mungassa ZEE"],
|
||||
"activities": [
|
||||
{
|
||||
"name": "Indústria",
|
||||
"status": "potential",
|
||||
"source_reference": "APIEX Manga-Mungassa page; potential sectors"
|
||||
},
|
||||
{
|
||||
"name": "Logística e transporte",
|
||||
"status": "potential",
|
||||
"source_reference": "APIEX Manga-Mungassa page; potential sectors"
|
||||
},
|
||||
{
|
||||
"name": "Hotelaria e serviços",
|
||||
"status": "potential",
|
||||
"source_reference": "APIEX Manga-Mungassa page; potential sectors"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"title": "Manga-Mungassa Special Economic Zone – APIEX",
|
||||
"url": "https://apiex.gov.mz/zee/manga-mungassa-special-economic-zone/",
|
||||
"diploma": "Decreto n.º 22/2012, de 6 de Junho",
|
||||
"article": "Act of creation; article reference pending review",
|
||||
"valid_from": "2012-06-06",
|
||||
"competent_entity": "Conselho de Ministros / APIEX"
|
||||
},
|
||||
"territories": [
|
||||
{
|
||||
"name": "CIDADE DA BEIRA",
|
||||
"type": "district",
|
||||
"scope_type": "administrative"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-MOCUBA-ZEE",
|
||||
"name": "Zona Económica Especial de Mocuba",
|
||||
"name_en": "Mocuba Special Economic Zone",
|
||||
"category": "zee",
|
||||
"physical_category": "economic_zone",
|
||||
"legal_regime": "zee",
|
||||
"operational_status": "unknown",
|
||||
"area_value": 10727,
|
||||
"area_unit": "km2",
|
||||
"description": "Criada pelo Decreto n.º 28/2014, de 6 de Junho. O INM e a APIEX indicam que abrange todo o Distrito de Mocuba e o Posto Administrativo de Munhamade, no Distrito de Lugela, Zambézia. A APIEX indica 10.727 km²; a unidade e a abrangência são preservadas sem as reduzir à cidade de Mocuba.",
|
||||
"description_en": "Created by Decree no. 28/2014 of 6 June. INM and APIEX state that it covers all of Mocuba District and Munhamade Administrative Post in Lugela District, Zambézia. APIEX reports 10,727 km²; the unit and scope are preserved without reducing it to Mocuba City.",
|
||||
"aliases": ["Mocuba ZEE"],
|
||||
"activities": [
|
||||
{
|
||||
"name": "Produção agrícola, pecuária, florestal e processamento",
|
||||
"status": "potential",
|
||||
"source_reference": "APIEX Mocuba SEZ page; stated attraction objectives"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"title": "BR n.º 46 de 06.06.14 — Decreto n.º 28/2014",
|
||||
"url": "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",
|
||||
"diploma": "Decreto n.º 28/2014, de 6 de Junho",
|
||||
"article": "Act of creation; article reference pending review",
|
||||
"valid_from": "2014-06-06",
|
||||
"competent_entity": "Conselho de Ministros / Imprensa Nacional de Moçambique"
|
||||
},
|
||||
"territories": [
|
||||
{
|
||||
"name": "MOCUBA",
|
||||
"type": "district",
|
||||
"scope_type": "legal_scope"
|
||||
},
|
||||
{
|
||||
"name": "MUNHAMADE",
|
||||
"type": "admin_post",
|
||||
"scope_type": "legal_scope"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-MOCUBA-ZFI",
|
||||
"name": "Zona Franca Industrial de Mocuba",
|
||||
"name_en": "Mocuba Industrial Free Zone",
|
||||
"category": "zfi",
|
||||
"physical_category": "industrial_free_zone",
|
||||
"legal_regime": "zfi",
|
||||
"operational_status": "unknown",
|
||||
"area_value": 50.58,
|
||||
"area_unit": "ha",
|
||||
"description": "Criada pelo Decreto n.º 29/2014, de 6 de Junho, e localizada na Cidade de Mocuba. A APIEX descreve esta ZFI como estando na fase de mobilização de recursos e do operador. É distinta da ZEE de Mocuba e não implica elegibilidade fiscal de empresas.",
|
||||
"description_en": "Created by Decree no. 29/2014 of 6 June and located in Mocuba City. APIEX describes this IFZ as mobilizing resources and an operator. It is distinct from Mocuba SEZ and does not imply tax eligibility for companies.",
|
||||
"aliases": ["Mocuba IFZ", "ZFI de Mocuba"],
|
||||
"activities": [],
|
||||
"source": {
|
||||
"title": "Mocuba IFZ – APIEX",
|
||||
"url": "https://apiex.gov.mz/zif/mocuba-free-industrial-zone/",
|
||||
"diploma": "Decreto n.º 29/2014, de 6 de Junho",
|
||||
"article": "Act of creation; article reference pending review",
|
||||
"valid_from": "2014-06-06",
|
||||
"competent_entity": "Conselho de Ministros / APIEX"
|
||||
},
|
||||
"territories": [
|
||||
{
|
||||
"name": "CIDADE DE MOCUBA",
|
||||
"type": "admin_post",
|
||||
"scope_type": "administrative"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"code": "MOZ-NACALA-ZEE",
|
||||
"name": "Zona Económica Especial de Nacala",
|
||||
"name_en": "Nacala Special Economic Zone",
|
||||
"category": "zee",
|
||||
"physical_category": "economic_zone",
|
||||
"legal_regime": "zee",
|
||||
"operational_status": "unknown",
|
||||
"area_value": 1539,
|
||||
"area_unit": "km2",
|
||||
"description": "A APIEX indica criação pelo Decreto n.º 76/2007, de 18 de Dezembro, abrangência dos distritos de Nacala e Nacala-à-Velha e área de 1.539 km². Não se confunde com o Corredor de Nacala, porto ou outras zonas francas. Os números de projectos e empregos publicados na página não são importados como actuais por não terem data de referência confirmada.",
|
||||
"description_en": "APIEX reports establishment by Decree no. 76/2007 of 18 December, covering Nacala and Nacala-à-Velha districts, with an area of 1,539 km². It is distinct from the Nacala Corridor, port, and other free zones. Project and employment counts on the page are not imported as current because their reference date is not confirmed.",
|
||||
"aliases": ["ZEEN", "Nacala SEZ"],
|
||||
"activities": [
|
||||
{
|
||||
"name": "Logística e indústria",
|
||||
"status": "potential",
|
||||
"source_reference": "APIEX Nacala SEZ page; sectors identified as potential"
|
||||
},
|
||||
{
|
||||
"name": "Agro-processamento e indústria química",
|
||||
"status": "potential",
|
||||
"source_reference": "APIEX Nacala SEZ page; sectors identified as potential"
|
||||
}
|
||||
],
|
||||
"source": {
|
||||
"title": "Nacala Special Economic Zone – APIEX",
|
||||
"url": "https://apiex.gov.mz/nacala-special-economic-zone/",
|
||||
"diploma": "Decreto n.º 76/2007, de 18 de Dezembro",
|
||||
"article": "Act of creation; article reference pending review",
|
||||
"valid_from": "2007-12-18",
|
||||
"competent_entity": "Conselho de Ministros / APIEX"
|
||||
},
|
||||
"territories": [
|
||||
{
|
||||
"name": "NACALA-PORTO",
|
||||
"type": "district",
|
||||
"scope_type": "legal_scope"
|
||||
},
|
||||
{
|
||||
"name": "NACALA-VELHA",
|
||||
"source_name": "Nacala-à-Velha",
|
||||
"type": "district",
|
||||
"scope_type": "legal_scope"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ return new class extends Migration
|
||||
$t->dropConstrainedForeignId('primary_territory_id');
|
||||
$t->dropColumn('territory_version');
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ return new class extends Migration
|
||||
]);
|
||||
DB::statement('DROP INDEX IF EXISTS simulation_form_drafts_owner_active_unique');
|
||||
DB::statement('CREATE INDEX simulation_form_drafts_owner_active_idx ON public.simulation_form_drafts(owner_user_id, updated_at DESC) WHERE owner_user_id IS NOT NULL AND deleted_at IS NULL');
|
||||
DB::statement("CREATE UNIQUE INDEX simulation_form_drafts_claimed_token_unique ON public.simulation_form_drafts(claimed_token_hash) WHERE claimed_token_hash IS NOT NULL");
|
||||
DB::statement('CREATE UNIQUE INDEX simulation_form_drafts_claimed_token_unique ON public.simulation_form_drafts(claimed_token_hash) WHERE claimed_token_hash IS NOT NULL');
|
||||
|
||||
Schema::create('simulation_draft_business_links', function ($table): void {
|
||||
$table->uuid('id')->primary();
|
||||
|
||||
@@ -26,7 +26,7 @@ return new class extends Migration
|
||||
DB::statement("ALTER TABLE investment_zones ADD CONSTRAINT investment_zones_physical_category_check CHECK (physical_category IS NULL OR physical_category IN ('economic_zone','industrial_park','industrial_free_zone','integrated_tourism_area','other','unknown'))");
|
||||
DB::statement("ALTER TABLE investment_zones ADD CONSTRAINT investment_zones_legal_regime_check CHECK (legal_regime IS NULL OR legal_regime IN ('zee','zfi','general','pending','not_applicable'))");
|
||||
DB::statement("ALTER TABLE investment_zones ADD CONSTRAINT investment_zones_operational_status_check CHECK (operational_status IN ('operational','implementation','planned','suspended','unknown'))");
|
||||
DB::statement("ALTER TABLE investment_zones ADD CONSTRAINT investment_zones_area_check CHECK (area_value IS NULL OR area_value >= 0)");
|
||||
DB::statement('ALTER TABLE investment_zones ADD CONSTRAINT investment_zones_area_check CHECK (area_value IS NULL OR area_value >= 0)');
|
||||
|
||||
Schema::create('investment_zone_locations', function (Blueprint $table): void {
|
||||
$table->uuid('id')->primary();
|
||||
@@ -65,7 +65,7 @@ return new class extends Migration
|
||||
DB::statement('ALTER TABLE investment_zones DROP CONSTRAINT IF EXISTS investment_zones_area_check, DROP CONSTRAINT IF EXISTS investment_zones_operational_status_check, DROP CONSTRAINT IF EXISTS investment_zones_legal_regime_check, DROP CONSTRAINT IF EXISTS investment_zones_physical_category_check');
|
||||
Schema::table('investment_zones', function (Blueprint $table): void {
|
||||
$table->dropConstrainedForeignId('parent_zone_id');
|
||||
$table->dropColumn(['physical_category','legal_regime','operational_status','area_value','area_unit','aliases','activities','resources','historical_territory','data_as_of','sources_consulted_at']);
|
||||
$table->dropColumn(['physical_category', 'legal_regime', 'operational_status', 'area_value', 'area_unit', 'aliases', 'activities', 'resources', 'historical_territory', 'data_as_of', 'sources_consulted_at']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -78,7 +78,7 @@ return new class extends Migration
|
||||
DB::statement("ALTER TABLE portal_resource_links ADD CONSTRAINT portal_resource_links_relationship_check CHECK (relationship IN ('applies_to','supports','version_of','translation_of','amends','repeals','implements','cites','related_to'))");
|
||||
DB::statement("ALTER TABLE portal_resource_links ADD CONSTRAINT portal_resource_links_applicability_check CHECK (applicability IN ('required','conditional','support','optional_template','evidence'))");
|
||||
DB::statement("ALTER TABLE portal_resource_links ADD CONSTRAINT portal_resource_links_basis_check CHECK ((applicability NOT IN ('required','conditional') OR length(trim(coalesce(basis_reference,''))) >= 5) AND (applicability <> 'conditional' OR length(trim(coalesce(condition_text,''))) >= 5))");
|
||||
DB::statement("ALTER TABLE portal_resources ADD CONSTRAINT portal_resources_metadata_version_check CHECK (metadata_version > 0)");
|
||||
DB::statement('ALTER TABLE portal_resources ADD CONSTRAINT portal_resources_metadata_version_check CHECK (metadata_version > 0)');
|
||||
DB::statement("ALTER TABLE portal_resource_revisions ADD CONSTRAINT portal_resource_revisions_change_type_check CHECK (change_type IN ('created','metadata_updated','published','unpublished'))");
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
|
||||
@@ -12,7 +12,7 @@ return new class extends Migration
|
||||
Schema::table('portal_resource_links', function (Blueprint $table): void {
|
||||
$table->string('review_status', 16)->default('pending');
|
||||
});
|
||||
DB::statement("ALTER TABLE portal_resource_links DROP CONSTRAINT portal_resource_links_target_type_check");
|
||||
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');
|
||||
|
||||
@@ -19,60 +19,128 @@ return new class extends Migration
|
||||
{
|
||||
$this->guard(true);
|
||||
Schema::create('desk_service_versions', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary(); $t->string('code'); $t->unsignedInteger('version'); $t->string('status')->default('draft');
|
||||
$t->jsonb('definition'); $t->jsonb('source'); $t->foreignId('author_id')->constrained('users')->restrictOnDelete();
|
||||
$t->foreignId('published_by')->nullable()->constrained('users')->restrictOnDelete(); $t->timestampTz('published_at')->nullable(); $t->timestampsTz(); $t->unique(['code', 'version']);
|
||||
$t->uuid('id')->primary();
|
||||
$t->string('code');
|
||||
$t->unsignedInteger('version');
|
||||
$t->string('status')->default('draft');
|
||||
$t->jsonb('definition');
|
||||
$t->jsonb('source');
|
||||
$t->foreignId('author_id')->constrained('users')->restrictOnDelete();
|
||||
$t->foreignId('published_by')->nullable()->constrained('users')->restrictOnDelete();
|
||||
$t->timestampTz('published_at')->nullable();
|
||||
$t->timestampsTz();
|
||||
$t->unique(['code', 'version']);
|
||||
});
|
||||
Schema::create('desk_cases', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary(); $t->foreignId('user_id')->constrained('users')->restrictOnDelete();
|
||||
$t->uuid('id')->primary();
|
||||
$t->foreignId('user_id')->constrained('users')->restrictOnDelete();
|
||||
$t->foreignUuid('organization_id')->nullable()->constrained('organizations')->restrictOnDelete();
|
||||
$t->foreignUuid('company_id')->nullable()->constrained('investment_company_declarations')->restrictOnDelete();
|
||||
$t->foreignUuid('submission_id')->nullable()->constrained('investment_submissions')->restrictOnDelete();
|
||||
$t->uuid('simulation_id')->nullable(); $t->string('title'); $t->uuid('client_key'); $t->unsignedInteger('version')->default(1);
|
||||
$t->text('payload'); $t->jsonb('workflow'); $t->timestampTz('closed_at')->nullable(); $t->timestampsTz(); $t->unique(['user_id', 'client_key']);
|
||||
$t->uuid('simulation_id')->nullable();
|
||||
$t->string('title');
|
||||
$t->uuid('client_key');
|
||||
$t->unsignedInteger('version')->default(1);
|
||||
$t->text('payload');
|
||||
$t->jsonb('workflow');
|
||||
$t->timestampTz('closed_at')->nullable();
|
||||
$t->timestampsTz();
|
||||
$t->unique(['user_id', 'client_key']);
|
||||
});
|
||||
Schema::create('desk_requests', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary(); $t->foreignUuid('case_id')->constrained('desk_cases')->restrictOnDelete();
|
||||
$t->uuid('id')->primary();
|
||||
$t->foreignUuid('case_id')->constrained('desk_cases')->restrictOnDelete();
|
||||
$t->foreignUuid('service_version_id')->constrained('desk_service_versions')->restrictOnDelete();
|
||||
$t->foreignId('analyst_id')->nullable()->constrained('users')->restrictOnDelete(); $t->string('status')->default('draft');
|
||||
$t->unsignedInteger('version')->default(1); $t->uuid('client_key'); $t->text('payload'); $t->string('protocol')->nullable()->unique();
|
||||
$t->text('submitted_snapshot')->nullable(); $t->jsonb('editable_fields')->nullable(); $t->timestampTz('submitted_at')->nullable();
|
||||
$t->timestampTz('due_at')->nullable(); $t->timestampTz('closed_at')->nullable(); $t->timestampsTz(); $t->unique(['case_id', 'client_key']);
|
||||
$t->foreignId('analyst_id')->nullable()->constrained('users')->restrictOnDelete();
|
||||
$t->string('status')->default('draft');
|
||||
$t->unsignedInteger('version')->default(1);
|
||||
$t->uuid('client_key');
|
||||
$t->text('payload');
|
||||
$t->string('protocol')->nullable()->unique();
|
||||
$t->text('submitted_snapshot')->nullable();
|
||||
$t->jsonb('editable_fields')->nullable();
|
||||
$t->timestampTz('submitted_at')->nullable();
|
||||
$t->timestampTz('due_at')->nullable();
|
||||
$t->timestampTz('closed_at')->nullable();
|
||||
$t->timestampsTz();
|
||||
$t->unique(['case_id', 'client_key']);
|
||||
});
|
||||
Schema::create('desk_documents', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary(); $t->foreignUuid('case_id')->constrained('desk_cases')->restrictOnDelete();
|
||||
$t->foreignUuid('document_id')->constrained('documents')->restrictOnDelete(); $t->foreignUuid('request_id')->nullable()->constrained('desk_requests')->restrictOnDelete();
|
||||
$t->string('category'); $t->string('stage')->nullable(); $t->date('valid_until')->nullable(); $t->text('source_reference');
|
||||
$t->foreignId('linked_by')->constrained('users')->restrictOnDelete(); $t->timestampTz('created_at')->useCurrent(); $t->unique(['case_id', 'document_id']);
|
||||
$t->uuid('id')->primary();
|
||||
$t->foreignUuid('case_id')->constrained('desk_cases')->restrictOnDelete();
|
||||
$t->foreignUuid('document_id')->constrained('documents')->restrictOnDelete();
|
||||
$t->foreignUuid('request_id')->nullable()->constrained('desk_requests')->restrictOnDelete();
|
||||
$t->string('category');
|
||||
$t->string('stage')->nullable();
|
||||
$t->date('valid_until')->nullable();
|
||||
$t->text('source_reference');
|
||||
$t->foreignId('linked_by')->constrained('users')->restrictOnDelete();
|
||||
$t->timestampTz('created_at')->useCurrent();
|
||||
$t->unique(['case_id', 'document_id']);
|
||||
});
|
||||
Schema::create('desk_events', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary(); $t->foreignUuid('case_id')->constrained('desk_cases')->restrictOnDelete();
|
||||
$t->foreignUuid('request_id')->nullable()->constrained('desk_requests')->restrictOnDelete(); $t->foreignId('actor_id')->constrained('users')->restrictOnDelete();
|
||||
$t->string('action'); $t->unsignedInteger('version'); $t->text('payload'); $t->timestampTz('created_at')->useCurrent();
|
||||
$t->uuid('id')->primary();
|
||||
$t->foreignUuid('case_id')->constrained('desk_cases')->restrictOnDelete();
|
||||
$t->foreignUuid('request_id')->nullable()->constrained('desk_requests')->restrictOnDelete();
|
||||
$t->foreignId('actor_id')->constrained('users')->restrictOnDelete();
|
||||
$t->string('action');
|
||||
$t->unsignedInteger('version');
|
||||
$t->text('payload');
|
||||
$t->timestampTz('created_at')->useCurrent();
|
||||
});
|
||||
Schema::create('desk_revisions', function (Blueprint $t) {
|
||||
$t->id(); $t->foreignUuid('request_id')->constrained('desk_requests')->restrictOnDelete(); $t->unsignedInteger('version');
|
||||
$t->text('snapshot'); $t->timestampTz('created_at')->useCurrent(); $t->unique(['request_id', 'version']);
|
||||
$t->id();
|
||||
$t->foreignUuid('request_id')->constrained('desk_requests')->restrictOnDelete();
|
||||
$t->unsignedInteger('version');
|
||||
$t->text('snapshot');
|
||||
$t->timestampTz('created_at')->useCurrent();
|
||||
$t->unique(['request_id', 'version']);
|
||||
});
|
||||
Schema::create('desk_messages', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary(); $t->foreignUuid('request_id')->constrained('desk_requests')->restrictOnDelete();
|
||||
$t->foreignId('actor_id')->constrained('users')->restrictOnDelete(); $t->uuid('client_key'); $t->text('body'); $t->timestampTz('created_at')->useCurrent();
|
||||
$t->uuid('id')->primary();
|
||||
$t->foreignUuid('request_id')->constrained('desk_requests')->restrictOnDelete();
|
||||
$t->foreignId('actor_id')->constrained('users')->restrictOnDelete();
|
||||
$t->uuid('client_key');
|
||||
$t->text('body');
|
||||
$t->timestampTz('created_at')->useCurrent();
|
||||
$t->unique(['request_id', 'actor_id', 'client_key']);
|
||||
});
|
||||
Schema::create('desk_notifications', function (Blueprint $t) {
|
||||
$t->id(); $t->foreignUuid('event_id')->constrained('desk_events')->restrictOnDelete(); $t->foreignId('user_id')->constrained('users')->restrictOnDelete();
|
||||
$t->string('title'); $t->string('href'); $t->timestampTz('read_at')->nullable(); $t->unique(['event_id', 'user_id']);
|
||||
$t->id();
|
||||
$t->foreignUuid('event_id')->constrained('desk_events')->restrictOnDelete();
|
||||
$t->foreignId('user_id')->constrained('users')->restrictOnDelete();
|
||||
$t->string('title');
|
||||
$t->string('href');
|
||||
$t->timestampTz('read_at')->nullable();
|
||||
$t->unique(['event_id', 'user_id']);
|
||||
});
|
||||
Schema::create('desk_institutional_links', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary(); $t->string('stage'); $t->string('entity'); $t->string('purpose'); $t->text('url'); $t->string('contact')->nullable();
|
||||
$t->string('status')->default('pending'); $t->unsignedInteger('version')->default(1); $t->text('source_reference');
|
||||
$t->timestampTz('verified_at')->nullable(); $t->foreignId('verified_by')->nullable()->constrained('users')->restrictOnDelete(); $t->timestampsTz();
|
||||
$t->uuid('id')->primary();
|
||||
$t->string('stage');
|
||||
$t->string('entity');
|
||||
$t->string('purpose');
|
||||
$t->text('url');
|
||||
$t->string('contact')->nullable();
|
||||
$t->string('status')->default('pending');
|
||||
$t->unsignedInteger('version')->default(1);
|
||||
$t->text('source_reference');
|
||||
$t->timestampTz('verified_at')->nullable();
|
||||
$t->foreignId('verified_by')->nullable()->constrained('users')->restrictOnDelete();
|
||||
$t->timestampsTz();
|
||||
});
|
||||
Schema::create('desk_payments', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary(); $t->foreignUuid('request_id')->constrained('desk_requests')->restrictOnDelete();
|
||||
$t->string('reference')->unique(); $t->decimal('amount', 18, 2); $t->string('currency', 3); $t->text('source_reference');
|
||||
$t->string('status')->default('pending'); $t->foreignUuid('document_id')->nullable()->constrained('documents')->restrictOnDelete();
|
||||
$t->foreignId('verified_by')->nullable()->constrained('users')->restrictOnDelete(); $t->text('reason')->nullable(); $t->unsignedInteger('version')->default(1); $t->timestampsTz();
|
||||
$t->uuid('id')->primary();
|
||||
$t->foreignUuid('request_id')->constrained('desk_requests')->restrictOnDelete();
|
||||
$t->string('reference')->unique();
|
||||
$t->decimal('amount', 18, 2);
|
||||
$t->string('currency', 3);
|
||||
$t->text('source_reference');
|
||||
$t->string('status')->default('pending');
|
||||
$t->foreignUuid('document_id')->nullable()->constrained('documents')->restrictOnDelete();
|
||||
$t->foreignId('verified_by')->nullable()->constrained('users')->restrictOnDelete();
|
||||
$t->text('reason')->nullable();
|
||||
$t->unsignedInteger('version')->default(1);
|
||||
$t->timestampsTz();
|
||||
});
|
||||
DB::statement("ALTER TABLE desk_requests ADD CONSTRAINT desk_request_status CHECK (status IN ('draft','submitted','reviewing','returned','opinion','accepted','rejected','closed','cancelled'))");
|
||||
DB::statement("ALTER TABLE desk_requests ADD CONSTRAINT desk_request_receipt CHECK (status IN ('draft','cancelled') OR (protocol IS NOT NULL AND submitted_snapshot IS NOT NULL AND submitted_at IS NOT NULL))");
|
||||
@@ -83,8 +151,12 @@ return new class extends Migration
|
||||
{
|
||||
$this->guard();
|
||||
foreach (['desk_cases', 'desk_service_versions', 'desk_institutional_links'] as $table) {
|
||||
if (DB::table($table)->exists()) throw new RuntimeException('Preserve virtual desk data before rollback.');
|
||||
if (DB::table($table)->exists()) {
|
||||
throw new RuntimeException('Preserve virtual desk data before rollback.');
|
||||
}
|
||||
}
|
||||
foreach (['desk_payments', 'desk_institutional_links', 'desk_notifications', 'desk_messages', 'desk_revisions', 'desk_events', 'desk_documents', 'desk_requests', 'desk_cases', 'desk_service_versions'] as $table) {
|
||||
Schema::drop($table);
|
||||
}
|
||||
foreach (['desk_payments', 'desk_institutional_links', 'desk_notifications', 'desk_messages', 'desk_revisions', 'desk_events', 'desk_documents', 'desk_requests', 'desk_cases', 'desk_service_versions'] as $table) Schema::drop($table);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
private function guard(): void
|
||||
{
|
||||
$identity = DB::selectOne('select current_database() as database, inet_server_port() as port');
|
||||
if (! (app()->environment('testing') && str_ends_with($identity->database, '_test') && (int) $identity->port === 55441)
|
||||
&& ! config('virtual-desk.installation_authorized')) {
|
||||
throw new RuntimeException('Guidance requires isolated QA or authenticated installation with verified restore.');
|
||||
}
|
||||
}
|
||||
|
||||
public function up(): void
|
||||
{
|
||||
$this->guard();
|
||||
DB::statement('ALTER TABLE desk_events ADD COLUMN event_order bigint GENERATED ALWAYS AS IDENTITY');
|
||||
DB::statement('CREATE UNIQUE INDEX desk_event_order_unique ON desk_events(event_order)');
|
||||
Schema::table('desk_cases', function (Blueprint $t) {
|
||||
$t->foreignUuid('investor_id')->nullable()->constrained('investors')->restrictOnDelete();
|
||||
$t->foreignUuid('project_id')->nullable()->constrained('projects')->restrictOnDelete();
|
||||
$t->foreignUuid('journey_id')->nullable()->constrained('investor_journeys')->restrictOnDelete();
|
||||
$t->foreignId('interest_request_id')->nullable()->constrained('interest_requests')->restrictOnDelete();
|
||||
});
|
||||
Schema::table('tasks', function (Blueprint $t) {
|
||||
$t->foreignUuid('desk_case_id')->nullable()->constrained('desk_cases')->restrictOnDelete();
|
||||
$t->foreignUuid('desk_request_id')->nullable()->constrained('desk_requests')->restrictOnDelete();
|
||||
$t->string('guidance_key', 100)->nullable();
|
||||
$t->foreignId('accepted_by')->nullable()->constrained('users')->restrictOnDelete();
|
||||
$t->timestampTz('accepted_at')->nullable();
|
||||
$t->string('external_sync_status')->nullable();
|
||||
$t->unique(['desk_case_id', 'guidance_key']);
|
||||
});
|
||||
DB::statement("ALTER TABLE tasks ADD CONSTRAINT guidance_task_acceptance CHECK (desk_case_id IS NULL OR (accepted_by IS NOT NULL AND accepted_at IS NOT NULL AND guidance_key IS NOT NULL AND data_classification = 'confidential' AND external_sync_status = 'pending_authorization'))");
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
$this->guard();
|
||||
if (DB::table('tasks')->whereNotNull('desk_case_id')->exists() || DB::table('desk_cases')->whereNotNull('investor_id')->exists()) {
|
||||
throw new RuntimeException('Preserve guidance associations and accepted tasks before rollback.');
|
||||
}
|
||||
DB::statement('ALTER TABLE tasks DROP CONSTRAINT guidance_task_acceptance');
|
||||
DB::statement('ALTER TABLE desk_events DROP COLUMN event_order');
|
||||
Schema::table('tasks', function (Blueprint $t) {
|
||||
$t->dropUnique(['desk_case_id', 'guidance_key']);
|
||||
foreach (['desk_case_id', 'desk_request_id', 'accepted_by'] as $column) {
|
||||
$t->dropConstrainedForeignId($column);
|
||||
}
|
||||
$t->dropColumn(['guidance_key', 'accepted_at', 'external_sync_status']);
|
||||
});
|
||||
Schema::table('desk_cases', function (Blueprint $t) {
|
||||
foreach (['investor_id', 'project_id', 'journey_id', 'interest_request_id'] as $column) {
|
||||
$t->dropConstrainedForeignId($column);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
+128
-124
@@ -1,128 +1,132 @@
|
||||
name: investmoz-development
|
||||
services:
|
||||
app:
|
||||
build:
|
||||
context: ../../vendor/laravel/sail/runtimes/8.5
|
||||
args:
|
||||
WWWGROUP: '${WWWGROUP:-1000}'
|
||||
NODE_VERSION: '24'
|
||||
POSTGRES_VERSION: '17'
|
||||
image: investmoz-development-php85
|
||||
environment: &application
|
||||
WWWUSER: '${WWWUSER:-1000}'
|
||||
APP_ENV: local
|
||||
APP_DEBUG: 'false'
|
||||
APP_KEY: '${APP_KEY:?Generate the isolated development key first}'
|
||||
APP_URL: 'http://localhost:${PORTAL_PORT:-8088}'
|
||||
DB_CONNECTION: pgsql
|
||||
DB_HOST: postgres
|
||||
DB_PORT: '5432'
|
||||
DB_DATABASE: investmoz_development
|
||||
DB_USERNAME: investmoz
|
||||
DB_PASSWORD: '${DB_PASSWORD:?Generate a local database password first}'
|
||||
DB_URL: ''
|
||||
CACHE_STORE: database
|
||||
QUEUE_CONNECTION: database
|
||||
DB_QUEUE_RETRY_AFTER: '210'
|
||||
SESSION_DRIVER: database
|
||||
SESSION_SECURE_COOKIE: 'false'
|
||||
MAIL_MAILER: array
|
||||
PORTAL_NOTIFICATIONS_ENABLED: 'false'
|
||||
LOG_CHANNEL: stderr
|
||||
FILESYSTEM_DISK: local
|
||||
MALWARE_SCANNER: clamd
|
||||
CLAMD_HOST: clamav
|
||||
CLAMD_PORT: '3310'
|
||||
CLAMD_TIMEOUT_SECONDS: '120'
|
||||
CLAMD_MAX_SIGNATURE_AGE_HOURS: '48'
|
||||
CLAMD_MAX_FILE_BYTES: '20971520'
|
||||
CLAMD_MAX_STREAM_BYTES: '26214400'
|
||||
CLAMD_MAX_DECOMPRESSED_BYTES: '104857600'
|
||||
CLAMD_MAX_ARCHIVE_ENTRIES: '1000'
|
||||
volumes:
|
||||
- ../..:/var/www/html
|
||||
- ./runtime.env.example:/var/www/html/.env:ro
|
||||
- application-storage:/var/www/html/storage
|
||||
- application-cache:/var/www/html/bootstrap/cache
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- default
|
||||
- malware-private
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '--fail', 'http://127.0.0.1/up']
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
web:
|
||||
image: nginx:stable-alpine
|
||||
ports:
|
||||
- '127.0.0.1:${PORTAL_PORT:-8088}:80'
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
queue:
|
||||
image: investmoz-development-php85
|
||||
environment:
|
||||
<<: *application
|
||||
SUPERVISOR_PHP_COMMAND: '/usr/bin/php /var/www/html/artisan queue:work --sleep=3 --tries=3 --timeout=150'
|
||||
volumes:
|
||||
- ../..:/var/www/html
|
||||
- ./runtime.env.example:/var/www/html/.env:ro
|
||||
- application-storage:/var/www/html/storage
|
||||
- application-cache:/var/www/html/bootstrap/cache
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- default
|
||||
- malware-private
|
||||
postgres:
|
||||
image: postgis/postgis:17-3.5
|
||||
environment:
|
||||
POSTGRES_DB: investmoz_development
|
||||
POSTGRES_USER: investmoz
|
||||
POSTGRES_PASSWORD: '${DB_PASSWORD:?Generate a local database password first}'
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ['CMD-SHELL', 'pg_isready -U investmoz -d investmoz_development']
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
networks:
|
||||
- default
|
||||
clamav:
|
||||
image: clamav/clamav:1.5.4
|
||||
environment:
|
||||
CLAMAV_NO_MILTERD: 'true'
|
||||
CLAMD_STARTUP_TIMEOUT: '1800'
|
||||
FRESHCLAM_CHECKS: '12'
|
||||
volumes:
|
||||
- clamav-database:/var/lib/clamav
|
||||
- ./clamd.conf:/etc/clamav/clamd.conf:ro
|
||||
mem_limit: 4g
|
||||
cpus: 2.0
|
||||
logging:
|
||||
options:
|
||||
max-size: '10m'
|
||||
max-file: '3'
|
||||
healthcheck:
|
||||
test: ['CMD', 'clamdscan', '--ping=3']
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30m
|
||||
networks:
|
||||
- malware-private
|
||||
app:
|
||||
build:
|
||||
context: ../../vendor/laravel/sail/runtimes/8.5
|
||||
args:
|
||||
WWWGROUP: '${WWWGROUP:-1000}'
|
||||
NODE_VERSION: '24'
|
||||
POSTGRES_VERSION: '17'
|
||||
image: investmoz-development-php85
|
||||
environment: &application
|
||||
WWWUSER: '${WWWUSER:-1000}'
|
||||
APP_ENV: local
|
||||
APP_DEBUG: 'false'
|
||||
APP_KEY: '${APP_KEY:?Generate the isolated development key first}'
|
||||
APP_URL: 'http://localhost:${PORTAL_PORT:-8088}'
|
||||
DB_CONNECTION: pgsql
|
||||
DB_HOST: postgres
|
||||
DB_PORT: '5432'
|
||||
DB_DATABASE: investmoz_development
|
||||
DB_USERNAME: investmoz
|
||||
DB_PASSWORD: '${DB_PASSWORD:?Generate a local database password first}'
|
||||
DB_URL: ''
|
||||
CACHE_STORE: database
|
||||
QUEUE_CONNECTION: database
|
||||
DB_QUEUE_RETRY_AFTER: '210'
|
||||
SESSION_DRIVER: database
|
||||
SESSION_SECURE_COOKIE: 'false'
|
||||
MAIL_MAILER: array
|
||||
PORTAL_NOTIFICATIONS_ENABLED: 'false'
|
||||
LOG_CHANNEL: stderr
|
||||
FILESYSTEM_DISK: local
|
||||
MALWARE_SCANNER: clamd
|
||||
CLAMD_HOST: clamav
|
||||
CLAMD_PORT: '3310'
|
||||
CLAMD_TIMEOUT_SECONDS: '120'
|
||||
CLAMD_MAX_SIGNATURE_AGE_HOURS: '48'
|
||||
CLAMD_MAX_FILE_BYTES: '20971520'
|
||||
CLAMD_MAX_STREAM_BYTES: '26214400'
|
||||
CLAMD_MAX_DECOMPRESSED_BYTES: '104857600'
|
||||
CLAMD_MAX_ARCHIVE_ENTRIES: '1000'
|
||||
volumes:
|
||||
- ../..:/var/www/html
|
||||
- ./runtime.env.example:/var/www/html/.env:ro
|
||||
- application-storage:/var/www/html/storage
|
||||
- application-cache:/var/www/html/bootstrap/cache
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- default
|
||||
- malware-private
|
||||
healthcheck:
|
||||
test: ['CMD', 'curl', '--fail', 'http://127.0.0.1/up']
|
||||
interval: 15s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
web:
|
||||
image: nginx:stable-alpine
|
||||
ports:
|
||||
- '127.0.0.1:${PORTAL_PORT:-8088}:80'
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
queue:
|
||||
image: investmoz-development-php85
|
||||
environment:
|
||||
<<: *application
|
||||
SUPERVISOR_PHP_COMMAND: '/usr/bin/php /var/www/html/artisan queue:work --sleep=3 --tries=3 --timeout=150'
|
||||
volumes:
|
||||
- ../..:/var/www/html
|
||||
- ./runtime.env.example:/var/www/html/.env:ro
|
||||
- application-storage:/var/www/html/storage
|
||||
- application-cache:/var/www/html/bootstrap/cache
|
||||
depends_on:
|
||||
app:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- default
|
||||
- malware-private
|
||||
postgres:
|
||||
image: postgis/postgis:17-3.5
|
||||
environment:
|
||||
POSTGRES_DB: investmoz_development
|
||||
POSTGRES_USER: investmoz
|
||||
POSTGRES_PASSWORD: '${DB_PASSWORD:?Generate a local database password first}'
|
||||
volumes:
|
||||
- database:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test:
|
||||
[
|
||||
'CMD-SHELL',
|
||||
'pg_isready -U investmoz -d investmoz_development',
|
||||
]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
networks:
|
||||
- default
|
||||
clamav:
|
||||
image: clamav/clamav:1.5.4
|
||||
environment:
|
||||
CLAMAV_NO_MILTERD: 'true'
|
||||
CLAMD_STARTUP_TIMEOUT: '1800'
|
||||
FRESHCLAM_CHECKS: '12'
|
||||
volumes:
|
||||
- clamav-database:/var/lib/clamav
|
||||
- ./clamd.conf:/etc/clamav/clamd.conf:ro
|
||||
mem_limit: 4g
|
||||
cpus: 2.0
|
||||
logging:
|
||||
options:
|
||||
max-size: '10m'
|
||||
max-file: '3'
|
||||
healthcheck:
|
||||
test: ['CMD', 'clamdscan', '--ping=3']
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 30m
|
||||
networks:
|
||||
- malware-private
|
||||
networks:
|
||||
malware-private:
|
||||
driver: bridge
|
||||
malware-private:
|
||||
driver: bridge
|
||||
volumes:
|
||||
database:
|
||||
application-storage:
|
||||
application-cache:
|
||||
clamav-database:
|
||||
database:
|
||||
application-storage:
|
||||
application-cache:
|
||||
clamav-database:
|
||||
|
||||
@@ -8,16 +8,16 @@ O repositório já tinha alterações funcionais locais no início da auditoria:
|
||||
|
||||
## Stack e versões confirmadas
|
||||
|
||||
| Componente | Baseline observado | Evidência |
|
||||
| --- | --- | --- |
|
||||
| Backend | Laravel 13.30.1 (`composer.lock`); `composer.json` exige PHP `^8.3` e Laravel `^13.17` | `composer.lock`, `composer.json` |
|
||||
| PHP CLI | PHP 8.5.11; `php.ini` em `C:\Users\macbook\.config\investmoz\php-8.5.11\php.ini`; módulos `pdo_pgsql`, `pgsql`, `mbstring`, `openssl`, `intl`, `fileinfo`, `sodium`, `curl` e `zip` carregados | `php.exe -v`, `--ini`, `-m` |
|
||||
| Composer | Composer 2.8.12; `composer.json` validado com o PHP 8.5.11 explícito | `composer.phar validate --no-check-publish` |
|
||||
| Frontend | React 19.2.8, React DOM 19.2.8, Inertia React 3.7.0, TypeScript 5.9.3, Vite 8.2.2, Vite Plus 0.3.0, Leaflet 1.9.4 | `package-lock.json` |
|
||||
| Node/npm | Node 24.20.0, npm 11.19.0 | `node --version`, `npm --version` |
|
||||
| Dados | PostgreSQL 17.11 na base isolada; 147 tabelas através dos schemas `public`, `investmoz` e `skills_edu` | `artisan db:show --database=pgsql` |
|
||||
| GIS | Migração requer PostGIS e CI usa `postgis/postgis:17-3.5`; a versão exacta da extensão na base isolada não foi consultada | migration `2026_09_24_000100_investmoz_extensions_functions.php`, `.github/workflows/tests.yml` |
|
||||
| UI/SSR | Laravel + Inertia + React/Vite; não é Next.js | `composer.json`, `package.json`, `vite.config.ts`, `resources/js` |
|
||||
| Componente | Baseline observado | Evidência |
|
||||
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- |
|
||||
| Backend | Laravel 13.30.1 (`composer.lock`); `composer.json` exige PHP `^8.3` e Laravel `^13.17` | `composer.lock`, `composer.json` |
|
||||
| PHP CLI | PHP 8.5.11; `php.ini` em `C:\Users\macbook\.config\investmoz\php-8.5.11\php.ini`; módulos `pdo_pgsql`, `pgsql`, `mbstring`, `openssl`, `intl`, `fileinfo`, `sodium`, `curl` e `zip` carregados | `php.exe -v`, `--ini`, `-m` |
|
||||
| Composer | Composer 2.8.12; `composer.json` validado com o PHP 8.5.11 explícito | `composer.phar validate --no-check-publish` |
|
||||
| Frontend | React 19.2.8, React DOM 19.2.8, Inertia React 3.7.0, TypeScript 5.9.3, Vite 8.2.2, Vite Plus 0.3.0, Leaflet 1.9.4 | `package-lock.json` |
|
||||
| Node/npm | Node 24.20.0, npm 11.19.0 | `node --version`, `npm --version` |
|
||||
| Dados | PostgreSQL 17.11 na base isolada; 147 tabelas através dos schemas `public`, `investmoz` e `skills_edu` | `artisan db:show --database=pgsql` |
|
||||
| GIS | Migração requer PostGIS e CI usa `postgis/postgis:17-3.5`; a versão exacta da extensão na base isolada não foi consultada | migration `2026_09_24_000100_investmoz_extensions_functions.php`, `.github/workflows/tests.yml` |
|
||||
| UI/SSR | Laravel + Inertia + React/Vite; não é Next.js | `composer.json`, `package.json`, `vite.config.ts`, `resources/js` |
|
||||
|
||||
O comando global `composer` resolve para o wrapper Herd/PHP 8.5.0. Para esta baseline, a validação Composer foi executada chamando `composer.phar` com o binário PHP 8.5.11 indicado pelo utilizador.
|
||||
|
||||
@@ -79,29 +79,29 @@ Inventário local: 36 ficheiros de testes Feature e 5 Unit, mais testes Node. `p
|
||||
|
||||
**Gate executado nesta auditoria:**
|
||||
|
||||
| Comando | Resultado |
|
||||
| --- | --- |
|
||||
| PHP 8.5.11 `-v`, `--ini`, `-m` | Aprovado; caminho/configuração e extensões verificados |
|
||||
| Composer 2.8.12 `validate --no-check-publish`, invocado pelo PHP 8.5.11 | Aprovado: `composer.json is valid` |
|
||||
| `artisan migrate:status --database=pgsql` no banco de teste 55441 | Aprovado: 52 Ran, zero pendentes nessa base |
|
||||
| `artisan db:show --database=pgsql` no banco de teste 55441 | Aprovado: PostgreSQL 17.11; 147 tabelas |
|
||||
| `npm.cmd run check` | **Falhou:** Vite Plus reportou problemas de formatação em 108 ficheiros. Não foi aplicado autofix. A execução parou aqui, conforme o gate pedido. |
|
||||
| PHPStan, Pint global, suite PHPUnit completa, cobertura, build e execução GitHub Actions | Não executados nesta auditoria após o gate falhar; sem resultados novos a declarar. |
|
||||
| Comando | Resultado |
|
||||
| ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| PHP 8.5.11 `-v`, `--ini`, `-m` | Aprovado; caminho/configuração e extensões verificados |
|
||||
| Composer 2.8.12 `validate --no-check-publish`, invocado pelo PHP 8.5.11 | Aprovado: `composer.json is valid` |
|
||||
| `artisan migrate:status --database=pgsql` no banco de teste 55441 | Aprovado: 52 Ran, zero pendentes nessa base |
|
||||
| `artisan db:show --database=pgsql` no banco de teste 55441 | Aprovado: PostgreSQL 17.11; 147 tabelas |
|
||||
| `npm.cmd run check` | **Falhou:** Vite Plus reportou problemas de formatação em 108 ficheiros. Não foi aplicado autofix. A execução parou aqui, conforme o gate pedido. |
|
||||
| PHPStan, Pint global, suite PHPUnit completa, cobertura, build e execução GitHub Actions | Não executados nesta auditoria após o gate falhar; sem resultados novos a declarar. |
|
||||
|
||||
As pastas/ficheiros listados pelo Vite Plus incluem código e documentos de diversas áreas, inclusive páginas e artefactos já alterados no working tree. Não foram modificados para preservar o escopo da Etapa 0.
|
||||
|
||||
## Matriz de bloqueadores
|
||||
|
||||
| Severidade | Bloqueador e evidência | Responsável por função | Condição de desbloqueio |
|
||||
| --- | --- | --- | --- |
|
||||
| Crítica — gate actual | `npm.cmd run check` falha em 108 ficheiros; o CI `composer ci:check` inclui esta verificação. | Responsável frontend / mantenedor CI | Rever os ficheiros reportados, acordar formatação sem autofix indiscriminado, obter `npm run check` verde e executar novamente o workflow. |
|
||||
| Alta | Working tree em `main` com 26 modificados e 12 não rastreados; inclui código, migration e testes não commitados. | Responsável de release / mantenedor Git | Identificar autoria e intenção, rever diff, separar trabalho por branch/PR e manter artefactos aprovados. |
|
||||
| Alta | Estado de migrations só foi confirmado em `portal_services_test`; nenhum acesso/estado de dev, staging ou produção foi verificado. | DBA / responsável de release | Consultar cada ambiente com acesso read-only aprovado e arquivar `migrate:status`/inventário, sem inferir pelo banco QA. |
|
||||
| Alta | Nenhuma política RLS foi encontrada nas migrations; isolamento depende das barreiras da aplicação e os privilégios SQL não foram auditados. | Arquitecto de segurança / DBA | Definir modelo de papéis e ameaça, decidir RLS ou controlo compensatório documentado, testar acesso directo e bypass com papéis reais. |
|
||||
| Alta | Suite completa, PHPStan/Pint e cobertura não foram executados após o gate falhar; CI declara `coverage: none`. | Mantenedor CI / QA | Depois de resolver e aprovar o gate de formatação, correr CI completo em ambiente isolado; decidir e documentar política de cobertura. |
|
||||
| Média | OpenAPI fragmentado por domínio, versões 3.0.3/3.1.0 e sem validação global observada no workflow. | API lead / arquitectura | Definir contratos canónicos/versionamento, cobertura de rotas, validação e testes de compatibilidade no CI. |
|
||||
| Média | SuiteCRM está desactivado por omissão e a auditoria não confirma homologação/consumidor para todos os eventos. | Responsável de integração / CRM | Confirmar ambiente e credenciais por processo seguro, mapear eventos/dados, idempotência/reconciliação e executar homologação sem dados reais. |
|
||||
| Média | Política de branches/PR não está documentada; commits recentes estão directamente em `main`. | Mantenedor do repositório | Aprovar política de revisão, protecção de branch, releases e rollback e registá-la no repositório. |
|
||||
| Severidade | Bloqueador e evidência | Responsável por função | Condição de desbloqueio |
|
||||
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Crítica — gate actual | `npm.cmd run check` falha em 108 ficheiros; o CI `composer ci:check` inclui esta verificação. | Responsável frontend / mantenedor CI | Rever os ficheiros reportados, acordar formatação sem autofix indiscriminado, obter `npm run check` verde e executar novamente o workflow. |
|
||||
| Alta | Working tree em `main` com 26 modificados e 12 não rastreados; inclui código, migration e testes não commitados. | Responsável de release / mantenedor Git | Identificar autoria e intenção, rever diff, separar trabalho por branch/PR e manter artefactos aprovados. |
|
||||
| Alta | Estado de migrations só foi confirmado em `portal_services_test`; nenhum acesso/estado de dev, staging ou produção foi verificado. | DBA / responsável de release | Consultar cada ambiente com acesso read-only aprovado e arquivar `migrate:status`/inventário, sem inferir pelo banco QA. |
|
||||
| Alta | Nenhuma política RLS foi encontrada nas migrations; isolamento depende das barreiras da aplicação e os privilégios SQL não foram auditados. | Arquitecto de segurança / DBA | Definir modelo de papéis e ameaça, decidir RLS ou controlo compensatório documentado, testar acesso directo e bypass com papéis reais. |
|
||||
| Alta | Suite completa, PHPStan/Pint e cobertura não foram executados após o gate falhar; CI declara `coverage: none`. | Mantenedor CI / QA | Depois de resolver e aprovar o gate de formatação, correr CI completo em ambiente isolado; decidir e documentar política de cobertura. |
|
||||
| Média | OpenAPI fragmentado por domínio, versões 3.0.3/3.1.0 e sem validação global observada no workflow. | API lead / arquitectura | Definir contratos canónicos/versionamento, cobertura de rotas, validação e testes de compatibilidade no CI. |
|
||||
| Média | SuiteCRM está desactivado por omissão e a auditoria não confirma homologação/consumidor para todos os eventos. | Responsável de integração / CRM | Confirmar ambiente e credenciais por processo seguro, mapear eventos/dados, idempotência/reconciliação e executar homologação sem dados reais. |
|
||||
| Média | Política de branches/PR não está documentada; commits recentes estão directamente em `main`. | Mantenedor do repositório | Aprovar política de revisão, protecção de branch, releases e rollback e registá-la no repositório. |
|
||||
|
||||
Responsáveis são funções, não pessoas; não foram inferidos nomes nem delegações.
|
||||
|
||||
@@ -203,6 +203,7 @@ Não há nomes de autoridades sem fonte oficial aprovada; o endpoint informa cat
|
||||
Rollback: executar `php artisan migrate:rollback --step=1` apenas num ambiente onde esta migration seja a última aplicada e após exportar/publicar nenhum conteúdo de catálogo; o `down()` protege-se contra remoção de dados de autoridade/actividade. Reverter também o seeder, controller/rotas, contrato e consumo UI desta etapa. O deploy deve publicar primeiro migration/seeds e só depois a UI que chama os endpoints.
|
||||
|
||||
**Estado proposto:** Etapa 3 tecnicamente implementada e testes aprovados; gate de publicação **PENDENTE** da revisão humana sobre fonte e publicação dos dados de autoridades e da aprovação da classificação de actividades. Não iniciar a etapa seguinte sem fechar esse gate.
|
||||
|
||||
## Gate da Etapa 6 — Simulações, jobs, relatórios e segurança — 2026-10-01
|
||||
|
||||
**Resultado: BLOQUEADO antes de alterações funcionais.** A inspecção confirmou Laravel 13.30.1, PHP CLI 8.5.11, React 19 com Inertia, PostgreSQL 17.11/PostGIS 3.6.2 e CI com PostgreSQL/PostGIS 17. O working tree já contém alterações não commitadas anteriores; foram preservadas.
|
||||
@@ -223,6 +224,7 @@ Após aprovação do modelo de parâmetros e das matrizes IAM/RLS, inspeccionar
|
||||
**Condição para desbloquear:** aprovar o esquema de parâmetros e snapshots; fornecer matriz actor × recurso × operação e tenant/contexto; aprovar papéis PostgreSQL separados, grants, comportamento de jobs e estratégia de conexão/pool; definir escopo/retensão de idempotência, política de soft delete e ciclo de vida do token anónimo; fixar retries/reconciliação por job.
|
||||
|
||||
**Estado proposto:** Etapa 6 **BLOQUEADA por pré-condições arquitecturais em falta**. Alteração desta tentativa: somente esta secção documental. Rollback: remover esta secção de `baseline.md`; não há alteração de código, migration, dado ou ambiente a reverter.
|
||||
|
||||
## Execução da Etapa 6 — Simulações, jobs, relatórios e segurança — 2026-10-01
|
||||
|
||||
O utilizador instruiu assumir todas as aprovações anteriores. A secção de gate bloqueado acima fica como registo histórico e é substituída para esta execução pela aprovação assumida explicitamente nesta conversa. A inspecção confirmou Laravel 13.30.1, PHP 8.5.11, React 19/Inertia, PostgreSQL 17.11/PostGIS 3.6.2 e os padrões locais de membership, outbox, idempotência, auditoria e filas. Não foram alteradas migrations anteriores nem `.env`.
|
||||
@@ -258,4 +260,4 @@ Os registos de `simulation_obligations` não são auto-gerados sem fonte version
|
||||
|
||||
Rollback: primeiro retirar o consumo das rotas, depois executar rollback direccionado à migration `2026_10_01_000200_create_simulation_execution_core.php` com o comando Artisan de rollback por `--path` no ambiente pretendido. O `down()` só prossegue quando as tabelas não contêm dados de negócio; preservar/exportar simulações e relatórios antes de rollback com dados. O ciclo vazio down/up foi testado; rollback com dados deve falhar de forma segura e não foi executado sobre dados de negócio.
|
||||
|
||||
**Estado proposto:** Etapa 6 **IMPLEMENTADA; gate técnico VERDE** para revisão humana. Não foi iniciado trabalho de etapa posterior nem feito deploy.
|
||||
**Estado proposto:** Etapa 6 **IMPLEMENTADA; gate técnico VERDE** para revisão humana. Não foi iniciado trabalho de etapa posterior nem feito deploy.
|
||||
|
||||
@@ -9,11 +9,11 @@
|
||||
|
||||
Esta governação define o MVP fiscal sem substituir funcionalidades existentes do Portal do Investidor. O âmbito funcional do Simulador Fiscal fica limitado ao que está aprovado nesta especificação; não declara homologação, publicação de regras, integração operacional ou cobertura fiscal que não tenha evidência.
|
||||
|
||||
| Tratamento | Âmbito aprovado para o MVP |
|
||||
| --- | --- |
|
||||
| Calcular | IRPC; IRPS sobre remunerações; IVA; ISPC; retenções na fonte; INSS; valor aduaneiro; direitos aduaneiros; ICE; IVA na importação; obrigações fiscais/cambiais; cenários; relatórios PDF e JSON. |
|
||||
| Identificar e orientar, sem cálculo automático | Imposto do selo; impostos autárquicos; mais-valias; preços de transferência; convenções completas de dupla tributação; benefícios complexos ZEE/ZFI; regimes sectoriais. |
|
||||
| Fora desta release | Manifestações formais; oportunidades de negócio; conversões para projecto/oportunidade; CRM completo; conta avançada do investidor; assistente inteligente. |
|
||||
| Tratamento | Âmbito aprovado para o MVP |
|
||||
| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Calcular | IRPC; IRPS sobre remunerações; IVA; ISPC; retenções na fonte; INSS; valor aduaneiro; direitos aduaneiros; ICE; IVA na importação; obrigações fiscais/cambiais; cenários; relatórios PDF e JSON. |
|
||||
| Identificar e orientar, sem cálculo automático | Imposto do selo; impostos autárquicos; mais-valias; preços de transferência; convenções completas de dupla tributação; benefícios complexos ZEE/ZFI; regimes sectoriais. |
|
||||
| Fora desta release | Manifestações formais; oportunidades de negócio; conversões para projecto/oportunidade; CRM completo; conta avançada do investidor; assistente inteligente. |
|
||||
|
||||
Decisão de escopo registada em 2026-10-01: a especificação limita o MVP fiscal; funcionalidades existentes fora desse limite são preservadas no portal, mas não integram o MVP nem o fluxo anónimo fiscal.
|
||||
|
||||
@@ -30,17 +30,17 @@ Decisão de escopo registada em 2026-10-01: a especificação limita o MVP fisca
|
||||
|
||||
## Matriz de conformidade e rastreabilidade
|
||||
|
||||
| Requisito/fase | Evidência actual | Estado | Lacuna ou divergência | Ficheiros/áreas de referência | Próximo gate |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Fase 0 — governação | Baseline, decisões e ADRs; este documento acrescenta escopo, RACI funcional, riscos, backlog e gates | CONFORME PARA INICIAR A FASE 1 | Responsáveis nominais e aprovações fiscal/jurídica/segurança/ops continuam necessários antes dos gates que delas dependem | `docs/implementation/baseline.md`, `docs/implementation/adr/`, este ficheiro | Gate da Fase 1 |
|
||||
| Fase 1 — fundação e contratos executáveis | Migrations, mapa SQL/OpenAPI, contratos por domínio e teste de ciclo de schema | PARCIAL | Contratos fragmentados em 3.0.3/3.1.0; validação integral e reconciliação de todos os caminhos/colunas não demonstradas neste gate | `database/migrations/`, `docs/implementation/sql-openapi-map.csv`, `docs/investmoz/*/openapi.json` | Inventário aprovado, validação OpenAPI integral e testes seguros em DB descartável |
|
||||
| Fase 2 — repositório legislativo | Existe rota de consulta de fontes legislativas e registos jurídicos noutros domínios | PARCIAL | Não há evidência suficiente de repositório fiscal versionado com disposições, vigência, hashes, relações, ingestão e pesquisa integral | `app/Http/Controllers/Api/V1/LegislationSourceController.php`, migrations, `routes/api.php` | Contrato e fontes oficiais aprovadas; verificar CRUD/importação e integridade |
|
||||
| Fase 3 — regras e publicação | Há catálogos versionados e padrões de revisão noutros domínios | AUSENTE para regras fiscais | Não foi identificada evidência de rule sets fiscais, parâmetros com fonte oficial, elegibilidade, revisão de quatro olhos, testes dourados e publicação imutável | migrations, models/services fiscais (a inventariar na Fase 3) | Aprovação de fontes, regras, responsabilidade fiscal/jurídica e vigência |
|
||||
| Fase 4 — API e formulário | `/Simulador`, catálogos, snapshots e execução aritmética existem | BLOQUEADO por divergência | O formulário contém identidade/contactos e finalidade de manifestação/oportunidade; a especificação limita o modo anónimo a dados económicos/fiscais/territoriais/operacionais e exclui esses fluxos. Decidir compatibilidade semântica antes de alterar a UI/API | `resources/js/lib/simulation-form.ts`, `resources/js/components/simulation-form.tsx`, `StoreSimulationFormDraftRequest.php`, `SimulationFormDrafts.php` | Decisão de produto/privacidade sobre fluxo anónimo e separação de funcionalidades fora da release |
|
||||
| Fase 5 — motor fiscal | Motor actual executa aritmética genérica introduzida pelo utilizador | AUSENTE para o âmbito fiscal | Não há evidência de cálculos fiscais oficiais de IRPC, IVA, ISPC, IRPS, INSS ou retenções com regras/fontes aprovadas, Decimal, arredondamento fiscal e casos dourados | `app/Services/SimulationExecution.php`, `app/Jobs/RunSimulation.php`, `docs/investmoz/simulations/openapi.json` | Aprovação de regras fiscais e especificação matemática antes de implementação |
|
||||
| Fase 6 — aduanas, câmbios e calendário | Campos/estrutura parcial de simulação e jobs genéricos | AUSENTE para os cálculos e calendário pedidos | Valor aduaneiro, direitos, ICE, IVA de importação, obrigações cambiais/fiscais e calendário não estão demonstrados como motor completo | app/migrations de simulação e integrações | Fontes e regras oficiais; contratos de moedas/origem e jobs/retries |
|
||||
| Fase 7 — relatórios e homologação | Snapshot de relatório JSON e hashes existem | PARCIAL | PDF, reconciliação integral API/DB/PDF/JSON, homologação, acessibilidade manual, resiliência e DR não demonstrados | `SimulationReport`, controller/serviço de simulação, testes | Contrato PDF/JSON, casos dourados, acessibilidade e prova de recuperação |
|
||||
| Fase 8 — release e estabilização | Workflow CI e compose de desenvolvimento existem | PARCIAL | Não há evidência de Go/No-Go fiscal, release imutável, backup/restauro, rollout/rollback ensaiados ou hypercare | `.github/workflows/tests.yml`, `deploy/docker/compose.yaml` | Aprovação de release, owners operacionais e evidência de rollout/rollback |
|
||||
| Requisito/fase | Evidência actual | Estado | Lacuna ou divergência | Ficheiros/áreas de referência | Próximo gate |
|
||||
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| Fase 0 — governação | Baseline, decisões e ADRs; este documento acrescenta escopo, RACI funcional, riscos, backlog e gates | CONFORME PARA INICIAR A FASE 1 | Responsáveis nominais e aprovações fiscal/jurídica/segurança/ops continuam necessários antes dos gates que delas dependem | `docs/implementation/baseline.md`, `docs/implementation/adr/`, este ficheiro | Gate da Fase 1 |
|
||||
| Fase 1 — fundação e contratos executáveis | Migrations, mapa SQL/OpenAPI, contratos por domínio e teste de ciclo de schema | PARCIAL | Contratos fragmentados em 3.0.3/3.1.0; validação integral e reconciliação de todos os caminhos/colunas não demonstradas neste gate | `database/migrations/`, `docs/implementation/sql-openapi-map.csv`, `docs/investmoz/*/openapi.json` | Inventário aprovado, validação OpenAPI integral e testes seguros em DB descartável |
|
||||
| Fase 2 — repositório legislativo | Existe rota de consulta de fontes legislativas e registos jurídicos noutros domínios | PARCIAL | Não há evidência suficiente de repositório fiscal versionado com disposições, vigência, hashes, relações, ingestão e pesquisa integral | `app/Http/Controllers/Api/V1/LegislationSourceController.php`, migrations, `routes/api.php` | Contrato e fontes oficiais aprovadas; verificar CRUD/importação e integridade |
|
||||
| Fase 3 — regras e publicação | Há catálogos versionados e padrões de revisão noutros domínios | AUSENTE para regras fiscais | Não foi identificada evidência de rule sets fiscais, parâmetros com fonte oficial, elegibilidade, revisão de quatro olhos, testes dourados e publicação imutável | migrations, models/services fiscais (a inventariar na Fase 3) | Aprovação de fontes, regras, responsabilidade fiscal/jurídica e vigência |
|
||||
| Fase 4 — API e formulário | `/Simulador`, catálogos, snapshots e execução aritmética existem | BLOQUEADO por divergência | O formulário contém identidade/contactos e finalidade de manifestação/oportunidade; a especificação limita o modo anónimo a dados económicos/fiscais/territoriais/operacionais e exclui esses fluxos. Decidir compatibilidade semântica antes de alterar a UI/API | `resources/js/lib/simulation-form.ts`, `resources/js/components/simulation-form.tsx`, `StoreSimulationFormDraftRequest.php`, `SimulationFormDrafts.php` | Decisão de produto/privacidade sobre fluxo anónimo e separação de funcionalidades fora da release |
|
||||
| Fase 5 — motor fiscal | Motor actual executa aritmética genérica introduzida pelo utilizador | AUSENTE para o âmbito fiscal | Não há evidência de cálculos fiscais oficiais de IRPC, IVA, ISPC, IRPS, INSS ou retenções com regras/fontes aprovadas, Decimal, arredondamento fiscal e casos dourados | `app/Services/SimulationExecution.php`, `app/Jobs/RunSimulation.php`, `docs/investmoz/simulations/openapi.json` | Aprovação de regras fiscais e especificação matemática antes de implementação |
|
||||
| Fase 6 — aduanas, câmbios e calendário | Campos/estrutura parcial de simulação e jobs genéricos | AUSENTE para os cálculos e calendário pedidos | Valor aduaneiro, direitos, ICE, IVA de importação, obrigações cambiais/fiscais e calendário não estão demonstrados como motor completo | app/migrations de simulação e integrações | Fontes e regras oficiais; contratos de moedas/origem e jobs/retries |
|
||||
| Fase 7 — relatórios e homologação | Snapshot de relatório JSON e hashes existem | PARCIAL | PDF, reconciliação integral API/DB/PDF/JSON, homologação, acessibilidade manual, resiliência e DR não demonstrados | `SimulationReport`, controller/serviço de simulação, testes | Contrato PDF/JSON, casos dourados, acessibilidade e prova de recuperação |
|
||||
| Fase 8 — release e estabilização | Workflow CI e compose de desenvolvimento existem | PARCIAL | Não há evidência de Go/No-Go fiscal, release imutável, backup/restauro, rollout/rollback ensaiados ou hypercare | `.github/workflows/tests.yml`, `deploy/docker/compose.yaml` | Aprovação de release, owners operacionais e evidência de rollout/rollback |
|
||||
|
||||
### Divergências que não serão resolvidas por inferência
|
||||
|
||||
@@ -54,29 +54,29 @@ Decisão de escopo registada em 2026-10-01: a especificação limita o MVP fisca
|
||||
|
||||
Não foram atribuídos nomes. A nomeação de responsáveis e a aprovação desta matriz são pendências do gate.
|
||||
|
||||
| Actividade | Responsável (R) | Accountable (A) | Consultados (C) | Informados (I) |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Aprovar fonte, regra, vigência e casos fiscais | Especialista fiscal | Dono de produto fiscal | Jurídico, dados, engenharia | QA, operações |
|
||||
| Publicar regra versionada | Engenharia fiscal | Dono de produto fiscal | Fiscal, jurídico, segurança | QA, operações |
|
||||
| Aprovar privacidade e fluxo anónimo | Privacidade/protecção de dados | Dono de produto | Jurídico, segurança, fiscal | Engenharia, QA |
|
||||
| Aprovar IAM/RLS/grants | DBA/segurança | Arquitectura de segurança | Engenharia, operações | Dono de produto, QA |
|
||||
| Homologar cálculos e relatórios | QA fiscal | Dono de produto fiscal | Fiscal, jurídico, engenharia | Operações |
|
||||
| Autorizar release, rollback e DR | Operações/release | Dono de serviço | DBA, segurança, QA, produto | Stakeholders |
|
||||
| Actividade | Responsável (R) | Accountable (A) | Consultados (C) | Informados (I) |
|
||||
| ---------------------------------------------- | ------------------------------ | ------------------------- | ---------------------------- | ------------------- |
|
||||
| Aprovar fonte, regra, vigência e casos fiscais | Especialista fiscal | Dono de produto fiscal | Jurídico, dados, engenharia | QA, operações |
|
||||
| Publicar regra versionada | Engenharia fiscal | Dono de produto fiscal | Fiscal, jurídico, segurança | QA, operações |
|
||||
| Aprovar privacidade e fluxo anónimo | Privacidade/protecção de dados | Dono de produto | Jurídico, segurança, fiscal | Engenharia, QA |
|
||||
| Aprovar IAM/RLS/grants | DBA/segurança | Arquitectura de segurança | Engenharia, operações | Dono de produto, QA |
|
||||
| Homologar cálculos e relatórios | QA fiscal | Dono de produto fiscal | Fiscal, jurídico, engenharia | Operações |
|
||||
| Autorizar release, rollback e DR | Operações/release | Dono de serviço | DBA, segurança, QA, produto | Stakeholders |
|
||||
|
||||
RACI é uma proposta de funções e não atribui autoridade a pessoas. Gate bloqueado até os responsáveis formais serem indicados e aceitarem.
|
||||
|
||||
## Backlog e gates de aceitação
|
||||
|
||||
| Prioridade | Item | Dependência | Critério de conclusão |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | Resolver divergência do modo anónimo, PII e funcionalidades fora da release | Decisão de produto e privacidade | Formulário e contrato aprovados sem recolha anónima vedada; decisão registada |
|
||||
| P0 | Aprovar fontes, regras, vigência e arredondamento de cada cálculo | Autoridade fiscal e jurídico | Fonte oficial/hash, regra versionada, casos dourados e dupla revisão registados |
|
||||
| P0 | Aprovar matriz IAM/RLS e papéis SQL | Segurança/DBA | Testes actor×recurso×operação com papel runtime sem bypass e contexto limpo |
|
||||
| P1 | Reconciliar OpenAPI integral, migrations e esquema | API/DBA | Validação semântica de contratos e cobertura de operações/campos no CI |
|
||||
| P1 | Implementar cálculos e obrigações do escopo | Regras aprovadas | Testes críticos 100%, totais aplicáveis ≥95%, Decimal e resultados explicáveis |
|
||||
| P1 | Aduanas, câmbios, calendário e jobs | Fontes/regras e contrato operacional | Casos dourados, idempotência/retry, reconciliação e dead-letter demonstrados |
|
||||
| P1 | PDF/JSON e acessibilidade/homologação | Schema de relatório e casos aprovados | API/DB/PDF/JSON reconciliados; teclado/leitor de ecrã/zoom e homologação registados |
|
||||
| P1 | Preparar release, backup/restauro e rollback | Operações/DBA | Release imutável, backup restaurado em prova, smoke tests e rollback ensaiados |
|
||||
| Prioridade | Item | Dependência | Critério de conclusão |
|
||||
| ---------- | --------------------------------------------------------------------------- | ------------------------------------- | ----------------------------------------------------------------------------------- |
|
||||
| P0 | Resolver divergência do modo anónimo, PII e funcionalidades fora da release | Decisão de produto e privacidade | Formulário e contrato aprovados sem recolha anónima vedada; decisão registada |
|
||||
| P0 | Aprovar fontes, regras, vigência e arredondamento de cada cálculo | Autoridade fiscal e jurídico | Fonte oficial/hash, regra versionada, casos dourados e dupla revisão registados |
|
||||
| P0 | Aprovar matriz IAM/RLS e papéis SQL | Segurança/DBA | Testes actor×recurso×operação com papel runtime sem bypass e contexto limpo |
|
||||
| P1 | Reconciliar OpenAPI integral, migrations e esquema | API/DBA | Validação semântica de contratos e cobertura de operações/campos no CI |
|
||||
| P1 | Implementar cálculos e obrigações do escopo | Regras aprovadas | Testes críticos 100%, totais aplicáveis ≥95%, Decimal e resultados explicáveis |
|
||||
| P1 | Aduanas, câmbios, calendário e jobs | Fontes/regras e contrato operacional | Casos dourados, idempotência/retry, reconciliação e dead-letter demonstrados |
|
||||
| P1 | PDF/JSON e acessibilidade/homologação | Schema de relatório e casos aprovados | API/DB/PDF/JSON reconciliados; teclado/leitor de ecrã/zoom e homologação registados |
|
||||
| P1 | Preparar release, backup/restauro e rollback | Operações/DBA | Release imutável, backup restaurado em prova, smoke tests e rollback ensaiados |
|
||||
|
||||
## Métrica, aprovação e bloqueio
|
||||
|
||||
|
||||
+10
-10
@@ -2,16 +2,16 @@
|
||||
|
||||
Base `/api/v1/ai`. Autenticação por sessão Laravel, email verificado, `manage-portal`, feature flag e CSRF do middleware web. Não é API OAuth/OIDC para terceiros. Enviar `Accept: application/json`; o React usa as rotas equivalentes `/management/ai` via Inertia.
|
||||
|
||||
| Método / recurso | Entrada | Resultado |
|
||||
| --- | --- | --- |
|
||||
| POST `/runs` | capability, purpose, language; query para assistant/search; source_id para summary; interest_id para classify; sector_id/province/maturity_level opcionais para recommend; source_id/nature opcionais para search | 201 com UUID; finalidade verificada também pelo gateway |
|
||||
| GET `/runs/{uuid}` | UUID | Metadados e resultado, ou resultado nulo quando fonte/consentimento revogados ou capacidade suspensa |
|
||||
| POST `/runs/{uuid}/feedback` | rating: helpful/unhelpful/unsafe; review_requested boolean | Feedback idempotente por utilizador/resultado; unsafe suspende a capacidade |
|
||||
| POST `/runs/{uuid}/review` | status: accepted/rejected; version_no | Revisão independente, conflito 409 em versão antiga; nenhum envio externo |
|
||||
| POST `/documents` | Multipart file texto simples, authorised=true | 201 document_id canónico; não torna a fonte pesquisável |
|
||||
| POST `/sources` | document_id, title, language pt/en, nature, source_url HTTPS, source_owner, evidence_reference, source_date, valid_until, authorised=true | Fonte pendente e fragmentos indexados, ainda inelegíveis |
|
||||
| POST `/sources/{uuid}/review` | status: approved/revoked; expected_status | Confirmação por outra pessoa para aprovar; conflito se estado mudou |
|
||||
| POST `/controls/{capability}` | suspended boolean | Suspensão imediata no armazenamento partilhado; fora do limite de consultas, mantendo autorização/CSRF |
|
||||
| Método / recurso | Entrada | Resultado |
|
||||
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
||||
| POST `/runs` | capability, purpose, language; query para assistant/search; source_id para summary; interest_id para classify; sector_id/province/maturity_level opcionais para recommend; source_id/nature opcionais para search | 201 com UUID; finalidade verificada também pelo gateway |
|
||||
| GET `/runs/{uuid}` | UUID | Metadados e resultado, ou resultado nulo quando fonte/consentimento revogados ou capacidade suspensa |
|
||||
| POST `/runs/{uuid}/feedback` | rating: helpful/unhelpful/unsafe; review_requested boolean | Feedback idempotente por utilizador/resultado; unsafe suspende a capacidade |
|
||||
| POST `/runs/{uuid}/review` | status: accepted/rejected; version_no | Revisão independente, conflito 409 em versão antiga; nenhum envio externo |
|
||||
| POST `/documents` | Multipart file texto simples, authorised=true | 201 document_id canónico; não torna a fonte pesquisável |
|
||||
| POST `/sources` | document_id, title, language pt/en, nature, source_url HTTPS, source_owner, evidence_reference, source_date, valid_until, authorised=true | Fonte pendente e fragmentos indexados, ainda inelegíveis |
|
||||
| POST `/sources/{uuid}/review` | status: approved/revoked; expected_status | Confirmação por outra pessoa para aprovar; conflito se estado mudou |
|
||||
| POST `/controls/{capability}` | suspended boolean | Suspensão imediata no armazenamento partilhado; fora do limite de consultas, mantendo autorização/CSRF |
|
||||
|
||||
Capacidades: assistant, search, recommend, classify, quality, sla, summary. Finalidades: investment_information para pesquisa/recomendação/excertos; interest_triage para triagem; data_quality e execution_monitoring para monitores.
|
||||
|
||||
|
||||
@@ -30,15 +30,15 @@ O React apenas comunica com Laravel. A primeira interface é interna, para gesto
|
||||
|
||||
## 3. Linha de base dos sete casos
|
||||
|
||||
| Caso | Execução inicial | Limite explícito |
|
||||
| --- | --- | --- |
|
||||
| UC-IA-001 | Recuperar excertos autorizados com fonte, data, natureza e recusa sem resultados | Sem resposta generativa ou aconselhamento jurídico/financeiro |
|
||||
| UC-IA-002 | Full-text search PT/EN, filtros de fontes e acesso antes da recuperação | Híbrido/embeddings/reranker exigem infraestrutura e benchmark |
|
||||
| Caso | Execução inicial | Limite explícito |
|
||||
| --------- | -------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| UC-IA-001 | Recuperar excertos autorizados com fonte, data, natureza e recusa sem resultados | Sem resposta generativa ou aconselhamento jurídico/financeiro |
|
||||
| UC-IA-002 | Full-text search PT/EN, filtros de fontes e acesso antes da recuperação | Híbrido/embeddings/reranker exigem infraestrutura e benchmark |
|
||||
| UC-IA-003 | Correspondência estruturada com oportunidades publicadas, factores, lacunas de informação e regras versionadas | Sem previsão de retorno, pontuação financeira ou inferência sensível |
|
||||
| UC-IA-004 | Regras sobre campos declarados da manifestação, sem usar nome/email/telefone | Não altera lead, prioridade ou responsável oficial |
|
||||
| UC-IA-005 | Alertas sobre campos, fontes expiradas e responsáveis | Nenhuma correcção automática de dados |
|
||||
| UC-IA-006 | Prazos vencidos de tarefas CRM abertas | Sem caminho crítico ou impacto orçamental inventado |
|
||||
| UC-IA-007 | Excertos de documentos autorizados | Não constitui resumo generativo completo nem acta oficial |
|
||||
| UC-IA-004 | Regras sobre campos declarados da manifestação, sem usar nome/email/telefone | Não altera lead, prioridade ou responsável oficial |
|
||||
| UC-IA-005 | Alertas sobre campos, fontes expiradas e responsáveis | Nenhuma correcção automática de dados |
|
||||
| UC-IA-006 | Prazos vencidos de tarefas CRM abertas | Sem caminho crítico ou impacto orçamental inventado |
|
||||
| UC-IA-007 | Excertos de documentos autorizados | Não constitui resumo generativo completo nem acta oficial |
|
||||
|
||||
## 4. Segurança e governação
|
||||
|
||||
|
||||
@@ -1,21 +1,82 @@
|
||||
{
|
||||
"executed_at": "2026-09-26T15:51:08+00:00",
|
||||
"backend": "PostgreSQL FTS",
|
||||
"model": null,
|
||||
"synthetic": true,
|
||||
"passed": 8,
|
||||
"cases": 8,
|
||||
"repetitions_per_case": 5,
|
||||
"measurements": [
|
||||
{"language":"pt","query":"formação técnica","expected_match":true,"passed":true,"p50_ms":1.604,"max_ms":8.87},
|
||||
{"language":"pt","query":"cadeia logística","expected_match":true,"passed":true,"p50_ms":1.228,"max_ms":1.551},
|
||||
{"language":"pt","query":"energia solar","expected_match":true,"passed":true,"p50_ms":1.207,"max_ms":1.724},
|
||||
{"language":"pt","query":"taxa oficial","expected_match":false,"passed":true,"p50_ms":1.091,"max_ms":1.457},
|
||||
{"language":"en","query":"vocational training","expected_match":true,"passed":true,"p50_ms":1.237,"max_ms":1.518},
|
||||
{"language":"en","query":"logistics corridor","expected_match":true,"passed":true,"p50_ms":1.233,"max_ms":1.3},
|
||||
{"language":"en","query":"solar energy","expected_match":true,"passed":true,"p50_ms":1.23,"max_ms":1.426},
|
||||
{"language":"en","query":"official tax","expected_match":false,"passed":true,"p50_ms":1.167,"max_ms":1.337}
|
||||
],
|
||||
"generative_benchmark": "not_executed_no_runtime_or_model",
|
||||
"limitations": ["small_single_source_cases", "not_a_representative_benchmark", "not_human_validated_PT_MZ", "no_concurrency_or_load_test"]
|
||||
"executed_at": "2026-09-26T15:51:08+00:00",
|
||||
"backend": "PostgreSQL FTS",
|
||||
"model": null,
|
||||
"synthetic": true,
|
||||
"passed": 8,
|
||||
"cases": 8,
|
||||
"repetitions_per_case": 5,
|
||||
"measurements": [
|
||||
{
|
||||
"language": "pt",
|
||||
"query": "formação técnica",
|
||||
"expected_match": true,
|
||||
"passed": true,
|
||||
"p50_ms": 1.604,
|
||||
"max_ms": 8.87
|
||||
},
|
||||
{
|
||||
"language": "pt",
|
||||
"query": "cadeia logística",
|
||||
"expected_match": true,
|
||||
"passed": true,
|
||||
"p50_ms": 1.228,
|
||||
"max_ms": 1.551
|
||||
},
|
||||
{
|
||||
"language": "pt",
|
||||
"query": "energia solar",
|
||||
"expected_match": true,
|
||||
"passed": true,
|
||||
"p50_ms": 1.207,
|
||||
"max_ms": 1.724
|
||||
},
|
||||
{
|
||||
"language": "pt",
|
||||
"query": "taxa oficial",
|
||||
"expected_match": false,
|
||||
"passed": true,
|
||||
"p50_ms": 1.091,
|
||||
"max_ms": 1.457
|
||||
},
|
||||
{
|
||||
"language": "en",
|
||||
"query": "vocational training",
|
||||
"expected_match": true,
|
||||
"passed": true,
|
||||
"p50_ms": 1.237,
|
||||
"max_ms": 1.518
|
||||
},
|
||||
{
|
||||
"language": "en",
|
||||
"query": "logistics corridor",
|
||||
"expected_match": true,
|
||||
"passed": true,
|
||||
"p50_ms": 1.233,
|
||||
"max_ms": 1.3
|
||||
},
|
||||
{
|
||||
"language": "en",
|
||||
"query": "solar energy",
|
||||
"expected_match": true,
|
||||
"passed": true,
|
||||
"p50_ms": 1.23,
|
||||
"max_ms": 1.426
|
||||
},
|
||||
{
|
||||
"language": "en",
|
||||
"query": "official tax",
|
||||
"expected_match": false,
|
||||
"passed": true,
|
||||
"p50_ms": 1.167,
|
||||
"max_ms": 1.337
|
||||
}
|
||||
],
|
||||
"generative_benchmark": "not_executed_no_runtime_or_model",
|
||||
"limitations": [
|
||||
"small_single_source_cases",
|
||||
"not_a_representative_benchmark",
|
||||
"not_human_validated_PT_MZ",
|
||||
"no_concurrency_or_load_test"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
|
||||
## Casos do pedido
|
||||
|
||||
| Caso | Implementado nesta etapa | Pendente |
|
||||
| --- | --- | --- |
|
||||
| UC-IA-001 | Excertos citados de fontes elegíveis, data/natureza, recusa sem evidência e pedido de revisão | Respostas generativas fundamentadas, corpus oficial, assistência pública |
|
||||
| UC-IA-002 | FTS PT/EN, idioma, fonte, natureza e acesso antes da recuperação; fusão RRF unitariamente testada | Embeddings, índice pgvector, reranker e filtros territoriais/multidimensionais completos; RRF ainda não ligado a vectores reais |
|
||||
| UC-IA-003 | Filtros eliminatórios de publicação/sector/província/maturidade, factores e dados em falta; apenas oportunidades canónicas publicadas | Perfil completo, regras institucionais aprovadas, similitude semântica, clusters/corredores/instrumentos/parceiros; pontuação actual conta filtros, não é ponderação financeira |
|
||||
| UC-IA-004 | Triagem por campos declarados, consentimento específico, sem dados de contacto ou texto livre; não altera pedido/lead | Modelo supervisionado, responsável sugerido, idioma/cluster/corredor/horizonte e regras de prioridade aprovadas |
|
||||
| UC-IA-005 | Fontes expiradas, documentos com checksum repetido, ausência de responsável/próxima acção, geometrias inválidas; alertas em resultados auditados | Conflitos semânticos entre fontes, sincronização SuiteCRM e cobertura de todos os domínios |
|
||||
| UC-IA-006 | Tarefas CRM abertas e vencidas, prazo, referência e acção de revisão sugerida | Marcos/dependências, caminho crítico, impacto e orçamento autorizado |
|
||||
| UC-IA-007 | Pré-visualização extractiva dos primeiros três fragmentos de fonte elegível | Resumo generativo completo, reuniões/actas/históricos e extracção de compromissos confirmados |
|
||||
| Caso | Implementado nesta etapa | Pendente |
|
||||
| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| UC-IA-001 | Excertos citados de fontes elegíveis, data/natureza, recusa sem evidência e pedido de revisão | Respostas generativas fundamentadas, corpus oficial, assistência pública |
|
||||
| UC-IA-002 | FTS PT/EN, idioma, fonte, natureza e acesso antes da recuperação; fusão RRF unitariamente testada | Embeddings, índice pgvector, reranker e filtros territoriais/multidimensionais completos; RRF ainda não ligado a vectores reais |
|
||||
| UC-IA-003 | Filtros eliminatórios de publicação/sector/província/maturidade, factores e dados em falta; apenas oportunidades canónicas publicadas | Perfil completo, regras institucionais aprovadas, similitude semântica, clusters/corredores/instrumentos/parceiros; pontuação actual conta filtros, não é ponderação financeira |
|
||||
| UC-IA-004 | Triagem por campos declarados, consentimento específico, sem dados de contacto ou texto livre; não altera pedido/lead | Modelo supervisionado, responsável sugerido, idioma/cluster/corredor/horizonte e regras de prioridade aprovadas |
|
||||
| UC-IA-005 | Fontes expiradas, documentos com checksum repetido, ausência de responsável/próxima acção, geometrias inválidas; alertas em resultados auditados | Conflitos semânticos entre fontes, sincronização SuiteCRM e cobertura de todos os domínios |
|
||||
| UC-IA-006 | Tarefas CRM abertas e vencidas, prazo, referência e acção de revisão sugerida | Marcos/dependências, caminho crítico, impacto e orçamento autorizado |
|
||||
| UC-IA-007 | Pré-visualização extractiva dos primeiros três fragmentos de fonte elegível | Resumo generativo completo, reuniões/actas/históricos e extracção de compromissos confirmados |
|
||||
|
||||
Nenhum caso é declarado integralmente concluído. A aceitação da revisão apenas grava um parecer interno. Não altera estados oficiais, cria compromissos ou transmite dados ao SuiteCRM.
|
||||
|
||||
## Pipeline RAG de 17 passos
|
||||
|
||||
| Passos | Situação |
|
||||
| --- | --- |
|
||||
| 1–2: recepção e segurança | Cadastro canónico, upload apenas texto UTF-8, limite de tamanho, confirmação de autorização, rejeição de fontes confidenciais/restritas; formatos complexos/OCR/antivírus pendentes |
|
||||
| Passos | Situação |
|
||||
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1–2: recepção e segurança | Cadastro canónico, upload apenas texto UTF-8, limite de tamanho, confirmação de autorização, rejeição de fontes confidenciais/restritas; formatos complexos/OCR/antivírus pendentes |
|
||||
| 3–6: extracção, versão, acesso e fragmentação | Texto simples, versão/checksum do documento, metadados de fonte, revisão independente, fragmentos com ID/checksum/posição; divisão por caracteres não substitui tokenização do modelo |
|
||||
| 7–8: embeddings e pgvector | Não executados. Extensão/runtime ausentes. Esquema proposto separado, sem tocar na base partilhada |
|
||||
| 9–10: índice lexical e ACL | tsvector gerado por idioma, GIN; status, validade, classificação, versão e checksum filtrados antes da pesquisa |
|
||||
| 11–12: híbrido e reranking | Algoritmo RRF testado isoladamente; corpus vectorial/modelos não provisionados, logo produção continua lexical |
|
||||
| 13–15: contexto, geração e suporte | Excertos devolvidos como citações; sem geração nem afirmações novas. Verificação semântica de afirmações depende de modelo e avaliação |
|
||||
| 16–17: fontes e rastreabilidade | Fonte/data/natureza/validade, modo, regras, versão de instrução e latência; resultado encriptado; sem falsa confiança probabilística |
|
||||
| 7–8: embeddings e pgvector | Não executados. Extensão/runtime ausentes. Esquema proposto separado, sem tocar na base partilhada |
|
||||
| 9–10: índice lexical e ACL | tsvector gerado por idioma, GIN; status, validade, classificação, versão e checksum filtrados antes da pesquisa |
|
||||
| 11–12: híbrido e reranking | Algoritmo RRF testado isoladamente; corpus vectorial/modelos não provisionados, logo produção continua lexical |
|
||||
| 13–15: contexto, geração e suporte | Excertos devolvidos como citações; sem geração nem afirmações novas. Verificação semântica de afirmações depende de modelo e avaliação |
|
||||
| 16–17: fontes e rastreabilidade | Fonte/data/natureza/validade, modo, regras, versão de instrução e latência; resultado encriptado; sem falsa confiança probabilística |
|
||||
|
||||
## Metadados
|
||||
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
|
||||
Consulta de fontes primárias em 26/09/2026. São candidatos identificados, não alegações de versões mais recentes nem selecção final. Licença do modelo não elimina obrigações de proveniência dos dados nem valida juridicamente o uso em Moçambique.
|
||||
|
||||
| Candidato fixado para avaliação | Licença declarada | Recursos e decisão |
|
||||
| --- | --- | --- |
|
||||
| mistralai/Mistral-Small-3.2-24B-Instruct-2506 | Apache-2.0 | A ficha indica cerca de 55 GB GPU em BF16/FP16. Incompatível com a GPU virtual disponível. Quantização precisa de benchmark próprio. |
|
||||
| Qwen/Qwen3-4B-Instruct-2507 | Apache-2.0 | Estimativa aritmética de pesos: cerca de 8 GB a 16 bits ou 2 GB a 4 bits, antes de KV cache, runtime e metadados. Candidato menor para benchmark, não aprovado. |
|
||||
| BAAI/bge-m3 | MIT | Embeddings de 1024 dimensões, contexto máximo indicado de 8192 tokens; memória e latência locais não medidas. |
|
||||
| BAAI/bge-reranker-v2-m3 | Apache-2.0 | Cross-encoder multilingue; custo cresce com pares pergunta/fragmento. Medir antes de habilitar. |
|
||||
| pgvector 0.8.6 | PostgreSQL License | Código open source; extensão ausente no PostgreSQL local. Usar a mesma base, não uma base paralela. |
|
||||
| vLLM | Verificar licença/release do artefacto de implantação | Ambiente de inferência controlado por provisionar; não instalado nem validado nesta máquina. |
|
||||
| Ollama | Apenas protótipo local, conforme pedido | Não instalado. Não tratar o Windows virtual como equivalente ao hardware macOS anfitrião. |
|
||||
| Candidato fixado para avaliação | Licença declarada | Recursos e decisão |
|
||||
| --------------------------------------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| mistralai/Mistral-Small-3.2-24B-Instruct-2506 | Apache-2.0 | A ficha indica cerca de 55 GB GPU em BF16/FP16. Incompatível com a GPU virtual disponível. Quantização precisa de benchmark próprio. |
|
||||
| Qwen/Qwen3-4B-Instruct-2507 | Apache-2.0 | Estimativa aritmética de pesos: cerca de 8 GB a 16 bits ou 2 GB a 4 bits, antes de KV cache, runtime e metadados. Candidato menor para benchmark, não aprovado. |
|
||||
| BAAI/bge-m3 | MIT | Embeddings de 1024 dimensões, contexto máximo indicado de 8192 tokens; memória e latência locais não medidas. |
|
||||
| BAAI/bge-reranker-v2-m3 | Apache-2.0 | Cross-encoder multilingue; custo cresce com pares pergunta/fragmento. Medir antes de habilitar. |
|
||||
| pgvector 0.8.6 | PostgreSQL License | Código open source; extensão ausente no PostgreSQL local. Usar a mesma base, não uma base paralela. |
|
||||
| vLLM | Verificar licença/release do artefacto de implantação | Ambiente de inferência controlado por provisionar; não instalado nem validado nesta máquina. |
|
||||
| Ollama | Apenas protótipo local, conforme pedido | Não instalado. Não tratar o Windows virtual como equivalente ao hardware macOS anfitrião. |
|
||||
|
||||
Estimativa de armazenamento vectorial bruto BGE-M3: 1024 × 4 bytes = 4096 bytes por fragmento; um milhão de fragmentos representa aproximadamente 4,1 GB decimais, sem índices, texto, WAL, backups e overhead. Planear esses adicionais por medição.
|
||||
|
||||
|
||||
@@ -66,13 +66,13 @@ QA de aceitação: pesquisa por um projecto documental publicado deve devolvê-l
|
||||
|
||||
## Avaliação transversal
|
||||
|
||||
| Área | Evidência e conclusão |
|
||||
| --- | --- |
|
||||
| Arquitectura | Laravel centraliza autorização e persistência; serviços de IA separados de controllers. R1 e R4 mostram necessidade de versões explícitas entre edição, cadastro e publicação. |
|
||||
| Dados | Importação transaccional/idempotente, códigos documentais e relações preservados. Valores indicativos continuam em texto. R3 impede reconstituição completa do histórico editorial. |
|
||||
| Segurança | Rotas de gestão exigem autenticação, email verificado e `manage-portal`; resultados IA encriptados e fontes documentais revalidadas. R2 deixa uma lacuna equivalente nas dependências estruturadas. |
|
||||
| QA | Execução anterior registada: 119 testes / 1.107 verificações, PHPStan, Pint, TypeScript e build aprovados. Não repetidos nesta revisão. Os cenários de concorrência e reclassificação acima precisam de cobertura; testes verdes não os demonstram. |
|
||||
| Produto | Consulta territorial, relações, painel e manifestação de interesse estão disponíveis. A assistência continua lexical/determinística e a integração externa SuiteCRM não foi executada. R5 limita a descoberta da carteira via IA. |
|
||||
| Área | Evidência e conclusão |
|
||||
| ------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Arquitectura | Laravel centraliza autorização e persistência; serviços de IA separados de controllers. R1 e R4 mostram necessidade de versões explícitas entre edição, cadastro e publicação. |
|
||||
| Dados | Importação transaccional/idempotente, códigos documentais e relações preservados. Valores indicativos continuam em texto. R3 impede reconstituição completa do histórico editorial. |
|
||||
| Segurança | Rotas de gestão exigem autenticação, email verificado e `manage-portal`; resultados IA encriptados e fontes documentais revalidadas. R2 deixa uma lacuna equivalente nas dependências estruturadas. |
|
||||
| QA | Execução anterior registada: 119 testes / 1.107 verificações, PHPStan, Pint, TypeScript e build aprovados. Não repetidos nesta revisão. Os cenários de concorrência e reclassificação acima precisam de cobertura; testes verdes não os demonstram. |
|
||||
| Produto | Consulta territorial, relações, painel e manifestação de interesse estão disponíveis. A assistência continua lexical/determinística e a integração externa SuiteCRM não foi executada. R5 limita a descoberta da carteira via IA. |
|
||||
|
||||
## Ordem de tratamento
|
||||
|
||||
|
||||
@@ -4,20 +4,20 @@ O SQL recebido foi convertido numa migração incremental para PostgreSQL/PostGI
|
||||
|
||||
## Correspondências
|
||||
|
||||
| SQL recebido | Modelo actual |
|
||||
| --- | --- |
|
||||
| `business.parties` | Contas autenticadas em `users`; proponentes institucionais em `organizations`; perfis de investimento em `investors` e `investor_accounts`. Não é criada uma segunda tabela de identidade. |
|
||||
| `business.opportunities` | `existing_businesses`: `tracking_code` usa `code`; proponente/criador usa `owner_user_id`; proprietário institucional usa `organization_id`; sector usa `sector_id`; versão usa `version_no`. |
|
||||
| `business.opportunity_locations` | Nova tabela de relações `existing_business_locations`, ligada a `geo_locations` por UUID e com validação da hierarquia e geometria PostGIS. |
|
||||
| `business.assets` | Campos tipados adicionais em `existing_business_objects`; o `payload` cifrado actual continua a guardar pormenores privados. |
|
||||
| `business.rights` | Nova tabela `existing_business_rights`, ligada ao negócio e, quando aplicável, ao objecto. O detalhe reservado fica em `confidential_payload`, que os serviços devem cifrar. |
|
||||
| `business.documents` | `documents` actual (`file_name`, `media_type`, `storage_key`, `sha256`, `status` e `data_classification`), com metadados adicionais; nova tabela de ligação ao negócio ou ao pedido de interesse. |
|
||||
| `business.investor_profiles` | Campos de perfil em `investors`, cuja ligação à conta autenticada já existe em `investor_accounts`. |
|
||||
| `business.interests` | `interest_requests`; acrescenta alvo `existing_business_id`, perfil do investidor, modalidade, montantes e atribuição. `opportunity_id` continua a referir-se ao catálogo público de investimentos, e uma submissão só pode indicar um desses dois alvos. |
|
||||
| `business.consents` | `consents` actual, com alvo opcional e versão da política; os estados existentes `granted`/`withdrawn` mantêm-se como fonte canónica. |
|
||||
| `business.workflow_transitions` | Histórico actual de revisões/reviews de negócios mais `business_workflow_transitions` para transições de negócios e pedidos de interesse. |
|
||||
| `business.tasks` / `business.outbox_messages` | `tasks` e `outbox_events` existentes recebem campos adicionais; não são criadas filas paralelas. |
|
||||
| `business.crm_links` | Nova tabela `external_record_links` para mapear identificadores remotos sem duplicar os registos de CRM. |
|
||||
| SQL recebido | Modelo actual |
|
||||
| --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `business.parties` | Contas autenticadas em `users`; proponentes institucionais em `organizations`; perfis de investimento em `investors` e `investor_accounts`. Não é criada uma segunda tabela de identidade. |
|
||||
| `business.opportunities` | `existing_businesses`: `tracking_code` usa `code`; proponente/criador usa `owner_user_id`; proprietário institucional usa `organization_id`; sector usa `sector_id`; versão usa `version_no`. |
|
||||
| `business.opportunity_locations` | Nova tabela de relações `existing_business_locations`, ligada a `geo_locations` por UUID e com validação da hierarquia e geometria PostGIS. |
|
||||
| `business.assets` | Campos tipados adicionais em `existing_business_objects`; o `payload` cifrado actual continua a guardar pormenores privados. |
|
||||
| `business.rights` | Nova tabela `existing_business_rights`, ligada ao negócio e, quando aplicável, ao objecto. O detalhe reservado fica em `confidential_payload`, que os serviços devem cifrar. |
|
||||
| `business.documents` | `documents` actual (`file_name`, `media_type`, `storage_key`, `sha256`, `status` e `data_classification`), com metadados adicionais; nova tabela de ligação ao negócio ou ao pedido de interesse. |
|
||||
| `business.investor_profiles` | Campos de perfil em `investors`, cuja ligação à conta autenticada já existe em `investor_accounts`. |
|
||||
| `business.interests` | `interest_requests`; acrescenta alvo `existing_business_id`, perfil do investidor, modalidade, montantes e atribuição. `opportunity_id` continua a referir-se ao catálogo público de investimentos, e uma submissão só pode indicar um desses dois alvos. |
|
||||
| `business.consents` | `consents` actual, com alvo opcional e versão da política; os estados existentes `granted`/`withdrawn` mantêm-se como fonte canónica. |
|
||||
| `business.workflow_transitions` | Histórico actual de revisões/reviews de negócios mais `business_workflow_transitions` para transições de negócios e pedidos de interesse. |
|
||||
| `business.tasks` / `business.outbox_messages` | `tasks` e `outbox_events` existentes recebem campos adicionais; não são criadas filas paralelas. |
|
||||
| `business.crm_links` | Nova tabela `external_record_links` para mapear identificadores remotos sem duplicar os registos de CRM. |
|
||||
|
||||
Campos confidenciais já protegidos no `payload` cifrado não são copiados para colunas em texto aberto. O campo `public_title` fica separado para publicação futura; esta migração não publica negócios nem muda a máquina de estados da aplicação.
|
||||
|
||||
@@ -37,12 +37,12 @@ Esta entrega altera o esquema e a serialização do modelo. Os endpoints/formul
|
||||
|
||||
A implementação da proposta reutiliza os serviços e modelos que já existem, sem introduzir `Opportunity`, `Interest`, `Party` ou `OutboxMessage` paralelos:
|
||||
|
||||
| Responsabilidade proposta | Implementação no portal |
|
||||
| --- | --- |
|
||||
| Criar e actualizar rascunhos | `ExistingBusinessDrafts`, usado pela API autenticada `api/v1/existing-business/listings`. |
|
||||
| Submeter e rever anúncios | `ExistingBusinessIntake`, com autorização via `ExistingBusinessPolicy`, controlo de versão e histórico cifrado. |
|
||||
| Pedidos de interesse idempotentes | `InterestRequestController` e `InterestSubmission`; eventos são gravados na outbox existente. |
|
||||
| Integração CRM | Cliente seguro `SuiteCrmClient`; a outbox existente usa `aggregate_type`, `aggregate_id`, `event_type`, `payload` e estados `pending/published/failed`. |
|
||||
| Estados tipados | Enums `OpportunityStatus` e `InterestStatus` em `app/Domain`, mantendo os valores de estado já persistidos. |
|
||||
| Responsabilidade proposta | Implementação no portal |
|
||||
| --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Criar e actualizar rascunhos | `ExistingBusinessDrafts`, usado pela API autenticada `api/v1/existing-business/listings`. |
|
||||
| Submeter e rever anúncios | `ExistingBusinessIntake`, com autorização via `ExistingBusinessPolicy`, controlo de versão e histórico cifrado. |
|
||||
| Pedidos de interesse idempotentes | `InterestRequestController` e `InterestSubmission`; eventos são gravados na outbox existente. |
|
||||
| Integração CRM | Cliente seguro `SuiteCrmClient`; a outbox existente usa `aggregate_type`, `aggregate_id`, `event_type`, `payload` e estados `pending/published/failed`. |
|
||||
| Estados tipados | Enums `OpportunityStatus` e `InterestStatus` em `app/Domain`, mantendo os valores de estado já persistidos. |
|
||||
|
||||
Os estados adicionais do enum `OpportunityStatus` alimentam agora as etapas de triagem, revisão, duas aprovações e publicação de `ExistingBusinessIntake`. A publicação usa uma projecção pública separada e allowlist de campos. As manifestações de interesse têm uma máquina de estados própria em `InterestWorkflow`; transições, notas cifradas e prazos são guardados no histórico comum. Isto ainda não constitui uma integração CRM em background, SLA automatizado, upload/scanner de documentos ou autorização institucional para publicar dados reais.
|
||||
|
||||
@@ -14,50 +14,50 @@ Data: 04/10/2026. Revisão feita por leitura da base activa e do código de impo
|
||||
|
||||
Todas as linhas abaixo têm `source_system=upgrade-v4`, `validation_status=pending`, `association_type=potential`, `source_reference=NULL`, `evidence_reference=NULL`, `valid_from=NULL`, `valid_until=NULL`, `editor_id=NULL` e `validator_id=NULL`. A correspondência está registada na matriz de importação da secção 4.3.4 do documento de programação indicado pelo seeder; não há citação individual de página/documento oficial ou evidência que permita elevar o tipo para `official`.
|
||||
|
||||
| Corredor | Cluster | Resultado da revisão |
|
||||
|---|---|---|
|
||||
| COR-02 Corredor da Beira e Centro | CLU-01 Agro-indústria e segurança alimentar | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-02 Corredor da Beira e Centro | CLU-02 Energia e industrialização | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-02 Corredor da Beira e Centro | CLU-03 Logística, corredores e exportação | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-02 Corredor da Beira e Centro | CLU-05 Têxtil, vestuário e algodão | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-01 Corredor de Nacala e Norte | CLU-01 Agro-indústria e segurança alimentar | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-01 Corredor de Nacala e Norte | CLU-02 Energia e industrialização | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-01 Corredor de Nacala e Norte | CLU-03 Logística, corredores e exportação | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-01 Corredor de Nacala e Norte | CLU-06 Mineração e transformação mineral | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-01 Corredor de Nacala e Norte | CLU-04 Turismo costeiro e economia azul | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-06 Corredor Industrial Nacional Norte–Sul | CLU-02 Energia e industrialização | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-06 Corredor Industrial Nacional Norte–Sul | CLU-03 Logística, corredores e exportação | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-06 Corredor Industrial Nacional Norte–Sul | CLU-07 Tecnologia e serviços digitais | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-04 Corredor Maputo–Limpopo | CLU-01 Agro-indústria e segurança alimentar | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-04 Corredor Maputo–Limpopo | CLU-03 Logística, corredores e exportação | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-05 Corredor Turístico Inhambane–Bazaruto | CLU-07 Tecnologia e serviços digitais | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-05 Corredor Turístico Inhambane–Bazaruto | CLU-04 Turismo costeiro e economia azul | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-03 Corredor Zambeze–Macuse | CLU-01 Agro-indústria e segurança alimentar | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-03 Corredor Zambeze–Macuse | CLU-02 Energia e industrialização | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-03 Corredor Zambeze–Macuse | CLU-03 Logística, corredores e exportação | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-03 Corredor Zambeze–Macuse | CLU-06 Mineração e transformação mineral | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| Corredor | Cluster | Resultado da revisão |
|
||||
| --------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| COR-02 Corredor da Beira e Centro | CLU-01 Agro-indústria e segurança alimentar | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-02 Corredor da Beira e Centro | CLU-02 Energia e industrialização | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-02 Corredor da Beira e Centro | CLU-03 Logística, corredores e exportação | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-02 Corredor da Beira e Centro | CLU-05 Têxtil, vestuário e algodão | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-01 Corredor de Nacala e Norte | CLU-01 Agro-indústria e segurança alimentar | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-01 Corredor de Nacala e Norte | CLU-02 Energia e industrialização | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-01 Corredor de Nacala e Norte | CLU-03 Logística, corredores e exportação | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-01 Corredor de Nacala e Norte | CLU-06 Mineração e transformação mineral | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-01 Corredor de Nacala e Norte | CLU-04 Turismo costeiro e economia azul | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-06 Corredor Industrial Nacional Norte–Sul | CLU-02 Energia e industrialização | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-06 Corredor Industrial Nacional Norte–Sul | CLU-03 Logística, corredores e exportação | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-06 Corredor Industrial Nacional Norte–Sul | CLU-07 Tecnologia e serviços digitais | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-04 Corredor Maputo–Limpopo | CLU-01 Agro-indústria e segurança alimentar | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-04 Corredor Maputo–Limpopo | CLU-03 Logística, corredores e exportação | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-05 Corredor Turístico Inhambane–Bazaruto | CLU-07 Tecnologia e serviços digitais | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-05 Corredor Turístico Inhambane–Bazaruto | CLU-04 Turismo costeiro e economia azul | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-03 Corredor Zambeze–Macuse | CLU-01 Agro-indústria e segurança alimentar | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-03 Corredor Zambeze–Macuse | CLU-02 Energia e industrialização | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-03 Corredor Zambeze–Macuse | CLU-03 Logística, corredores e exportação | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
| COR-03 Corredor Zambeze–Macuse | CLU-06 Mineração e transformação mineral | Mapeamento de planeamento registado; relação factual por confirmar. |
|
||||
|
||||
Os outros 30 vínculos (`cluster_projects`: 13; `corridor_projects`: 17) têm os mesmos campos de validação e prova vazios. A sua lista exacta é reproduzível em `InvestmentPortfolioSeeder`: as âncoras são extraídas dos campos `anchors` do JSON de importação e das matrizes de COR-01 a COR-06. Não foi localizada evidência individual; nenhum deles foi validado.
|
||||
|
||||
## Projectos âncora — estado documental observado
|
||||
|
||||
| Código | Projecto | Estado de execução | Orçamento/calendário | Fonte e decisão |
|
||||
|---|---|---|---|---|
|
||||
| A01 | MozParks – 20 Parques Industriais | draft | vazio | Fonte genérica do documento de programação, secções 6.1/6.6; situação operacional não confirmada. |
|
||||
| A02 | Linha Ferroviária Norte–Sul | draft | vazio | Idem; fonte primária individual por confirmar. |
|
||||
| A03 | Programa de Desenvolvimento Integrado do Corredor de Nacala | draft | vazio | Idem; financiamento, calendário e execução por confirmar. |
|
||||
| A04 | Crusse Jamali – Zona Económica Especial de Turismo | draft | vazio | Idem; estatuto da zona e disponibilidade por confirmar. |
|
||||
| A05 | Linha de Transmissão Namialo–Metoro (400 kV) | draft | vazio | Idem; fonte e calendário por confirmar. |
|
||||
| A06 | Cadeia de Valor do Arroz por Projecto de Processamento | draft | vazio | Idem; promotor e projecto executável por confirmar. |
|
||||
| A07 | Parque Têxtil Integrado | draft | vazio | Idem; estatuto, infra-estruturas e disponibilidade por confirmar. |
|
||||
| A08 | Zona Económica Especial de Mocuba | draft | vazio | Idem; diploma, operador e condições por confirmar. |
|
||||
| A09 | Construção da Cidadela Parlamentar de Mocuba | draft | vazio | Idem; entidade, projecto e calendário por confirmar. |
|
||||
| A10 | Mphanda Nkuwa HPP – Hidroeléctrica | draft | vazio | Idem; fase, fonte individual e calendário por confirmar. |
|
||||
| A11 | Corredor Ferroviário e Portuário de Macuse–Moatize | draft | vazio | Idem; fase, financiamento e calendário por confirmar. |
|
||||
| A12 | Programa de Conectividade e Resiliência Costeira (Compacto II) | draft | vazio | Idem; designação/fase e fonte primária por confirmar. |
|
||||
| A13 | Vilanculos – Região Integrada de Turismo | draft | vazio | Idem; promotor, estatuto e disponibilidade por confirmar. |
|
||||
| A14 | Zona Económica Especial de Agronegócio do Limpopo | draft | vazio | Idem; diploma, operador e condições por confirmar. |
|
||||
| A15 | Regadio na Bacia Hidrográfica do Rio Maputo | draft | vazio | Idem; entidade, calendário e disponibilidade por confirmar. |
|
||||
| Código | Projecto | Estado de execução | Orçamento/calendário | Fonte e decisão |
|
||||
| ------ | -------------------------------------------------------------- | ------------------ | -------------------- | ------------------------------------------------------------------------------------------------- |
|
||||
| A01 | MozParks – 20 Parques Industriais | draft | vazio | Fonte genérica do documento de programação, secções 6.1/6.6; situação operacional não confirmada. |
|
||||
| A02 | Linha Ferroviária Norte–Sul | draft | vazio | Idem; fonte primária individual por confirmar. |
|
||||
| A03 | Programa de Desenvolvimento Integrado do Corredor de Nacala | draft | vazio | Idem; financiamento, calendário e execução por confirmar. |
|
||||
| A04 | Crusse Jamali – Zona Económica Especial de Turismo | draft | vazio | Idem; estatuto da zona e disponibilidade por confirmar. |
|
||||
| A05 | Linha de Transmissão Namialo–Metoro (400 kV) | draft | vazio | Idem; fonte e calendário por confirmar. |
|
||||
| A06 | Cadeia de Valor do Arroz por Projecto de Processamento | draft | vazio | Idem; promotor e projecto executável por confirmar. |
|
||||
| A07 | Parque Têxtil Integrado | draft | vazio | Idem; estatuto, infra-estruturas e disponibilidade por confirmar. |
|
||||
| A08 | Zona Económica Especial de Mocuba | draft | vazio | Idem; diploma, operador e condições por confirmar. |
|
||||
| A09 | Construção da Cidadela Parlamentar de Mocuba | draft | vazio | Idem; entidade, projecto e calendário por confirmar. |
|
||||
| A10 | Mphanda Nkuwa HPP – Hidroeléctrica | draft | vazio | Idem; fase, fonte individual e calendário por confirmar. |
|
||||
| A11 | Corredor Ferroviário e Portuário de Macuse–Moatize | draft | vazio | Idem; fase, financiamento e calendário por confirmar. |
|
||||
| A12 | Programa de Conectividade e Resiliência Costeira (Compacto II) | draft | vazio | Idem; designação/fase e fonte primária por confirmar. |
|
||||
| A13 | Vilanculos – Região Integrada de Turismo | draft | vazio | Idem; promotor, estatuto e disponibilidade por confirmar. |
|
||||
| A14 | Zona Económica Especial de Agronegócio do Limpopo | draft | vazio | Idem; diploma, operador e condições por confirmar. |
|
||||
| A15 | Regadio na Bacia Hidrográfica do Rio Maputo | draft | vazio | Idem; entidade, calendário e disponibilidade por confirmar. |
|
||||
|
||||
## Ensaios e evidências
|
||||
|
||||
@@ -72,4 +72,3 @@ Os outros 30 vínculos (`cluster_projects`: 13; `corridor_projects`: 17) têm os
|
||||
2. Fonte primária e estado actual para cada um dos 15 projectos âncora; decisão humana de publicação/actualização após confronto documental. Nenhum estado de concurso é inferido.
|
||||
3. Para zonas, fornecer fonte legal validada e registo real autorizado para ensaio visual de ponta a ponta. O fluxo sintético prova implementação técnica, não conteúdo real.
|
||||
4. Para investimentos, substituir ou validar os seis registos de seed; fornecer ao menos uma oportunidade e um anúncio de negócio autorizados, com fonte e data, antes de ensaiar uma pretensão real. O fluxo de submissão foi coberto por fixtures na suite, sem criar dados reais.
|
||||
|
||||
|
||||
@@ -1,128 +1,256 @@
|
||||
{
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "InvestMoz reference catalogues",
|
||||
"version": "1.0.0",
|
||||
"description": "Read-only, rate-limited reference options with stable codes, version metadata and pt-MZ labels. Jurisdictions use the packaged territorial reference set. Economic activities are portal-curated sector groupings, not an official statutory classification. Authority entries are withheld until an authoritative source is approved."
|
||||
},
|
||||
"servers": [{ "url": "/api/v1/catalogs" }],
|
||||
"security": [],
|
||||
"paths": {
|
||||
"/authorities": { "$ref": "#/components/pathItems/Authorities" },
|
||||
"/jurisdictions": { "$ref": "#/components/pathItems/Jurisdictions" },
|
||||
"/economic-activities": { "$ref": "#/components/pathItems/EconomicActivities" }
|
||||
},
|
||||
"components": {
|
||||
"pathItems": {
|
||||
"Authorities": {
|
||||
"get": {
|
||||
"summary": "Search the published authorities catalogue",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/q" }, { "$ref": "#/components/parameters/version" },
|
||||
{ "$ref": "#/components/parameters/page" }, { "$ref": "#/components/parameters/per_page" },
|
||||
{ "name": "jurisdiction_id", "in": "query", "schema": { "type": "string", "format": "uuid" } }
|
||||
],
|
||||
"responses": { "200": { "$ref": "#/components/responses/CatalogPage" }, "429": { "description": "Rate limit exceeded" } }
|
||||
}
|
||||
},
|
||||
"Jurisdictions": {
|
||||
"get": {
|
||||
"summary": "List territorial reference options for dependent dropdowns",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/q" }, { "$ref": "#/components/parameters/version" },
|
||||
{ "$ref": "#/components/parameters/page" }, { "$ref": "#/components/parameters/per_page" },
|
||||
{ "name": "type", "in": "query", "required": true, "schema": { "type": "string", "enum": ["country", "province", "district", "admin_post", "locality"] } },
|
||||
{ "name": "parent_id", "in": "query", "schema": { "type": "string", "format": "uuid" }, "description": "Required for district, admin_post and locality; the parent must be of the immediately preceding level." }
|
||||
],
|
||||
"responses": { "200": { "$ref": "#/components/responses/CatalogPage" }, "422": { "description": "Invalid or missing parent selection" }, "429": { "description": "Rate limit exceeded" } }
|
||||
}
|
||||
},
|
||||
"EconomicActivities": {
|
||||
"get": {
|
||||
"summary": "Search the portal-curated economic activity groupings",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/q" }, { "$ref": "#/components/parameters/version" },
|
||||
{ "$ref": "#/components/parameters/page" }, { "$ref": "#/components/parameters/per_page" },
|
||||
{ "name": "parent_id", "in": "query", "schema": { "type": "string", "format": "uuid" }, "description": "Optional parent activity for hierarchical catalog browsing." },
|
||||
{ "name": "sector_id", "in": "query", "schema": { "type": "integer", "minimum": 1 }, "description": "Optional sector filter for the dependent activity dropdown." }
|
||||
],
|
||||
"responses": { "200": { "$ref": "#/components/responses/CatalogPage" }, "429": { "description": "Rate limit exceeded" } }
|
||||
}
|
||||
}
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "InvestMoz reference catalogues",
|
||||
"version": "1.0.0",
|
||||
"description": "Read-only, rate-limited reference options with stable codes, version metadata and pt-MZ labels. Jurisdictions use the packaged territorial reference set. Economic activities are portal-curated sector groupings, not an official statutory classification. Authority entries are withheld until an authoritative source is approved."
|
||||
},
|
||||
"parameters": {
|
||||
"q": { "name": "q", "in": "query", "schema": { "type": "string", "maxLength": 100 } },
|
||||
"version": { "name": "version", "in": "query", "schema": { "type": "integer", "minimum": 1 }, "description": "A published immutable version; omitted selects the current effective version." },
|
||||
"page": { "name": "page", "in": "query", "schema": { "type": "integer", "minimum": 1 } },
|
||||
"per_page": { "name": "per_page", "in": "query", "schema": { "type": "integer", "minimum": 1, "maximum": 100 } }
|
||||
"servers": [{ "url": "/api/v1/catalogs" }],
|
||||
"security": [],
|
||||
"paths": {
|
||||
"/authorities": { "$ref": "#/components/pathItems/Authorities" },
|
||||
"/jurisdictions": { "$ref": "#/components/pathItems/Jurisdictions" },
|
||||
"/economic-activities": {
|
||||
"$ref": "#/components/pathItems/EconomicActivities"
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"CatalogPage": {
|
||||
"description": "Stable option values with Portuguese (Mozambique) labels. A not-yet-published catalogue returns an empty page and status not_published.",
|
||||
"headers": {
|
||||
"Cache-Control": { "schema": { "type": "string" } },
|
||||
"ETag": { "schema": { "type": "string" } }
|
||||
},
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["data", "links", "meta", "next_page_url", "prev_page_url", "current_page", "per_page", "total", "last_page"],
|
||||
"properties": {
|
||||
"data": { "type": "array", "items": { "$ref": "#/components/schemas/CatalogEntry" } },
|
||||
"links": { "type": "object", "properties": { "next": { "type": ["string", "null"] }, "previous": { "type": ["string", "null"] } } },
|
||||
"next_page_url": { "type": ["string", "null"] },
|
||||
"prev_page_url": { "type": ["string", "null"] },
|
||||
"current_page": { "type": "integer" },
|
||||
"per_page": { "type": "integer" },
|
||||
"total": { "type": "integer" },
|
||||
"last_page": { "type": "integer" },
|
||||
"meta": { "$ref": "#/components/schemas/CatalogMetadata" }
|
||||
}
|
||||
"components": {
|
||||
"pathItems": {
|
||||
"Authorities": {
|
||||
"get": {
|
||||
"summary": "Search the published authorities catalogue",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/q" },
|
||||
{ "$ref": "#/components/parameters/version" },
|
||||
{ "$ref": "#/components/parameters/page" },
|
||||
{ "$ref": "#/components/parameters/per_page" },
|
||||
{
|
||||
"name": "jurisdiction_id",
|
||||
"in": "query",
|
||||
"schema": { "type": "string", "format": "uuid" }
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": { "$ref": "#/components/responses/CatalogPage" },
|
||||
"429": { "description": "Rate limit exceeded" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"Jurisdictions": {
|
||||
"get": {
|
||||
"summary": "List territorial reference options for dependent dropdowns",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/q" },
|
||||
{ "$ref": "#/components/parameters/version" },
|
||||
{ "$ref": "#/components/parameters/page" },
|
||||
{ "$ref": "#/components/parameters/per_page" },
|
||||
{
|
||||
"name": "type",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"country",
|
||||
"province",
|
||||
"district",
|
||||
"admin_post",
|
||||
"locality"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "parent_id",
|
||||
"in": "query",
|
||||
"schema": { "type": "string", "format": "uuid" },
|
||||
"description": "Required for district, admin_post and locality; the parent must be of the immediately preceding level."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": { "$ref": "#/components/responses/CatalogPage" },
|
||||
"422": {
|
||||
"description": "Invalid or missing parent selection"
|
||||
},
|
||||
"429": { "description": "Rate limit exceeded" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"EconomicActivities": {
|
||||
"get": {
|
||||
"summary": "Search the portal-curated economic activity groupings",
|
||||
"parameters": [
|
||||
{ "$ref": "#/components/parameters/q" },
|
||||
{ "$ref": "#/components/parameters/version" },
|
||||
{ "$ref": "#/components/parameters/page" },
|
||||
{ "$ref": "#/components/parameters/per_page" },
|
||||
{
|
||||
"name": "parent_id",
|
||||
"in": "query",
|
||||
"schema": { "type": "string", "format": "uuid" },
|
||||
"description": "Optional parent activity for hierarchical catalog browsing."
|
||||
},
|
||||
{
|
||||
"name": "sector_id",
|
||||
"in": "query",
|
||||
"schema": { "type": "integer", "minimum": 1 },
|
||||
"description": "Optional sector filter for the dependent activity dropdown."
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": { "$ref": "#/components/responses/CatalogPage" },
|
||||
"429": { "description": "Rate limit exceeded" }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"q": {
|
||||
"name": "q",
|
||||
"in": "query",
|
||||
"schema": { "type": "string", "maxLength": 100 }
|
||||
},
|
||||
"version": {
|
||||
"name": "version",
|
||||
"in": "query",
|
||||
"schema": { "type": "integer", "minimum": 1 },
|
||||
"description": "A published immutable version; omitted selects the current effective version."
|
||||
},
|
||||
"page": {
|
||||
"name": "page",
|
||||
"in": "query",
|
||||
"schema": { "type": "integer", "minimum": 1 }
|
||||
},
|
||||
"per_page": {
|
||||
"name": "per_page",
|
||||
"in": "query",
|
||||
"schema": { "type": "integer", "minimum": 1, "maximum": 100 }
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"CatalogPage": {
|
||||
"description": "Stable option values with Portuguese (Mozambique) labels. A not-yet-published catalogue returns an empty page and status not_published.",
|
||||
"headers": {
|
||||
"Cache-Control": { "schema": { "type": "string" } },
|
||||
"ETag": { "schema": { "type": "string" } }
|
||||
},
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"data",
|
||||
"links",
|
||||
"meta",
|
||||
"next_page_url",
|
||||
"prev_page_url",
|
||||
"current_page",
|
||||
"per_page",
|
||||
"total",
|
||||
"last_page"
|
||||
],
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/CatalogEntry"
|
||||
}
|
||||
},
|
||||
"links": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"next": { "type": ["string", "null"] },
|
||||
"previous": {
|
||||
"type": ["string", "null"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"next_page_url": { "type": ["string", "null"] },
|
||||
"prev_page_url": { "type": ["string", "null"] },
|
||||
"current_page": { "type": "integer" },
|
||||
"per_page": { "type": "integer" },
|
||||
"total": { "type": "integer" },
|
||||
"last_page": { "type": "integer" },
|
||||
"meta": {
|
||||
"$ref": "#/components/schemas/CatalogMetadata"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"CatalogEntry": {
|
||||
"type": "object",
|
||||
"required": ["id", "value", "code", "name", "label", "locale"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "format": "uuid" },
|
||||
"value": {
|
||||
"type": "string",
|
||||
"description": "Stable API value; never the translated label."
|
||||
},
|
||||
"code": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "Presentation label in pt-MZ."
|
||||
},
|
||||
"locale": { "const": "pt-MZ" },
|
||||
"parent_id": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uuid"
|
||||
},
|
||||
"type": { "type": "string" },
|
||||
"version_no": { "type": "integer", "minimum": 1 },
|
||||
"sector_id": { "type": "integer" },
|
||||
"source_code": { "type": ["string", "null"] },
|
||||
"authority_type": { "type": "string" },
|
||||
"organization_id": { "type": "string", "format": "uuid" },
|
||||
"jurisdiction_id": {
|
||||
"type": ["string", "null"],
|
||||
"format": "uuid"
|
||||
},
|
||||
"jurisdiction_name": { "type": ["string", "null"] }
|
||||
}
|
||||
},
|
||||
"CatalogMetadata": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"catalog",
|
||||
"version",
|
||||
"locale",
|
||||
"current_page",
|
||||
"per_page",
|
||||
"total",
|
||||
"last_page"
|
||||
],
|
||||
"properties": {
|
||||
"catalog": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"authorities",
|
||||
"jurisdictions",
|
||||
"economic-activities"
|
||||
]
|
||||
},
|
||||
"version": { "type": ["integer", "null"] },
|
||||
"locale": { "const": "pt-MZ" },
|
||||
"status": { "type": "string", "enum": ["not_published"] },
|
||||
"source_version": { "type": ["string", "null"] },
|
||||
"valid_from": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date"
|
||||
},
|
||||
"valid_to": {
|
||||
"type": ["string", "null"],
|
||||
"format": "date"
|
||||
},
|
||||
"current_page": { "type": "integer" },
|
||||
"per_page": { "type": "integer" },
|
||||
"total": { "type": "integer" },
|
||||
"last_page": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"schemas": {
|
||||
"CatalogEntry": {
|
||||
"type": "object",
|
||||
"required": ["id", "value", "code", "name", "label", "locale"],
|
||||
"properties": {
|
||||
"id": { "type": "string", "format": "uuid" },
|
||||
"value": { "type": "string", "description": "Stable API value; never the translated label." },
|
||||
"code": { "type": "string" },
|
||||
"name": { "type": "string" },
|
||||
"label": { "type": "string", "description": "Presentation label in pt-MZ." },
|
||||
"locale": { "const": "pt-MZ" },
|
||||
"parent_id": { "type": ["string", "null"], "format": "uuid" },
|
||||
"type": { "type": "string" },
|
||||
"version_no": { "type": "integer", "minimum": 1 },
|
||||
"sector_id": { "type": "integer" },
|
||||
"source_code": { "type": ["string", "null"] },
|
||||
"authority_type": { "type": "string" },
|
||||
"organization_id": { "type": "string", "format": "uuid" },
|
||||
"jurisdiction_id": { "type": ["string", "null"], "format": "uuid" },
|
||||
"jurisdiction_name": { "type": ["string", "null"] }
|
||||
}
|
||||
},
|
||||
"CatalogMetadata": {
|
||||
"type": "object",
|
||||
"required": ["catalog", "version", "locale", "current_page", "per_page", "total", "last_page"],
|
||||
"properties": {
|
||||
"catalog": { "type": "string", "enum": ["authorities", "jurisdictions", "economic-activities"] },
|
||||
"version": { "type": ["integer", "null"] },
|
||||
"locale": { "const": "pt-MZ" },
|
||||
"status": { "type": "string", "enum": ["not_published"] },
|
||||
"source_version": { "type": ["string", "null"] },
|
||||
"valid_from": { "type": ["string", "null"], "format": "date" },
|
||||
"valid_to": { "type": ["string", "null"], "format": "date" },
|
||||
"current_page": { "type": "integer" },
|
||||
"per_page": { "type": "integer" },
|
||||
"total": { "type": "integer" },
|
||||
"last_page": { "type": "integer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,24 +8,24 @@ Este relatório é um índice de estado actual; prevalece sobre afirmações his
|
||||
|
||||
## Reconciliação por grupos de pedidos
|
||||
|
||||
| Grupo | Estado técnico actual | Pendência que impede conclusão |
|
||||
| --- | --- | --- |
|
||||
| Diagnóstico integral da aplicação e inventário de funcionalidades | Parcial; documentação/matriz consolidada existe | Rever manualmente todas as páginas e validar com perfis reais. Integrações e comportamentos não comprovados pelo código continuam “a validar”. |
|
||||
| Migrações, PostgreSQL/PostGIS, segurança e QA | Parcial/concluído tecnicamente em componentes | Suite foi repetida em cluster descartável; o incidente histórico em `investmoz_test:5432` carece de comparação/restauro validado por DBA. Não executar operações destrutivas nessa base. |
|
||||
| Geografia e formulários de investimento/oportunidades | Implementação extensa e testes presentes | Confirmar catálogo territorial e conteúdo com fonte de negócio, percorrer browser/dispositivos e validar permissões operacionais. |
|
||||
| Projectos, estruturas de investimento, carteira e catálogo | Parcial | Aprovação institucional dos dados, contratos de submissão e percurso completo de investimento real não demonstrados. Não há processamento/custódia de investimento financeiro confirmado. |
|
||||
| Portal público de manifestações e CRM local | Recepção e conversão local idempotentes com consentimento | Fluxo é local. Processamento externo, sincronização e reconciliação não activos. |
|
||||
| SuiteCRM | Cliente API OAuth implementado e desligado por defeito; testes simulados; catálogo lógico/matriz com 15 colunas criado em `suitecrm/mapping-catalog.md` | Docker/Podman ausentes e WSL não instalado; a release SuiteCRM 8.10.x consultada só lista PHP 8.2–8.4, enquanto o projecto exige PHP 8.5. Instalação, campos, RBAC, workers, reconciliação, segurança e operação aguardam runtime compatível/homologação. `SUITECRM_ENABLED=false` deve manter-se. |
|
||||
| Simulador financeiro, plano e tesouraria | Calculadoras independentes implementadas e cobertas por testes | Validação contabilística/institucional e teste de utilizadores. Estimativas não são ofertas de crédito ou aconselhamento. |
|
||||
| Motor fiscal e reforma 2026 | DSL/governação e resolvedor técnicos parciais; componentes legais permanecem indeterminadas | Texto oficial consolidado, anexos, vigência, fundamentos, parâmetros aprovados e casos dourados assinados por Fiscalista. Não activar taxas nem declarar apuramento. |
|
||||
| IVA e rectificação da Lei 13/2016 | Investigação documental parcial; cópia local do BR 89/2017 tem proveniência incompleta; digitalização rectificada não foi lida com fiabilidade | Confirmar autenticidade, texto integral dos anexos corrigidos, vigência e consolidação em fonte oficial. Nenhuma regra decorrente foi activada. |
|
||||
| Incentivos, ISPC, IRPS/IRPC, INSS, retenções, ICE e pauta aduaneira | Estrutura permite manter resultados indeterminados | Validação legal por regime/período, fontes oficiais integrais, elegibilidade/classificação e aprovação profissional. |
|
||||
| Educação e catálogo de serviços | Funcionalidade implementada parcialmente e testes existentes | Aprovação institucional dos cursos, prestadores, reconhecimento e correspondência a emprego. |
|
||||
| Importação/revisão de dados e oportunidades | Revisões documentais e staging existentes; alguns registos foram deixados privados | Resolver duplicados, moedas e autoridade de origem com os responsáveis; não publicar dados incertos. |
|
||||
| Dados pessoais/PII | Revisão e controlos de snapshots, consentimento e minimização existentes | Confirmar finalidade, retenção, acesso, eliminação, bases legais e transferência com encarregado/jurídico; revisão completa de ficheiros de entrada. |
|
||||
| Formulário e acompanhamento de assistência fundiária/DUAT | Fluxo e testes implementados | Não substitui pedido ou decisão oficial. Rever base legal, documentos, acesso, retenção, integração externa e percursos com equipa responsável. |
|
||||
| Acessibilidade e experiência | Testes unitários, browser/visual e validações responsivas registadas para alguns percursos | Teste presencial com teclado, leitor de ecrã, dispositivos e utilizadores; rever mensagens, erros, carregamento e todos os percursos. |
|
||||
| Entrega/produção | Não concluído | Sem autorização de publicação, integração real, migração ou activação. Requer revisão humana e plano de operação/rollback. |
|
||||
| Grupo | Estado técnico actual | Pendência que impede conclusão |
|
||||
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Diagnóstico integral da aplicação e inventário de funcionalidades | Parcial; documentação/matriz consolidada existe | Rever manualmente todas as páginas e validar com perfis reais. Integrações e comportamentos não comprovados pelo código continuam “a validar”. |
|
||||
| Migrações, PostgreSQL/PostGIS, segurança e QA | Parcial/concluído tecnicamente em componentes | Suite foi repetida em cluster descartável; o incidente histórico em `investmoz_test:5432` carece de comparação/restauro validado por DBA. Não executar operações destrutivas nessa base. |
|
||||
| Geografia e formulários de investimento/oportunidades | Implementação extensa e testes presentes | Confirmar catálogo territorial e conteúdo com fonte de negócio, percorrer browser/dispositivos e validar permissões operacionais. |
|
||||
| Projectos, estruturas de investimento, carteira e catálogo | Parcial | Aprovação institucional dos dados, contratos de submissão e percurso completo de investimento real não demonstrados. Não há processamento/custódia de investimento financeiro confirmado. |
|
||||
| Portal público de manifestações e CRM local | Recepção e conversão local idempotentes com consentimento | Fluxo é local. Processamento externo, sincronização e reconciliação não activos. |
|
||||
| SuiteCRM | Cliente API OAuth implementado e desligado por defeito; testes simulados; catálogo lógico/matriz com 15 colunas criado em `suitecrm/mapping-catalog.md` | Docker/Podman ausentes e WSL não instalado; a release SuiteCRM 8.10.x consultada só lista PHP 8.2–8.4, enquanto o projecto exige PHP 8.5. Instalação, campos, RBAC, workers, reconciliação, segurança e operação aguardam runtime compatível/homologação. `SUITECRM_ENABLED=false` deve manter-se. |
|
||||
| Simulador financeiro, plano e tesouraria | Calculadoras independentes implementadas e cobertas por testes | Validação contabilística/institucional e teste de utilizadores. Estimativas não são ofertas de crédito ou aconselhamento. |
|
||||
| Motor fiscal e reforma 2026 | DSL/governação e resolvedor técnicos parciais; componentes legais permanecem indeterminadas | Texto oficial consolidado, anexos, vigência, fundamentos, parâmetros aprovados e casos dourados assinados por Fiscalista. Não activar taxas nem declarar apuramento. |
|
||||
| IVA e rectificação da Lei 13/2016 | Investigação documental parcial; cópia local do BR 89/2017 tem proveniência incompleta; digitalização rectificada não foi lida com fiabilidade | Confirmar autenticidade, texto integral dos anexos corrigidos, vigência e consolidação em fonte oficial. Nenhuma regra decorrente foi activada. |
|
||||
| Incentivos, ISPC, IRPS/IRPC, INSS, retenções, ICE e pauta aduaneira | Estrutura permite manter resultados indeterminados | Validação legal por regime/período, fontes oficiais integrais, elegibilidade/classificação e aprovação profissional. |
|
||||
| Educação e catálogo de serviços | Funcionalidade implementada parcialmente e testes existentes | Aprovação institucional dos cursos, prestadores, reconhecimento e correspondência a emprego. |
|
||||
| Importação/revisão de dados e oportunidades | Revisões documentais e staging existentes; alguns registos foram deixados privados | Resolver duplicados, moedas e autoridade de origem com os responsáveis; não publicar dados incertos. |
|
||||
| Dados pessoais/PII | Revisão e controlos de snapshots, consentimento e minimização existentes | Confirmar finalidade, retenção, acesso, eliminação, bases legais e transferência com encarregado/jurídico; revisão completa de ficheiros de entrada. |
|
||||
| Formulário e acompanhamento de assistência fundiária/DUAT | Fluxo e testes implementados | Não substitui pedido ou decisão oficial. Rever base legal, documentos, acesso, retenção, integração externa e percursos com equipa responsável. |
|
||||
| Acessibilidade e experiência | Testes unitários, browser/visual e validações responsivas registadas para alguns percursos | Teste presencial com teclado, leitor de ecrã, dispositivos e utilizadores; rever mensagens, erros, carregamento e todos os percursos. |
|
||||
| Entrega/produção | Não concluído | Sem autorização de publicação, integração real, migração ou activação. Requer revisão humana e plano de operação/rollback. |
|
||||
|
||||
## Verificações executadas nesta revisão
|
||||
|
||||
|
||||
@@ -4,74 +4,74 @@ Data: 2026-10-03. Âmbito: prompt master recebido em `Pasted text.txt`, document
|
||||
|
||||
## Estado inicial e ambiente
|
||||
|
||||
| Item | Evidência observada |
|
||||
| --- | --- |
|
||||
| Branch / commit | `main` / `4f35313d06ccd597a4ebf9ed043964d898c95813` |
|
||||
| Árvore de trabalho | 50 ficheiros rastreados modificados, muitos não rastreados; cerca de 7.748 inserções e 1.053 remoções rastreadas. Estado anterior preservado, sem reset ou limpeza. |
|
||||
| Runtime | PHP 8.5.11, Composer 2.8.12, Node 24.20.0, npm 11.19.0. Laravel 13.30.1, Inertia Laravel 3.3.3, React 19.2.8, TypeScript 5.9.3, Vite 8.2.2, PHPUnit 12.5.34. |
|
||||
| Dados | Configuração local aponta PostgreSQL em `127.0.0.1:5432`; listener activo. O harness PHPUnit usa PostgreSQL/PostGIS na porta 55441 e allowlist de bases QA. Não foi executada suite PHP nesta linha de base por não haver serviço QA 55441 disponível e por não ser seguro apontar testes com `RefreshDatabase` à instância local activa. |
|
||||
| API / CI | Rotas web e `api/v1`; contratos em `api/openapi.yaml` e contratos por domínio em `docs/investmoz/**`. Workflow `.github/workflows/tests.yml` configura PHP 8.5, Node 24 e serviço PostGIS 17-3.5. |
|
||||
| Documentos pedidos | Prompt master recebido como anexo e `Prompt master para revisão do potal.docx`, documentos em `Formularios` e `por executar`, e relatório PII estão acessíveis. Não localizei `revisao-prompts-investmoz.md` nem `prototipo-portal-v2_1.html` no caminho pesquisado. A falta desses caminhos não prova ausência de funcionalidades. |
|
||||
| Item | Evidência observada |
|
||||
| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Branch / commit | `main` / `4f35313d06ccd597a4ebf9ed043964d898c95813` |
|
||||
| Árvore de trabalho | 50 ficheiros rastreados modificados, muitos não rastreados; cerca de 7.748 inserções e 1.053 remoções rastreadas. Estado anterior preservado, sem reset ou limpeza. |
|
||||
| Runtime | PHP 8.5.11, Composer 2.8.12, Node 24.20.0, npm 11.19.0. Laravel 13.30.1, Inertia Laravel 3.3.3, React 19.2.8, TypeScript 5.9.3, Vite 8.2.2, PHPUnit 12.5.34. |
|
||||
| Dados | Configuração local aponta PostgreSQL em `127.0.0.1:5432`; listener activo. O harness PHPUnit usa PostgreSQL/PostGIS na porta 55441 e allowlist de bases QA. Não foi executada suite PHP nesta linha de base por não haver serviço QA 55441 disponível e por não ser seguro apontar testes com `RefreshDatabase` à instância local activa. |
|
||||
| API / CI | Rotas web e `api/v1`; contratos em `api/openapi.yaml` e contratos por domínio em `docs/investmoz/**`. Workflow `.github/workflows/tests.yml` configura PHP 8.5, Node 24 e serviço PostGIS 17-3.5. |
|
||||
| Documentos pedidos | Prompt master recebido como anexo e `Prompt master para revisão do potal.docx`, documentos em `Formularios` e `por executar`, e relatório PII estão acessíveis. Não localizei `revisao-prompts-investmoz.md` nem `prototipo-portal-v2_1.html` no caminho pesquisado. A falta desses caminhos não prova ausência de funcionalidades. |
|
||||
|
||||
## Matriz única
|
||||
|
||||
Estados: **implementado e verificado**, **parcial**, **não implementado**, **não verificado**, **bloqueado**, **substituído por decisão documentada**.
|
||||
|
||||
| ID / origem | Requisito e aceite verificável | Implementação / evidência actual | Estado / lacuna | Dependências / prioridade | Próxima alteração ou teste / validação pendente |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| M-01 §1–3, master | Preservar autorização, tecnologia, trabalho existente e limites de publicação/dados. | Branch e commit registados; Laravel 13 + React/Inertia/TS + PostgreSQL/PostGIS confirmados; diff preexistente mantido. | Implementado e verificado no procedimento desta revisão. | Transversal / crítica. | Continuar a executar mudanças reversíveis; nenhuma publicação ou integração real sem configuração e autorização específicas. |
|
||||
| M-02 §2, master | Reconciliar documentos e caminhos sem inferir ausência funcional. | Prompt master, directórios `Formularios`/`por executar`, relatório PII e docs internos localizados. Revisão consolidada Markdown e protótipo HTML não localizados no caminho pesquisado. | Parcial. | Inventário documental / alta. | Extrair conteúdo/hash dos DOCX relevantes por lote e mapear requisitos aos IDs; confirmar se os dois artefactos ausentes existem noutra pasta. |
|
||||
| M-03 §4 e §13, master | Inventariar e manter matriz completa, sequencial e persistente. | Esta matriz e o ledger `simulator-evolution-progress.md`; rotas, testes, migrations, CI e OpenAPI localizados. | Parcial. | Todas as etapas / crítica. | Expandir matriz com requisitos atómicos e referências de parágrafo/tabela dos DOCX; actualizar estado após cada etapa. |
|
||||
| M-04 §5, master | Resolver conflitos documentais sem executar prompts/DDL literalmente. | Decisões reconciliadas em `docs/investmoz/simulations/current-status-2026-10-03.md`, `fiscal-reform-2026-review.md`, `official-source-register.md`, docs GIS e existing-business. | Parcial. | Evidência e decisões do utilizador / alta. | Confirmar precedência documental por domínio e registar decisão quando versões de formulário divergem. |
|
||||
| M-05 §6 e §14, master | Validar segurança, relações, rascunhos, concorrência, permissões, exposição, erros, pesquisa e UX. | Policies, migrations, formulários, testes de concorrência e fluxo de rascunhos; relatórios browser em alguns domínios. | Parcial; cobertura transversal não verificada. | Acesso QA, critérios por domínio / crítica. | Testes negativos de autorização, erros, pesquisa/paginação, concorrência e acessibilidade para cada fluxo antes de aceite. |
|
||||
| D-01 §7, master | Dados mestres territoriais consistentes, acessíveis e pesquisáveis. | `PublicGeographyController`, `TerritorialRegistry`, migrations/seeder e `docs/investmoz/gis`; teste frontend limpa descendentes quando ascendente muda. | Parcial. | Fontes oficiais, integridade hierárquica / alta. | Confirmar cobertura de países/CAE/localidades, pesquisa sem acentos, pertença servidor e reconciliação de importação sem fusão fuzzy automática. |
|
||||
| D-02 §8, master | Projectos, clusters, corredores, parques, ZEE/ZFI e PII com fonte, relação e revisão. | Controllers e rotas para projectos/clusters/zonas; documentos de zonas e baseline; relatório PII DOCX inventariado em `pii-extraction-review-2026-10-03.md`. | Parcial. Corredores/parques e importação rastreável do PII precisam de auditoria funcional específica. | Origem, aprovação editorial, relações versionadas / alta. | 174 linhas de projecto inventariadas com tabelas de origem; um título repetido e uma tabela com moeda USD/MZN ambígua. Staging CSV aguarda reconciliação do duplicado/moeda e validação responsável/localização; sem publicar projectos previstos como oportunidades. |
|
||||
| D-03 §9, master | Catálogo consistente e formulários curtos, condicionais, retomáveis, com dados públicos separados e submissão não publica automaticamente. | Catálogo público, intake de empresas existentes, formulários de investidor/simulador e rascunhos; `docs/investmoz/existing-business/**`. | Parcial. Upload seguro/quarentena e reconciliação completa mapa/lista/contagens não verificados. | RBAC, armazenamento privado, política antimalware / crítica. | Fazer threat review e testes de formato/tamanho/autorização/revogação; anexos pendentes não devem ser distribuídos. |
|
||||
| D-04 §10, master | CRM interno com interesses, contactos, responsáveis, tarefas, consentimento e acompanhamento; SuiteCRM por API suportada. | `CrmController`, modelos/rotas management, consentimento e documentação `docs/investmoz/suitecrm/**`; testes de intake e cliente. | Parcial. Mandatos, diligência, propostas versionadas, fecho e integração real não verificados ou ausentes. | Papéis, consentimento, configuração SuiteCRM / alta. | Cobrir revogação e isolamento por organização; manter CRM interno distinto; não activar SuiteCRM nem tratar aceite como transacção concluída. |
|
||||
| D-05 §11, master | Fontes legais versionadas; simulador com pressupostos e histórico; cálculo fiscal apenas com fontes/regras aprovadas. | Governança de regras, contratos, snapshots, fontes arquivadas e testes; `current-status-2026-10-03.md` mantém cálculo fiscal público bloqueado. | Parcial; cálculo fiscal bloqueado. | Fiscalista, diplomas integrais e parâmetros vigentes / crítica. | Manter indeterminação; não activar taxas/benefícios; validar alterações por período e cenários financeiros independentes. |
|
||||
| D-06 §12, master | Educação baseada em evidência, sem certificação/contratação automática; assistência com fontes autorizadas e revisão. | Controllers/páginas/testes Educação; AiGateway e controles internos documentados. | Parcial. Cobertura de correspondência oferta/procura e revalidação das fontes precisa aceite funcional. | Dados autorizados, moderação humana / média-alta. | Auditar permissões, fonte e decisão humana; não introduzir modelo generativo nem recomendação automática sem gate. |
|
||||
| D-07 §13, master | Executar sequência dependente sem parar após plano; guardar estado e bloqueios por módulo. | Etapas documentadas em gates 9–11 e ledger; várias etapas já parcialmente executadas. | Parcial. | Matriz + validação por módulo / alta. | Continuar com segurança/integridade e módulos independentes enquanto gates legal/external ficam bloqueados. |
|
||||
| M-08 §14–15, master | Verificações reproduzíveis, comparação com base, sem alegar sucesso global. | CI existe; evidências PHP 231/231. Nesta linha: frontend 99/99, TypeScript passou e build passou; PHPStan 0 erros. Pint e `vp check` globais mantêm achados descritos abaixo. | Parcial. A cobertura manual com leitor de ecrã permanece em falta. | QA PostGIS isolado / crítica. | Corrigir lint legado/ledger sem reescrita não autorizada; completar validação assistiva manual. |
|
||||
| M-09 §16, master | Relatório final com evidência, reconciliação, rollback e prontidão por módulo. | Matriz inicial criada; runbooks e status parciais por domínio. | Parcial. | Conclusão das etapas / média. | Actualizar tabela e prontidão no fecho de cada tranche; não declarar lançamento integral antes de gates. |
|
||||
| C-01 regra de conflito, master | Não fabricar coordenadas; manter lista para não georreferenciados. | Fluxos de coordenadas e mapas existentes; documentação GIS prevê revisão de localização. | Não verificado ponta a ponta. | Fonte espacial validada / alta. | Teste de oportunidade sem coordenadas na lista e não inclusão no mapa; verificar contagens. |
|
||||
| C-02 regra de conflito, master | Distinguir CRM interno de SuiteCRM e domínios de projecto/dossier/oportunidade/negócio/território. | Rotas, modelos e contratos separados; SuiteCRM tem cliente/outbox documentados. | Parcial. | Dicionário canónico / alta. | Rever imports, conversões e UI para impedir associação/tipo implícito indevido. |
|
||||
| C-03 regra de conflito, master | Aprovações próprias de cada domínio; não aplicar DDL histórico se estrutura equivalente existir. | Gates de domínio, migrations incrementais e documentação de esquema existentes. | Parcial. | Mapeamento migrations/modelos/gates / alta. | Validar cada migration contra schema real QA e manter as migrations executadas imutáveis. |
|
||||
| V-01 relatório PII + §8 master | Extrair investimento, período, moeda, fase, responsáveis e localização com origem verificável. | DOCX identificado por SHA-256 e estruturalmente lido; resumo em `pii-extraction-review-2026-10-03.md`; nenhuma importação executada. | Parcial; estrutura e contagens verificadas, conteúdo substancial não validado. | Confirmar duplicado, moeda da tabela 19, responsável, estado e topónimos / alta. | Produzir staging rastreável apenas depois da revisão humana; sem publicar como oportunidade. |
|
||||
| V-02 §9 master | Upload/download protegido: MIME real, tamanho, classe, storage privado, ACL, antimalware e histórico. | `PortalServicesController::upload` passou a gravar em `quarantined/internal`; o catálogo e `resource()` só aceitam `active/public`; teste actualizado cobre bloqueio e fluxo de documento previamente revisto. | Parcial; a quarentena segura foi corrigida, scanner/fluxo de revisão autorizado não existe/verificado. | Configuração de storage/scanner e perfis / crítica. | Manter novos anexos indisponíveis até scanner + revisão autorizada; teste de integração pendente por falta de PostgreSQL QA activo. |
|
||||
| V-03 §10 master | Consentimento revogável e acompanhamento sem custódia/pagamentos. | Rotas CRM e retirada de consentimento de divulgação do proponente; sem evidência neste inventário de custódia/pagamento. | Parcial. | Política de consentimento, estado de negociação / alta. | Cobrir revogação com acesso posterior negado e auditoria sem PII; manter pagamentos fora do âmbito. |
|
||||
| V-04 §11 master + reforma 2026 | Separar períodos fiscais históricos e novos, distinguir crédito de caixa, sem residência inferida por nacionalidade/dias. | Testes frontend de intake fiscal e ponte financeira; status fiscal indeterminado sem regra aprovada. | Parcial; regras legais bloqueadas. | Diploma integral, aprovação fiscal e testes de fronteira / crítica. | Manter estados indeterminados e testar antes/depois de vigência somente quando regra oficial estiver aprovada. |
|
||||
| V-05 §12 master | Educação e IA apenas com dados autorizados, fontes revalidadas e revisão humana. | Educação e AI gateway com controle de feature; testes locais existem. | Parcial; activação externa não verificada. | Dados e configuração, avaliação humana / alta. | Testar feature desligada, ACL e fontes revogadas; integração permanece desactivada até configuração/autorização. |
|
||||
| B-01 baseline 2026-10-03 | Checks seguros disponíveis sem alterar dados operacionais. | Nesta extensão: frontend `node --test tests/frontend/*.test.mjs` 99/99; TypeScript passou; build 2.388 módulos; formatter/lint dirigidos passaram. Última suite PHP global permanece 231/231, 2.299 assertions. `composer run lint:check` global falhou em cinco ficheiros anteriores à tranche; `npm.cmd run check` global falhou ao ler o ledger histórico. | Parcial. | QA isolado para novas alterações backend e aceitação final / alta. | Corrigir causa do ledger binário por migração de formato separada; manter histórico append-only. Corrigir lint global em tranche identificada. |
|
||||
| ID / origem | Requisito e aceite verificável | Implementação / evidência actual | Estado / lacuna | Dependências / prioridade | Próxima alteração ou teste / validação pendente |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| M-01 §1–3, master | Preservar autorização, tecnologia, trabalho existente e limites de publicação/dados. | Branch e commit registados; Laravel 13 + React/Inertia/TS + PostgreSQL/PostGIS confirmados; diff preexistente mantido. | Implementado e verificado no procedimento desta revisão. | Transversal / crítica. | Continuar a executar mudanças reversíveis; nenhuma publicação ou integração real sem configuração e autorização específicas. |
|
||||
| M-02 §2, master | Reconciliar documentos e caminhos sem inferir ausência funcional. | Prompt master, directórios `Formularios`/`por executar`, relatório PII e docs internos localizados. Revisão consolidada Markdown e protótipo HTML não localizados no caminho pesquisado. | Parcial. | Inventário documental / alta. | Extrair conteúdo/hash dos DOCX relevantes por lote e mapear requisitos aos IDs; confirmar se os dois artefactos ausentes existem noutra pasta. |
|
||||
| M-03 §4 e §13, master | Inventariar e manter matriz completa, sequencial e persistente. | Esta matriz e o ledger `simulator-evolution-progress.md`; rotas, testes, migrations, CI e OpenAPI localizados. | Parcial. | Todas as etapas / crítica. | Expandir matriz com requisitos atómicos e referências de parágrafo/tabela dos DOCX; actualizar estado após cada etapa. |
|
||||
| M-04 §5, master | Resolver conflitos documentais sem executar prompts/DDL literalmente. | Decisões reconciliadas em `docs/investmoz/simulations/current-status-2026-10-03.md`, `fiscal-reform-2026-review.md`, `official-source-register.md`, docs GIS e existing-business. | Parcial. | Evidência e decisões do utilizador / alta. | Confirmar precedência documental por domínio e registar decisão quando versões de formulário divergem. |
|
||||
| M-05 §6 e §14, master | Validar segurança, relações, rascunhos, concorrência, permissões, exposição, erros, pesquisa e UX. | Policies, migrations, formulários, testes de concorrência e fluxo de rascunhos; relatórios browser em alguns domínios. | Parcial; cobertura transversal não verificada. | Acesso QA, critérios por domínio / crítica. | Testes negativos de autorização, erros, pesquisa/paginação, concorrência e acessibilidade para cada fluxo antes de aceite. |
|
||||
| D-01 §7, master | Dados mestres territoriais consistentes, acessíveis e pesquisáveis. | `PublicGeographyController`, `TerritorialRegistry`, migrations/seeder e `docs/investmoz/gis`; teste frontend limpa descendentes quando ascendente muda. | Parcial. | Fontes oficiais, integridade hierárquica / alta. | Confirmar cobertura de países/CAE/localidades, pesquisa sem acentos, pertença servidor e reconciliação de importação sem fusão fuzzy automática. |
|
||||
| D-02 §8, master | Projectos, clusters, corredores, parques, ZEE/ZFI e PII com fonte, relação e revisão. | Controllers e rotas para projectos/clusters/zonas; documentos de zonas e baseline; relatório PII DOCX inventariado em `pii-extraction-review-2026-10-03.md`. | Parcial. Corredores/parques e importação rastreável do PII precisam de auditoria funcional específica. | Origem, aprovação editorial, relações versionadas / alta. | 174 linhas de projecto inventariadas com tabelas de origem; um título repetido e uma tabela com moeda USD/MZN ambígua. Staging CSV aguarda reconciliação do duplicado/moeda e validação responsável/localização; sem publicar projectos previstos como oportunidades. |
|
||||
| D-03 §9, master | Catálogo consistente e formulários curtos, condicionais, retomáveis, com dados públicos separados e submissão não publica automaticamente. | Catálogo público, intake de empresas existentes, formulários de investidor/simulador e rascunhos; `docs/investmoz/existing-business/**`. | Parcial. Upload seguro/quarentena e reconciliação completa mapa/lista/contagens não verificados. | RBAC, armazenamento privado, política antimalware / crítica. | Fazer threat review e testes de formato/tamanho/autorização/revogação; anexos pendentes não devem ser distribuídos. |
|
||||
| D-04 §10, master | CRM interno com interesses, contactos, responsáveis, tarefas, consentimento e acompanhamento; SuiteCRM por API suportada. | `CrmController`, modelos/rotas management, consentimento e documentação `docs/investmoz/suitecrm/**`; testes de intake e cliente. | Parcial. Mandatos, diligência, propostas versionadas, fecho e integração real não verificados ou ausentes. | Papéis, consentimento, configuração SuiteCRM / alta. | Cobrir revogação e isolamento por organização; manter CRM interno distinto; não activar SuiteCRM nem tratar aceite como transacção concluída. |
|
||||
| D-05 §11, master | Fontes legais versionadas; simulador com pressupostos e histórico; cálculo fiscal apenas com fontes/regras aprovadas. | Governança de regras, contratos, snapshots, fontes arquivadas e testes; `current-status-2026-10-03.md` mantém cálculo fiscal público bloqueado. | Parcial; cálculo fiscal bloqueado. | Fiscalista, diplomas integrais e parâmetros vigentes / crítica. | Manter indeterminação; não activar taxas/benefícios; validar alterações por período e cenários financeiros independentes. |
|
||||
| D-06 §12, master | Educação baseada em evidência, sem certificação/contratação automática; assistência com fontes autorizadas e revisão. | Controllers/páginas/testes Educação; AiGateway e controles internos documentados. | Parcial. Cobertura de correspondência oferta/procura e revalidação das fontes precisa aceite funcional. | Dados autorizados, moderação humana / média-alta. | Auditar permissões, fonte e decisão humana; não introduzir modelo generativo nem recomendação automática sem gate. |
|
||||
| D-07 §13, master | Executar sequência dependente sem parar após plano; guardar estado e bloqueios por módulo. | Etapas documentadas em gates 9–11 e ledger; várias etapas já parcialmente executadas. | Parcial. | Matriz + validação por módulo / alta. | Continuar com segurança/integridade e módulos independentes enquanto gates legal/external ficam bloqueados. |
|
||||
| M-08 §14–15, master | Verificações reproduzíveis, comparação com base, sem alegar sucesso global. | CI existe; evidências PHP 231/231. Nesta linha: frontend 99/99, TypeScript passou e build passou; PHPStan 0 erros. Pint e `vp check` globais mantêm achados descritos abaixo. | Parcial. A cobertura manual com leitor de ecrã permanece em falta. | QA PostGIS isolado / crítica. | Corrigir lint legado/ledger sem reescrita não autorizada; completar validação assistiva manual. |
|
||||
| M-09 §16, master | Relatório final com evidência, reconciliação, rollback e prontidão por módulo. | Matriz inicial criada; runbooks e status parciais por domínio. | Parcial. | Conclusão das etapas / média. | Actualizar tabela e prontidão no fecho de cada tranche; não declarar lançamento integral antes de gates. |
|
||||
| C-01 regra de conflito, master | Não fabricar coordenadas; manter lista para não georreferenciados. | Fluxos de coordenadas e mapas existentes; documentação GIS prevê revisão de localização. | Não verificado ponta a ponta. | Fonte espacial validada / alta. | Teste de oportunidade sem coordenadas na lista e não inclusão no mapa; verificar contagens. |
|
||||
| C-02 regra de conflito, master | Distinguir CRM interno de SuiteCRM e domínios de projecto/dossier/oportunidade/negócio/território. | Rotas, modelos e contratos separados; SuiteCRM tem cliente/outbox documentados. | Parcial. | Dicionário canónico / alta. | Rever imports, conversões e UI para impedir associação/tipo implícito indevido. |
|
||||
| C-03 regra de conflito, master | Aprovações próprias de cada domínio; não aplicar DDL histórico se estrutura equivalente existir. | Gates de domínio, migrations incrementais e documentação de esquema existentes. | Parcial. | Mapeamento migrations/modelos/gates / alta. | Validar cada migration contra schema real QA e manter as migrations executadas imutáveis. |
|
||||
| V-01 relatório PII + §8 master | Extrair investimento, período, moeda, fase, responsáveis e localização com origem verificável. | DOCX identificado por SHA-256 e estruturalmente lido; resumo em `pii-extraction-review-2026-10-03.md`; nenhuma importação executada. | Parcial; estrutura e contagens verificadas, conteúdo substancial não validado. | Confirmar duplicado, moeda da tabela 19, responsável, estado e topónimos / alta. | Produzir staging rastreável apenas depois da revisão humana; sem publicar como oportunidade. |
|
||||
| V-02 §9 master | Upload/download protegido: MIME real, tamanho, classe, storage privado, ACL, antimalware e histórico. | `PortalServicesController::upload` passou a gravar em `quarantined/internal`; o catálogo e `resource()` só aceitam `active/public`; teste actualizado cobre bloqueio e fluxo de documento previamente revisto. | Parcial; a quarentena segura foi corrigida, scanner/fluxo de revisão autorizado não existe/verificado. | Configuração de storage/scanner e perfis / crítica. | Manter novos anexos indisponíveis até scanner + revisão autorizada; teste de integração pendente por falta de PostgreSQL QA activo. |
|
||||
| V-03 §10 master | Consentimento revogável e acompanhamento sem custódia/pagamentos. | Rotas CRM e retirada de consentimento de divulgação do proponente; sem evidência neste inventário de custódia/pagamento. | Parcial. | Política de consentimento, estado de negociação / alta. | Cobrir revogação com acesso posterior negado e auditoria sem PII; manter pagamentos fora do âmbito. |
|
||||
| V-04 §11 master + reforma 2026 | Separar períodos fiscais históricos e novos, distinguir crédito de caixa, sem residência inferida por nacionalidade/dias. | Testes frontend de intake fiscal e ponte financeira; status fiscal indeterminado sem regra aprovada. | Parcial; regras legais bloqueadas. | Diploma integral, aprovação fiscal e testes de fronteira / crítica. | Manter estados indeterminados e testar antes/depois de vigência somente quando regra oficial estiver aprovada. |
|
||||
| V-05 §12 master | Educação e IA apenas com dados autorizados, fontes revalidadas e revisão humana. | Educação e AI gateway com controle de feature; testes locais existem. | Parcial; activação externa não verificada. | Dados e configuração, avaliação humana / alta. | Testar feature desligada, ACL e fontes revogadas; integração permanece desactivada até configuração/autorização. |
|
||||
| B-01 baseline 2026-10-03 | Checks seguros disponíveis sem alterar dados operacionais. | Nesta extensão: frontend `node --test tests/frontend/*.test.mjs` 99/99; TypeScript passou; build 2.388 módulos; formatter/lint dirigidos passaram. Última suite PHP global permanece 231/231, 2.299 assertions. `composer run lint:check` global falhou em cinco ficheiros anteriores à tranche; `npm.cmd run check` global falhou ao ler o ledger histórico. | Parcial. | QA isolado para novas alterações backend e aceitação final / alta. | Corrigir causa do ledger binário por migração de formato separada; manter histórico append-only. Corrigir lint global em tranche identificada. |
|
||||
|
||||
## Reconciliação de versões e conflitos
|
||||
|
||||
| Conflito | Decisão baseada no pedido e evidência actual | Consequência |
|
||||
| --- | --- | --- |
|
||||
| Coordenada obrigatória vs localização por validar | Não inventar coordenada; lista continua disponível. | Publicação no mapa requer evidência geográfica validada. |
|
||||
| CRM vs SuiteCRM | CRM interno é domínio próprio; SuiteCRM é integração por mecanismo suportado. | Sem escrita directa na BD SuiteCRM; manter integração desligada até configuração e teste. |
|
||||
| Projecto previsto vs oportunidade disponível | São entidades e estados editoriais distintos. | Investimento do PII não entra automaticamente no catálogo público. |
|
||||
| Benefício/regra fiscal em resumo secundário vs diploma oficial | Somente fonte primária vigente e aprovação formal permitem regra executável. | Cenário público mantém indeterminação quando falta fonte/aprovação. |
|
||||
| Propostas DDL históricas vs esquema já evoluído | Reconciliar pela estrutura actual e migrations incrementais; não executar DDL literalmente nem editar migration aplicada. | Qualquer divergência requer teste de schema em QA antes da alteração. |
|
||||
| Conflito | Decisão baseada no pedido e evidência actual | Consequência |
|
||||
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
|
||||
| Coordenada obrigatória vs localização por validar | Não inventar coordenada; lista continua disponível. | Publicação no mapa requer evidência geográfica validada. |
|
||||
| CRM vs SuiteCRM | CRM interno é domínio próprio; SuiteCRM é integração por mecanismo suportado. | Sem escrita directa na BD SuiteCRM; manter integração desligada até configuração e teste. |
|
||||
| Projecto previsto vs oportunidade disponível | São entidades e estados editoriais distintos. | Investimento do PII não entra automaticamente no catálogo público. |
|
||||
| Benefício/regra fiscal em resumo secundário vs diploma oficial | Somente fonte primária vigente e aprovação formal permitem regra executável. | Cenário público mantém indeterminação quando falta fonte/aprovação. |
|
||||
| Propostas DDL históricas vs esquema já evoluído | Reconciliar pela estrutura actual e migrations incrementais; não executar DDL literalmente nem editar migration aplicada. | Qualquer divergência requer teste de schema em QA antes da alteração. |
|
||||
|
||||
## Linha de base e evidências
|
||||
|
||||
| Comando executado | Resultado |
|
||||
| --- | --- |
|
||||
| `git branch --show-current; git rev-parse HEAD; git status --short` | `main`, commit acima; diff preexistente preservado. |
|
||||
| `php artisan route:list --json` com PHP 8.5.11 | Sucesso; rotas públicas, autenticadas e management listadas. |
|
||||
| `composer show --no-interaction --direct` | Sucesso; versões do backend confirmadas. |
|
||||
| `npm.cmd --version; npm.cmd ls --depth=0 --json` | npm 11.19.0; dependências directas listadas. |
|
||||
| `node --test tests/frontend/*.test.mjs` | 98 aprovados, 0 falhas. |
|
||||
| `npm.cmd run types:check` | Passou, `tsc --noEmit`. |
|
||||
| `composer run types:check --no-interaction` | Passou, PHPStan: 0 erros. |
|
||||
| `composer run lint:check --no-interaction` | Falhou em cinco ficheiros: migration territorial de 28/09, `routes/investment-zones.php`, `routes/web.php`, `tests/Feature/ApiContractTest.php`, `tools/test-mne-integrity.php`. Não alterados nesta tranche. |
|
||||
| `npm.cmd run check` | Falhou antes da análise porque `docs/investmoz/simulator-evolution-progress.md` não pode ser lido pelo formatador (ficheiro binário/inacessível). Nenhuma opção `--fix` executada. |
|
||||
| `php vendor/bin/phpunit --configuration=phpunit.xml --filter PortalServicesTest --do-not-cache-result` | 5/5 testes, 191 assertions; passou na base QA `investmoz_browser_e2e_test`, porta 55441.
|
||||
| `php vendor/bin/phpunit --configuration=phpunit.xml --do-not-cache-result` | 231/231 testes, 2.299 assertions; passou em 71,593 s na mesma base QA isolada. |
|
||||
| Build Vite | 2.388 módulos compilados com sucesso nesta revisão. |
|
||||
| `npm.cmd run build` | Passou nesta revisão; 2.388 módulos. Aviso não bloqueante: dependência opcional `fontaine`. |
|
||||
| `php -l app/Http/Controllers/PortalServicesController.php`; `php -l tests/Feature/PortalServicesTest.php`; Pint dirigido `--test` nesses dois ficheiros | Sintaxe passou e Pint dirigido passou. |
|
||||
| `git diff --check` | Passou; avisos apenas de normalização CRLF/LF nos docs/forms preexistentes. |
|
||||
| Comando executado | Resultado |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `git branch --show-current; git rev-parse HEAD; git status --short` | `main`, commit acima; diff preexistente preservado. |
|
||||
| `php artisan route:list --json` com PHP 8.5.11 | Sucesso; rotas públicas, autenticadas e management listadas. |
|
||||
| `composer show --no-interaction --direct` | Sucesso; versões do backend confirmadas. |
|
||||
| `npm.cmd --version; npm.cmd ls --depth=0 --json` | npm 11.19.0; dependências directas listadas. |
|
||||
| `node --test tests/frontend/*.test.mjs` | 98 aprovados, 0 falhas. |
|
||||
| `npm.cmd run types:check` | Passou, `tsc --noEmit`. |
|
||||
| `composer run types:check --no-interaction` | Passou, PHPStan: 0 erros. |
|
||||
| `composer run lint:check --no-interaction` | Falhou em cinco ficheiros: migration territorial de 28/09, `routes/investment-zones.php`, `routes/web.php`, `tests/Feature/ApiContractTest.php`, `tools/test-mne-integrity.php`. Não alterados nesta tranche. |
|
||||
| `npm.cmd run check` | Falhou antes da análise porque `docs/investmoz/simulator-evolution-progress.md` não pode ser lido pelo formatador (ficheiro binário/inacessível). Nenhuma opção `--fix` executada. |
|
||||
| `php vendor/bin/phpunit --configuration=phpunit.xml --filter PortalServicesTest --do-not-cache-result` | 5/5 testes, 191 assertions; passou na base QA `investmoz_browser_e2e_test`, porta 55441. |
|
||||
| `php vendor/bin/phpunit --configuration=phpunit.xml --do-not-cache-result` | 231/231 testes, 2.299 assertions; passou em 71,593 s na mesma base QA isolada. |
|
||||
| Build Vite | 2.388 módulos compilados com sucesso nesta revisão. |
|
||||
| `npm.cmd run build` | Passou nesta revisão; 2.388 módulos. Aviso não bloqueante: dependência opcional `fontaine`. |
|
||||
| `php -l app/Http/Controllers/PortalServicesController.php`; `php -l tests/Feature/PortalServicesTest.php`; Pint dirigido `--test` nesses dois ficheiros | Sintaxe passou e Pint dirigido passou. |
|
||||
| `git diff --check` | Passou; avisos apenas de normalização CRLF/LF nos docs/forms preexistentes. |
|
||||
|
||||
## Prioridade para continuidade
|
||||
|
||||
@@ -152,75 +152,75 @@ The three investment structure junction tables now support association type, sou
|
||||
|
||||
Esta adenda prevalece sobre estados e caminhos pesquisados em 2026-10-03, sem apagar o registo historico. A matriz continua sendo o indice consolidado; `simulator-evolution-progress.md` e o diario de execucao, nao uma segunda matriz.
|
||||
|
||||
| ID | Requisito | Evidencia actual conferida | Estado actual e accao | Teste/gate |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| M-02 | Reconciliar prompts, fontes e pastas indicadas, sem inferir ausencia a partir de um caminho errado. | A pasta `C:\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\prompt` e acessivel; existe conjunto de DOCX, incluindo prompts do simulador e formulários. A pasta `Legislacao` tem codigos e alteracoes fiscais antes registados como nao disponiveis. Foi localizado `Informacao Publica\Legislacao - resumos\Reforma fiscal de 2026.docx`. | Corrigida a afirmacao de indisponibilidade do resumo fiscal. A coleccao foi inventariada e documentos relevantes foram extraidos selectivamente; nao se declara leitura/reconciliacao integral de todos os DOCX. `DUAT.docx` permanece bloqueado por uso do ficheiro e nao foi alterado. | Continuar leitura por dominio e actualizar esta matriz; scans exigem OCR/cotejo visual. |
|
||||
| D-05 / V-04 | Motor fiscal reproduzivel, por periodo, com fontes, formulas, pressupostos e resultado indeterminado quando faltar dado. | `SimulationTaxRuleExpression` implementa DSL allowlist/DAG/BigRational; `SimulationTaxParameterResolver` trata periodo, `scope_key`, contexto e fonte; endpoint publico projecta parametros; interface apresenta estado/fonte. `RunSimulation` nao invoca o avaliador e continua aritmetica geral/custom. | **Parcial; nenhum motor de apuramento fiscal integrado.** Testes de DSL/resolucao existem, mas sem casos dourados externos ligados a resultados independentes. Nao chamar o fluxo genericamente estimativo de calculo legal. | Executar os testes unitarios tecnicos; gate por componente requer texto legivel/autenticado, regra ligada, snapshot/explicabilidade e resultado de referencia confirmado. |
|
||||
| D-05 / M-04 | Consolidar CIVA sem somar texto substituido e respeitar vigencia. | O acervo tem Lei 13/2016 (32 paginas, sem camada de texto), PDF denominado rectificado (15 paginas, sem camada de texto), Lei 22/2022 (26 paginas, camada de texto) e alteracao CIVA 2023 (18 paginas, texto extraivel). Ha fontes oficiais INM/Parlamento ja citadas em `official-source-register.md`. | Localizacao confirmada; OCR util nao obtido nesta execucao para scans. Aplicar Lei 13/2016 como base metodologica, reconciliar diplomas anteriores e aplicar posteriores apenas por artigo/vigencia. Anexos rectificados continuam pendentes de cotejo legivel. | Sem activar isencoes/taxas afectadas ate cotejo de fonte/autenticidade e testes antes/depois da vigencia. |
|
||||
| D-05 / M-04 | Distinguir diploma e resumo secundario, preservar a origem. | BR 248 arquivado tem 21 paginas, 440.018 bytes e SHA-256 `CCF2A8DEECA820F2DD076B4B2141B1EFE3F6A806568C3A587CA59A8E92F4C251`; a extraccao foi conferida nas disposicoes abaixo. A copia do acervo `BR_248_I_SERIE_SUPLEMENTO_2025.pdf` tem 20 paginas e SHA-256 `A0110696253715567AF39C159A21BACFEBC32F944FA43A4EFD18070D36959E50`, logo nao e o mesmo binario. CBF e ISPC_Financas foram localizados, mas sao scans ou extraccoes parciais. | O resumo “Reforma fiscal de 2026” localizado e fonte secundaria. Existencia e texto extraivel nao equivalem a autenticidade/pericial nem a aprovacao do caso. Nao inserir parametros com base em tabelas de resumo. | Comparacao artigo a artigo contra edicao oficial arquivada e teste de vigencia por regra. |
|
||||
| D-05 / M-09 | Registar revisao profissional informada sem alargar o ambito de documentos. | O proprietario declarou que contabilista/fiscalista reviu e validou o simulador, e ja autorizou formalmente matriz/regras. O PDF OCAM refere-se a Declaracao Anual de Informacao Contabilistica e Fiscal (Modelo 20) da empresa no exercicio de 2025. | Registar a declaracao como informacao/autorizacao do proprietario, nao como validacao independente. O PDF do Modelo 20 nao certifica simulador nem testes fiscais; nao reproduzir PII. A falta de relatorio individual nao impede codigo neutro, testes nem cenarios estimativos fundamentados. | Confirmacao especifica necessaria para regras com fonte/anexo/vigencia ilegivel ou nao consolidada, elegibilidade de beneficios, classificacao pautal/ICE, parametros novos de 2026 e resultados dourados por componente. |
|
||||
| B-01 | Preservar ambiente e provar apenas comandos efectivamente executados. | Worktree apresenta muitas alteracoes preexistentes/nao comprometidas; foram preservadas. PHP 8.5 permanece requisito do portal. OCR foi tentado com dependencias temporarias; originais nao foram alterados. | Sem reset/clean, migration operacional, activacao fiscal ou mudanca de infraestrutura nesta tranche. | Tests devem usar PHP 8.5.11 e DB descartavel; nao inferir QA a partir da base local. |
|
||||
| ID | Requisito | Evidencia actual conferida | Estado actual e accao | Teste/gate |
|
||||
| ----------- | ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| M-02 | Reconciliar prompts, fontes e pastas indicadas, sem inferir ausencia a partir de um caminho errado. | A pasta `C:\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\prompt` e acessivel; existe conjunto de DOCX, incluindo prompts do simulador e formulários. A pasta `Legislacao` tem codigos e alteracoes fiscais antes registados como nao disponiveis. Foi localizado `Informacao Publica\Legislacao - resumos\Reforma fiscal de 2026.docx`. | Corrigida a afirmacao de indisponibilidade do resumo fiscal. A coleccao foi inventariada e documentos relevantes foram extraidos selectivamente; nao se declara leitura/reconciliacao integral de todos os DOCX. `DUAT.docx` permanece bloqueado por uso do ficheiro e nao foi alterado. | Continuar leitura por dominio e actualizar esta matriz; scans exigem OCR/cotejo visual. |
|
||||
| D-05 / V-04 | Motor fiscal reproduzivel, por periodo, com fontes, formulas, pressupostos e resultado indeterminado quando faltar dado. | `SimulationTaxRuleExpression` implementa DSL allowlist/DAG/BigRational; `SimulationTaxParameterResolver` trata periodo, `scope_key`, contexto e fonte; endpoint publico projecta parametros; interface apresenta estado/fonte. `RunSimulation` nao invoca o avaliador e continua aritmetica geral/custom. | **Parcial; nenhum motor de apuramento fiscal integrado.** Testes de DSL/resolucao existem, mas sem casos dourados externos ligados a resultados independentes. Nao chamar o fluxo genericamente estimativo de calculo legal. | Executar os testes unitarios tecnicos; gate por componente requer texto legivel/autenticado, regra ligada, snapshot/explicabilidade e resultado de referencia confirmado. |
|
||||
| D-05 / M-04 | Consolidar CIVA sem somar texto substituido e respeitar vigencia. | O acervo tem Lei 13/2016 (32 paginas, sem camada de texto), PDF denominado rectificado (15 paginas, sem camada de texto), Lei 22/2022 (26 paginas, camada de texto) e alteracao CIVA 2023 (18 paginas, texto extraivel). Ha fontes oficiais INM/Parlamento ja citadas em `official-source-register.md`. | Localizacao confirmada; OCR util nao obtido nesta execucao para scans. Aplicar Lei 13/2016 como base metodologica, reconciliar diplomas anteriores e aplicar posteriores apenas por artigo/vigencia. Anexos rectificados continuam pendentes de cotejo legivel. | Sem activar isencoes/taxas afectadas ate cotejo de fonte/autenticidade e testes antes/depois da vigencia. |
|
||||
| D-05 / M-04 | Distinguir diploma e resumo secundario, preservar a origem. | BR 248 arquivado tem 21 paginas, 440.018 bytes e SHA-256 `CCF2A8DEECA820F2DD076B4B2141B1EFE3F6A806568C3A587CA59A8E92F4C251`; a extraccao foi conferida nas disposicoes abaixo. A copia do acervo `BR_248_I_SERIE_SUPLEMENTO_2025.pdf` tem 20 paginas e SHA-256 `A0110696253715567AF39C159A21BACFEBC32F944FA43A4EFD18070D36959E50`, logo nao e o mesmo binario. CBF e ISPC_Financas foram localizados, mas sao scans ou extraccoes parciais. | O resumo “Reforma fiscal de 2026” localizado e fonte secundaria. Existencia e texto extraivel nao equivalem a autenticidade/pericial nem a aprovacao do caso. Nao inserir parametros com base em tabelas de resumo. | Comparacao artigo a artigo contra edicao oficial arquivada e teste de vigencia por regra. |
|
||||
| D-05 / M-09 | Registar revisao profissional informada sem alargar o ambito de documentos. | O proprietario declarou que contabilista/fiscalista reviu e validou o simulador, e ja autorizou formalmente matriz/regras. O PDF OCAM refere-se a Declaracao Anual de Informacao Contabilistica e Fiscal (Modelo 20) da empresa no exercicio de 2025. | Registar a declaracao como informacao/autorizacao do proprietario, nao como validacao independente. O PDF do Modelo 20 nao certifica simulador nem testes fiscais; nao reproduzir PII. A falta de relatorio individual nao impede codigo neutro, testes nem cenarios estimativos fundamentados. | Confirmacao especifica necessaria para regras com fonte/anexo/vigencia ilegivel ou nao consolidada, elegibilidade de beneficios, classificacao pautal/ICE, parametros novos de 2026 e resultados dourados por componente. |
|
||||
| B-01 | Preservar ambiente e provar apenas comandos efectivamente executados. | Worktree apresenta muitas alteracoes preexistentes/nao comprometidas; foram preservadas. PHP 8.5 permanece requisito do portal. OCR foi tentado com dependencias temporarias; originais nao foram alterados. | Sem reset/clean, migration operacional, activacao fiscal ou mudanca de infraestrutura nesta tranche. | Tests devem usar PHP 8.5.11 e DB descartavel; nao inferir QA a partir da base local. |
|
||||
|
||||
### Reconciliacao do resumo fiscal secundario localizado — 2026-10-04
|
||||
|
||||
Documento lido: `Informacao Publica/Legislacao - resumos/Reforma fiscal de 2026.docx`, SHA-256 `36666904ADB982AD578145E7B70244146C6414299B18F086042A5B4A6C2385B6`. Referencia primaria consultada no BR 248 previamente arquivado e no sumario oficial INM. Uma segunda copia local tem texto extraivel, mas hash e paginação diferem do exemplar arquivado.
|
||||
|
||||
| Tema | Alegacao secundária | Dispositivo primário cotejado | Resultado / estado |
|
||||
| --- | --- | --- | --- |
|
||||
| IVA vs ISPC | Revogacao dos regimes simplificados de IVA pode ser lida como eliminacao do ISPC. | Lei 9/2025 cria/actualiza ISPC; Lei 10/2025, arts. 3 e 53, altera/revoga disposicoes do CIVA e regula transicao do IVA. | Regimes distintos; divergencia resolvida. ISPC permanece sujeito a elegibilidade propria, nao basta limite de volume. |
|
||||
| Reembolso IVA | 150 dias e prescricao de 10 anos. | Lei 10/2025, art. 21, n.os 10 e 15 no texto extraido. | Correspondencia textual encontrada. Nao confundir credito/pedido com reconhecimento ou recebimento; regulamento/procedimento e estados de tesouraria continuam por validar. |
|
||||
| Dedutibilidade | Proporcao em casos de base reduzida e exclusoes. | Lei 10/2025, art. 18(6) e art. 19(2), este referente a operacoes do art. 17-A. | Confirmado apenas no escopo dos dispositivos citados; nao generalizar a todas as operacoes. |
|
||||
| Operacoes digitais IVA | Responsabilidade do adquirente nao residente/digital descrita de forma ampla. | Lei 10/2025, arts. 6(7), 23(2), 32 e 33. | Depende da natureza da operacao e do sujeito definido em cada artigo; intake nao deve aplicar uma regra universal. |
|
||||
| Estabelecimento estavel IRPC | Regra de 90 dias pode ser lida como aplicavel tambem a servicos digitais. | Lei 12/2025, art. 3(2)(c), exclui servicos digitais do teste de permanencia por prestacao de servicos; arts. 5(5)–(7) e 62(5) tratam rendimento digital em vias distintas. | Divergencia resolvida; nao importar 90 dias para servico digital por analogia. |
|
||||
| Mais-valias | Taxa de 32% referida para IRPC. | Lei 12/2025, art. 61-A; Lei 11/2025, art. 54-A do IRPS tem tabela propria. | 32% e regra textual IRPC; nao transportar para IRPS. Aplicacao por categoria e bases ainda requer teste e validacao. |
|
||||
| Isencoes em importacao e Decreto 52/2025 | Resumo indica caducidade de bens especificos e prazos/condicoes de reembolso especiais. | Lei 10/2025, anexos/dispositivos e Decreto 52/2025 exigem cotejo integral; texto primario legivel correspondente nao foi fechado nesta tranche. | Pendente. Sem regras de isencao nem previsao de reembolso implementadas a partir desta alegacao. |
|
||||
| Tema | Alegacao secundária | Dispositivo primário cotejado | Resultado / estado |
|
||||
| ---------------------------------------- | --------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| IVA vs ISPC | Revogacao dos regimes simplificados de IVA pode ser lida como eliminacao do ISPC. | Lei 9/2025 cria/actualiza ISPC; Lei 10/2025, arts. 3 e 53, altera/revoga disposicoes do CIVA e regula transicao do IVA. | Regimes distintos; divergencia resolvida. ISPC permanece sujeito a elegibilidade propria, nao basta limite de volume. |
|
||||
| Reembolso IVA | 150 dias e prescricao de 10 anos. | Lei 10/2025, art. 21, n.os 10 e 15 no texto extraido. | Correspondencia textual encontrada. Nao confundir credito/pedido com reconhecimento ou recebimento; regulamento/procedimento e estados de tesouraria continuam por validar. |
|
||||
| Dedutibilidade | Proporcao em casos de base reduzida e exclusoes. | Lei 10/2025, art. 18(6) e art. 19(2), este referente a operacoes do art. 17-A. | Confirmado apenas no escopo dos dispositivos citados; nao generalizar a todas as operacoes. |
|
||||
| Operacoes digitais IVA | Responsabilidade do adquirente nao residente/digital descrita de forma ampla. | Lei 10/2025, arts. 6(7), 23(2), 32 e 33. | Depende da natureza da operacao e do sujeito definido em cada artigo; intake nao deve aplicar uma regra universal. |
|
||||
| Estabelecimento estavel IRPC | Regra de 90 dias pode ser lida como aplicavel tambem a servicos digitais. | Lei 12/2025, art. 3(2)(c), exclui servicos digitais do teste de permanencia por prestacao de servicos; arts. 5(5)–(7) e 62(5) tratam rendimento digital em vias distintas. | Divergencia resolvida; nao importar 90 dias para servico digital por analogia. |
|
||||
| Mais-valias | Taxa de 32% referida para IRPC. | Lei 12/2025, art. 61-A; Lei 11/2025, art. 54-A do IRPS tem tabela propria. | 32% e regra textual IRPC; nao transportar para IRPS. Aplicacao por categoria e bases ainda requer teste e validacao. |
|
||||
| Isencoes em importacao e Decreto 52/2025 | Resumo indica caducidade de bens especificos e prazos/condicoes de reembolso especiais. | Lei 10/2025, anexos/dispositivos e Decreto 52/2025 exigem cotejo integral; texto primario legivel correspondente nao foi fechado nesta tranche. | Pendente. Sem regras de isencao nem previsao de reembolso implementadas a partir desta alegacao. |
|
||||
|
||||
### Inventario de fontes fiscais localizadas nesta revisao
|
||||
|
||||
| Ficheiro (fora do repositorio) | Tamanho / SHA-256 | Leitura nesta execucao |
|
||||
| --- | --- | --- |
|
||||
| `Legislacao/Lei 13_2016_Reimpressao Codigo do IVA.pdf` | 2.127.554 bytes / `b90dc7c14ece2a2e3784c62e8e5a47d54a5644c457baeb8b8053b92fd2a5710b` | 32 paginas, sem camada de texto; OCR nao produziu texto utilizavel. |
|
||||
| `Legislacao/RectificaçãoLei.13.2016_Republicação.Codigo.IVA.pdf` | 19.539.751 bytes / `ad675fef757016b730e859c834ab1a1602576260510912dda6f9d320393d415c` | 15 paginas, sem camada de texto; OCR nao produziu texto utilizavel. |
|
||||
| `Legislacao/alteracao CIVA 2023.pdf` | 708.707 bytes / `0c13f4b03dadea7f2dfe30694650078fa7b9de00ef84cb47389422d23a5271cd` | 18 paginas; texto extraivel, ainda nao reconciliado artigo a artigo. |
|
||||
| `Legislacao/Legislacao/Legislacao/Lei 22 2022 IVA.pdf` | 1.989.462 bytes / `5bd31ff1231659ce8d89e1260a9de3b067e69e4ace128b3fe0af60e2e928e6af` | 26 paginas; texto extraivel, cotejo oficial por fazer. |
|
||||
| `Legislacao/Lei 12.2025_IRPC.pdf` | 511.828 bytes / `3b67d10beaa3bf65d62a3d75fa01cc5a8181000d674e7b2ba62d06c60d449757` | 4 paginas; texto extraivel inclui Suplemento 1 do BR 248 e Lei 12/2025. |
|
||||
| `Legislacao/Legislacao/2009/Lei 4_2009 Código dos Benefícios Fiscais - versão consolidada.pdf` | 253.019 bytes / `61e3439426f1eab4c3eec91f8d4587baa2e71249c7d60bbb17c8d4427dcd0aa3` | 13 paginas; extraccao parcial com aviso de fonte tipografica. |
|
||||
| `Legislacao/Legislacao/a digitalizar/ISPC_Financas.pdf` | 363.424 bytes / `761502b64d65bfac3f9a6e1b23071a397c1c59a961bd15b27708dc01962ea1de` | 17 paginas; scan/camada de texto insuficiente. |
|
||||
| Ficheiro (fora do repositorio) | Tamanho / SHA-256 | Leitura nesta execucao |
|
||||
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- |
|
||||
| `Legislacao/Lei 13_2016_Reimpressao Codigo do IVA.pdf` | 2.127.554 bytes / `b90dc7c14ece2a2e3784c62e8e5a47d54a5644c457baeb8b8053b92fd2a5710b` | 32 paginas, sem camada de texto; OCR nao produziu texto utilizavel. |
|
||||
| `Legislacao/RectificaçãoLei.13.2016_Republicação.Codigo.IVA.pdf` | 19.539.751 bytes / `ad675fef757016b730e859c834ab1a1602576260510912dda6f9d320393d415c` | 15 paginas, sem camada de texto; OCR nao produziu texto utilizavel. |
|
||||
| `Legislacao/alteracao CIVA 2023.pdf` | 708.707 bytes / `0c13f4b03dadea7f2dfe30694650078fa7b9de00ef84cb47389422d23a5271cd` | 18 paginas; texto extraivel, ainda nao reconciliado artigo a artigo. |
|
||||
| `Legislacao/Legislacao/Legislacao/Lei 22 2022 IVA.pdf` | 1.989.462 bytes / `5bd31ff1231659ce8d89e1260a9de3b067e69e4ace128b3fe0af60e2e928e6af` | 26 paginas; texto extraivel, cotejo oficial por fazer. |
|
||||
| `Legislacao/Lei 12.2025_IRPC.pdf` | 511.828 bytes / `3b67d10beaa3bf65d62a3d75fa01cc5a8181000d674e7b2ba62d06c60d449757` | 4 paginas; texto extraivel inclui Suplemento 1 do BR 248 e Lei 12/2025. |
|
||||
| `Legislacao/Legislacao/2009/Lei 4_2009 Código dos Benefícios Fiscais - versão consolidada.pdf` | 253.019 bytes / `61e3439426f1eab4c3eec91f8d4587baa2e71249c7d60bbb17c8d4427dcd0aa3` | 13 paginas; extraccao parcial com aviso de fonte tipografica. |
|
||||
| `Legislacao/Legislacao/a digitalizar/ISPC_Financas.pdf` | 363.424 bytes / `761502b64d65bfac3f9a6e1b23071a397c1c59a961bd15b27708dc01962ea1de` | 17 paginas; scan/camada de texto insuficiente. |
|
||||
|
||||
O PDF de compromisso OCAM foi lido apenas para estabelecer o seu escopo (Modelo 20 empresarial, exercicio 2025); nomes, NUIT, codigos e outros dados pessoais foram omitidos. A extraccao dos restantes codigos legiveis e do resumo secundario permanece por reconciliar antes de se afirmar cobertura fiscal completa.
|
||||
|
||||
## REC001-REC016 - connected journey tranche (2026-10-04)
|
||||
|
||||
| REC | Current evidence and decision | State after this tranche | Next concrete work |
|
||||
| --- | --- | --- | --- |
|
||||
| REC001 | `SimulationFormDraft` already encrypts snapshots; `SimulationBusinessPlan` stores a versioned plan snapshot. Adapt existing draft rather than add a second dossier. | Partial: exploratory small-business draft now contains idea and plan stages; not yet one persistent canonical project linked to all modules. | Define canonical project relationship and cross-module ownership before linking records. |
|
||||
| REC002 | Guided sections already exist in `simulation-form.ts`. | Partial: the small-business exploratory path captures problem, customers, value proposition, sector/activity, location and declared market assumptions in the existing resumable draft. | Add structured market evidence and reusable operational detail. |
|
||||
| REC003 | Business plan and cashflow tools have independent tests and versioned snapshots. | Partial: financial plan is available in exploratory small-business path; fiscal rules remain separate and may be indeterminate. | Test full plan-to-simulator reconciliation and scenario history. |
|
||||
| REC004 | Existing plan supports user-edited financial assumptions. | Partial: a free-text milestone field accepts planned/actual notes and dates; it is not yet a structured plan-vs-actual register. | Add structured milestones, status transitions and cost reconciliation after canonical dossier ownership is settled. |
|
||||
| REC005 | Public procedures and DUAT guidance exist. | Partial: the location stage links to `/terra-duat` and `/como-investir`; no project-specific checklist is saved. | Add stage-specific checklist with source and applicability, without universal required documents. |
|
||||
| REC006 | Interest/CRM intake exists; SuiteCRM remains disabled. | Partial: internal intake is not official submission. | Record referrals and proof only when an actual supported channel exists. |
|
||||
| REC007 | Indicator schemas/data exist but source metadata/catalogue coverage is partial. | Partial. | Expose a small catalog with unit, period, geography, source and coverage metadata. |
|
||||
| REC008 | Territorial catalogue exists; completeness and comparative analytics are not established. | Partial. | Compare only verified coverage; preserve list view where coordinates are absent. |
|
||||
| REC009 | Several CSV exports exist; no common dossier export established. | Partial. | Export the project and its versioned plan once a canonical aggregate is defined. |
|
||||
| REC010 | Existing assistance/AI paths have controls; explanations and scope vary. | Partial. | Use rule-based, cited suggestions first; keep AI optional and reviewable. |
|
||||
| REC011 | Portal operations include status and audit data, but no complete journey funnel verified. | Partial. | Define cohort, denominator, declared vs verified outcomes and privacy rules. |
|
||||
| REC012 | Automated tests exist; no full connected pilot/browser/accessibility pass executed in this tranche. | Partial. | Test end-to-end journey, authorization, keyboard, narrow viewport and regressions. |
|
||||
| REC013 | Education tables/controllers exist. | Partial: the draft captures user-declared competency/team needs as text; it does not structure availability, gap, solution, cost and deadline or match a training provider. | Map available occupation/skill entities and build declared needs/actions without implying certification. |
|
||||
| REC014 | Education discovery and partner routes exist; no verified project-to-provider match. | Partial. | Match by explicit task, location, availability, cost and evidence. |
|
||||
| REC015 | Portal aggregates exist in some dashboards; safe public aggregate semantics need review. | Partial. | Document universe, method, periods and suppression/privacy controls. |
|
||||
| REC016 | Finance catalog/search exists but no complete product-level compatibility contract verified. | Partial: the draft accepts a financing/resource need and optional amount, but does not match products or establish pre-eligibility. | Show compatible, incompatible by documented criterion, or insufficient information; no approval promise. |
|
||||
| REC | Current evidence and decision | State after this tranche | Next concrete work |
|
||||
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| REC001 | `SimulationFormDraft` already encrypts snapshots; `SimulationBusinessPlan` stores a versioned plan snapshot. Adapt existing draft rather than add a second dossier. | Partial: exploratory small-business draft now contains idea and plan stages; not yet one persistent canonical project linked to all modules. | Define canonical project relationship and cross-module ownership before linking records. |
|
||||
| REC002 | Guided sections already exist in `simulation-form.ts`. | Partial: the small-business exploratory path captures problem, customers, value proposition, sector/activity, location and declared market assumptions in the existing resumable draft. | Add structured market evidence and reusable operational detail. |
|
||||
| REC003 | Business plan and cashflow tools have independent tests and versioned snapshots. | Partial: financial plan is available in exploratory small-business path; fiscal rules remain separate and may be indeterminate. | Test full plan-to-simulator reconciliation and scenario history. |
|
||||
| REC004 | Existing plan supports user-edited financial assumptions. | Partial: a free-text milestone field accepts planned/actual notes and dates; it is not yet a structured plan-vs-actual register. | Add structured milestones, status transitions and cost reconciliation after canonical dossier ownership is settled. |
|
||||
| REC005 | Public procedures and DUAT guidance exist. | Partial: the location stage links to `/terra-duat` and `/como-investir`; no project-specific checklist is saved. | Add stage-specific checklist with source and applicability, without universal required documents. |
|
||||
| REC006 | Interest/CRM intake exists; SuiteCRM remains disabled. | Partial: internal intake is not official submission. | Record referrals and proof only when an actual supported channel exists. |
|
||||
| REC007 | Indicator schemas/data exist but source metadata/catalogue coverage is partial. | Partial. | Expose a small catalog with unit, period, geography, source and coverage metadata. |
|
||||
| REC008 | Territorial catalogue exists; completeness and comparative analytics are not established. | Partial. | Compare only verified coverage; preserve list view where coordinates are absent. |
|
||||
| REC009 | Several CSV exports exist; no common dossier export established. | Partial. | Export the project and its versioned plan once a canonical aggregate is defined. |
|
||||
| REC010 | Existing assistance/AI paths have controls; explanations and scope vary. | Partial. | Use rule-based, cited suggestions first; keep AI optional and reviewable. |
|
||||
| REC011 | Portal operations include status and audit data, but no complete journey funnel verified. | Partial. | Define cohort, denominator, declared vs verified outcomes and privacy rules. |
|
||||
| REC012 | Automated tests exist; no full connected pilot/browser/accessibility pass executed in this tranche. | Partial. | Test end-to-end journey, authorization, keyboard, narrow viewport and regressions. |
|
||||
| REC013 | Education tables/controllers exist. | Partial: the draft captures user-declared competency/team needs as text; it does not structure availability, gap, solution, cost and deadline or match a training provider. | Map available occupation/skill entities and build declared needs/actions without implying certification. |
|
||||
| REC014 | Education discovery and partner routes exist; no verified project-to-provider match. | Partial. | Match by explicit task, location, availability, cost and evidence. |
|
||||
| REC015 | Portal aggregates exist in some dashboards; safe public aggregate semantics need review. | Partial. | Document universe, method, periods and suppression/privacy controls. |
|
||||
| REC016 | Finance catalog/search exists but no complete product-level compatibility contract verified. | Partial: the draft accepts a financing/resource need and optional amount, but does not match products or establish pre-eligibility. | Show compatible, incompatible by documented criterion, or insufficient information; no approval promise. |
|
||||
|
||||
Pilot: synthetic, unseeded small-business idea using the public `/Simulador` path, `journey_type=small_business` and `purpose=simulate_only`. It captures optional idea/market responses, existing sector and territorial selections, competency/resource/milestone notes and the versioned financial plan in the existing draft. It can save locally and, when signed in, use the existing server draft. Market data, competency matches and financing products are not verified or matched. The pilot is not a public business listing, formal filing, fiscal determination, financing pre-approval or official submission. No operational fixture was created.
|
||||
|
||||
### REC001–REC016 follow-up — 2026-10-04
|
||||
|
||||
| Area | Delivered in this tranche | Remaining gate |
|
||||
| --- | --- | --- |
|
||||
| Dossier, idea and finance | Reused encrypted `SimulationFormDraft` and existing versioned business-plan snapshot; added dossier CSV with market claim/source, location, finance assumptions, declared needs, team and execution rows. | Cross-module canonical project identity and plan snapshot reconciliation remain future work. |
|
||||
| Team and execution | New structured planner rows capture competency, availability/gap, planned response, estimated cost/date, milestone status and planned/actual amounts. Server enforces JSON shape, state whitelist, 100-row limit and nonnegative amounts. | Matching to education providers/certified skills and reconciling realized amounts to accounting/plan remain pending. |
|
||||
| Market origin and indicators | UI and dossier explicitly say “Informação declarada, não verificada”; source/date optional. Exports distinguish declared assumptions and estimates from selected territorial catalogue values. | Official indicator catalogue with source, period, geography, unit and coverage is not present as a complete connected dataset. |
|
||||
| Financing | Missing product catalogue and criteria produce “Informação insuficiente”; no pre-eligibility result is invented. | Documented product feed, current sources/eligibility criteria and tests required before indicative matching. |
|
||||
| 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. |
|
||||
| Area | Delivered in this tranche | Remaining gate |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Dossier, idea and finance | Reused encrypted `SimulationFormDraft` and existing versioned business-plan snapshot; added dossier CSV with market claim/source, location, finance assumptions, declared needs, team and execution rows. | Cross-module canonical project identity and plan snapshot reconciliation remain future work. |
|
||||
| Team and execution | New structured planner rows capture competency, availability/gap, planned response, estimated cost/date, milestone status and planned/actual amounts. Server enforces JSON shape, state whitelist, 100-row limit and nonnegative amounts. | Matching to education providers/certified skills and reconciling realized amounts to accounting/plan remain pending. |
|
||||
| Market origin and indicators | UI and dossier explicitly say “Informação declarada, não verificada”; source/date optional. Exports distinguish declared assumptions and estimates from selected territorial catalogue values. | Official indicator catalogue with source, period, geography, unit and coverage is not present as a complete connected dataset. |
|
||||
| Financing | Missing product catalogue and criteria produce “Informação insuficiente”; no pre-eligibility result is invented. | Documented product feed, current sources/eligibility criteria and tests required before indicative matching. |
|
||||
| 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.
|
||||
|
||||
@@ -237,34 +237,34 @@ Esta secção é a reconciliação corrente desta matriz para Documentos e Formu
|
||||
|
||||
### 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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
| 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.
|
||||
|
||||
@@ -272,24 +272,24 @@ O status `published` já está activo, mas “Promotor em validação” e a dat
|
||||
|
||||
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. |
|
||||
| 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
|
||||
|
||||
|
||||
@@ -21,17 +21,17 @@ Backup/restauro verificado antes desta correcção: `storage/app/private/zones-b
|
||||
|
||||
## Trabalho que continua por terminar
|
||||
|
||||
| Área | Lacuna confirmada nos relatórios/código | Trabalho necessário |
|
||||
| --- | --- | --- |
|
||||
| GIS completo | Limites geográficos versionados, transformação/validação de geometrias, endpoints resolve/near/bbox e cobertura espacial não entregues integralmente | Implementar serviços e contratos; carregar geometrias apenas quando fornecidas, sem inventar coordenadas |
|
||||
| Integração territorial transversal | A nova associação é principal; os consumidores educativos, organizações e restantes formulários não usam ainda todos o mesmo selector; relações múltiplas legadas continuam separadas | Consolidar consumidores e testar cada jornada, incluindo históricos e remoção de referências |
|
||||
| Importação territorial | CSV auditado, mas não existe ainda o workflow integral de batches/rejeições/aprovação/carga; PDF histórico tem colunas ambíguas | Terminar pipeline, excepções documentadas e reconciliação dos códigos; não tratar aprovação como reparação automática de dados contraditórios |
|
||||
| IA | Base lexical/determinística; não há modelo generativo, embeddings, reranker ou benchmark generativo | Implementar RAG e avaliação com corpus autorizado; documentar recursos e fornecedor efectivamente usados |
|
||||
| Mercado de Negócios | Preparação/submissão privada e objectos tipados; publicação, revisão completa, sala documental segura e integrações não concluídas | Implementar os estados/jornadas restantes, acesso documental, scanner, retenção e testes |
|
||||
| ZEE/ZFI | Núcleo e filtros existentes; cadastro local de zonas continua vazio; modelo integral de actividades, benefícios, obrigações, lotes e indicadores não entregue | Completar domínio e importar conteúdos disponíveis com origem; não substituir dados ausentes por zonas fictícias |
|
||||
| Educação | Há implementação parcial; metodologia, consumidores, integração e piloto não estão integralmente entregues | Confrontar decisões posteriores com o relatório histórico, executar lacunas técnicas e testar o fluxo completo |
|
||||
| CRM/integrações | A integração externa real com SuiteCRM e a consolidação da outra máquina não foram executadas | Validar ligação quando disponível; a consolidação da outra máquina foi expressamente adiada pelo utilizador |
|
||||
| Operação/QA | Testes automáticos e alguns percursos de navegador não cobrem todos os prompts; carga, acessibilidade assistiva e produção não demonstradas | Matriz de aceitação por jornada, testes com dados reais, ensaios operacionais e disponibilização no destino configurado |
|
||||
| Área | Lacuna confirmada nos relatórios/código | Trabalho necessário |
|
||||
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| GIS completo | Limites geográficos versionados, transformação/validação de geometrias, endpoints resolve/near/bbox e cobertura espacial não entregues integralmente | Implementar serviços e contratos; carregar geometrias apenas quando fornecidas, sem inventar coordenadas |
|
||||
| Integração territorial transversal | A nova associação é principal; os consumidores educativos, organizações e restantes formulários não usam ainda todos o mesmo selector; relações múltiplas legadas continuam separadas | Consolidar consumidores e testar cada jornada, incluindo históricos e remoção de referências |
|
||||
| Importação territorial | CSV auditado, mas não existe ainda o workflow integral de batches/rejeições/aprovação/carga; PDF histórico tem colunas ambíguas | Terminar pipeline, excepções documentadas e reconciliação dos códigos; não tratar aprovação como reparação automática de dados contraditórios |
|
||||
| IA | Base lexical/determinística; não há modelo generativo, embeddings, reranker ou benchmark generativo | Implementar RAG e avaliação com corpus autorizado; documentar recursos e fornecedor efectivamente usados |
|
||||
| Mercado de Negócios | Preparação/submissão privada e objectos tipados; publicação, revisão completa, sala documental segura e integrações não concluídas | Implementar os estados/jornadas restantes, acesso documental, scanner, retenção e testes |
|
||||
| ZEE/ZFI | Núcleo e filtros existentes; cadastro local de zonas continua vazio; modelo integral de actividades, benefícios, obrigações, lotes e indicadores não entregue | Completar domínio e importar conteúdos disponíveis com origem; não substituir dados ausentes por zonas fictícias |
|
||||
| Educação | Há implementação parcial; metodologia, consumidores, integração e piloto não estão integralmente entregues | Confrontar decisões posteriores com o relatório histórico, executar lacunas técnicas e testar o fluxo completo |
|
||||
| CRM/integrações | A integração externa real com SuiteCRM e a consolidação da outra máquina não foram executadas | Validar ligação quando disponível; a consolidação da outra máquina foi expressamente adiada pelo utilizador |
|
||||
| Operação/QA | Testes automáticos e alguns percursos de navegador não cobrem todos os prompts; carga, acessibilidade assistiva e produção não demonstradas | Matriz de aceitação por jornada, testes com dados reais, ensaios operacionais e disponibilização no destino configurado |
|
||||
|
||||
Estas lacunas não são todas bloqueios de autorização. A maior parte corresponde a implementação que ficou por fazer. Dependências de dados, de ligações externas e de recursos de execução devem ser identificadas separadamente; não servem para suspender trabalho independente já autorizado.
|
||||
|
||||
|
||||
@@ -4,21 +4,21 @@ Data: 06-10-2026. Inventário limitado aos anexos enumerados no manifesto local
|
||||
|
||||
## 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 |
|
||||
| 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 |
|
||||
| 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.
|
||||
|
||||
@@ -28,12 +28,12 @@ Os 10 itens inacessíveis são as sete fontes antigas referidas por caminho rela
|
||||
|
||||
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 |
|
||||
| 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`.
|
||||
|
||||
@@ -51,17 +51,17 @@ As 25 referências cobrem: catálogos da AT e INSS; cadeia do CIVA (Leis 3/2012,
|
||||
|
||||
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 |
|
||||
| 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.
|
||||
|
||||
|
||||
@@ -10,12 +10,12 @@ Foram importados quatro documentos para armazenamento privado. Todos permanecem
|
||||
|
||||
## Inventário e decisões pendentes por documento
|
||||
|
||||
| Documento | Proveniência e estado | Relações | Decisão pendente antes de publicar |
|
||||
|---|---|---|---|
|
||||
| Lei n.º 22/2022, republicação do Código do IVA | PDF comparado byte a byte com o exemplar da AT; origem oficial confirmada. Estado jurídico registado como alterado, não como consolidação integral actual. | Evidência documental do simulador fiscal. | Gestor deve rever a possibilidade de divulgação da composição PDF e confirmar a versão/alcance editorial. Direitos: `unconfirmed`; classificação: `internal`. |
|
||||
| BR n.º 248/2025, Suplemento n.º 1 | PDF comparado byte a byte com endpoint oficial da AT. Reúne vários diplomas; estado jurídico agregado desconhecido. | Evidência documental fiscal, sem afirmar que cada tema cria obrigação ou regra activa. | Avaliar direitos da composição e identificar os diplomas e vigências individualmente antes de apresentar como referência consolidada. |
|
||||
| BR n.º 89, I Série, de 08/06/2017 | A ficha do INM identifica o aviso de rectificação da Lei n.º 13/2016 na p. 569; o binário local não foi comparado com um download oficial. Proveniência apenas identificada. | Evidência fiscal; efeito temporal continua por confirmar. | Obter o PDF/binário oficial, comparar hash e confirmar autenticidade, anexos e efeito da rectificação. Direitos continuam por confirmar. |
|
||||
| Codificador da Divisão Político-Administrativa do INE, edição de 2012 | Os bytes locais (67.067 bytes) coincidem com o PDF servido pelo URL oficial do INE. Confirma a origem do ficheiro, não a actualidade dos códigos nem os direitos de reutilização da composição. | Sem associação automática às relações territoriais actuais. | Rever reutilização/publicação e utilidade histórica. Não usar esta edição para atribuir localizações actuais sem reconciliação documentada. |
|
||||
| Documento | Proveniência e estado | Relações | Decisão pendente antes de publicar |
|
||||
| --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Lei n.º 22/2022, republicação do Código do IVA | PDF comparado byte a byte com o exemplar da AT; origem oficial confirmada. Estado jurídico registado como alterado, não como consolidação integral actual. | Evidência documental do simulador fiscal. | Gestor deve rever a possibilidade de divulgação da composição PDF e confirmar a versão/alcance editorial. Direitos: `unconfirmed`; classificação: `internal`. |
|
||||
| BR n.º 248/2025, Suplemento n.º 1 | PDF comparado byte a byte com endpoint oficial da AT. Reúne vários diplomas; estado jurídico agregado desconhecido. | Evidência documental fiscal, sem afirmar que cada tema cria obrigação ou regra activa. | Avaliar direitos da composição e identificar os diplomas e vigências individualmente antes de apresentar como referência consolidada. |
|
||||
| BR n.º 89, I Série, de 08/06/2017 | A ficha do INM identifica o aviso de rectificação da Lei n.º 13/2016 na p. 569; o binário local não foi comparado com um download oficial. Proveniência apenas identificada. | Evidência fiscal; efeito temporal continua por confirmar. | Obter o PDF/binário oficial, comparar hash e confirmar autenticidade, anexos e efeito da rectificação. Direitos continuam por confirmar. |
|
||||
| Codificador da Divisão Político-Administrativa do INE, edição de 2012 | Os bytes locais (67.067 bytes) coincidem com o PDF servido pelo URL oficial do INE. Confirma a origem do ficheiro, não a actualidade dos códigos nem os direitos de reutilização da composição. | Sem associação automática às relações territoriais actuais. | Rever reutilização/publicação e utilidade histórica. Não usar esta edição para atribuir localizações actuais sem reconciliação documentada. |
|
||||
|
||||
Nos quatro registos, a análise antimalware foi concluída sem ameaça e os hashes analisados coincidem com os ficheiros guardados. Este resultado é uma verificação técnica, não prova de direitos de publicação, exactidão jurídica, vigência ou actualidade.
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ Uma consulta apenas de leitura à base configurada como `investemoz`, juntando `
|
||||
|
||||
Os três ficheiros abaixo estão em `docs/investmoz/simulations/source-archive/`. O inventário registou nome, tamanho e hash; esta tarefa **não confirmou o texto integral, fonte oficial, vigência, idioma, direitos de reprodução, âmbito ou aptidão para publicação**. Permanecem no arquivo original e não foram importados para o catálogo.
|
||||
|
||||
| Ficheiro | Bytes | SHA-256 | Situação para a biblioteca |
|
||||
|---|---:|---|---|
|
||||
| Ficheiro | Bytes | SHA-256 | Situação para a biblioteca |
|
||||
| ----------------------------------------------------- | --------: | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- |
|
||||
| `Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf` | 1.834.284 | `EA1B0DC6F5CF6E595F6D9A19E7042F398999E0087CAEF1785F6E8FAC571B5F53` | Candidato a legislação; fonte canónica, artigos, vigência consolidada e direitos por rever. |
|
||||
| `BR-89-I-Serie-2017-06-08.pdf` | 9.456.358 | `11F0CAB15EB6E51010952203FFA24FDC533A24FD642E19525FE4F46B17A1F5E5` | Boletim da República; acto/artigos e relação com diplomas por identificar. |
|
||||
| `BR-248-I-Suplemento-1-2025.pdf` | 440.018 | `CCF2A8DEECA820F2DD076B4B2141B1EFE3F6A806568C3A587CA59A8E92F4C251` | Boletim da República; acto/artigos e relação com diplomas por identificar. |
|
||||
| `BR-89-I-Serie-2017-06-08.pdf` | 9.456.358 | `11F0CAB15EB6E51010952203FFA24FDC533A24FD642E19525FE4F46B17A1F5E5` | Boletim da República; acto/artigos e relação com diplomas por identificar. |
|
||||
| `BR-248-I-Suplemento-1-2025.pdf` | 440.018 | `CCF2A8DEECA820F2DD076B4B2141B1EFE3F6A806568C3A587CA59A8E92F4C251` | Boletim da República; acto/artigos e relação com diplomas por identificar. |
|
||||
|
||||
## Exclusões e limites do inventário
|
||||
|
||||
|
||||
@@ -12,14 +12,14 @@ A migração aditiva `2026_10_06_200000_add_document_requirement_evidence_links`
|
||||
|
||||
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 |
|
||||
| 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`.
|
||||
|
||||
@@ -27,12 +27,12 @@ Também existe a chave `fiscal.iva.lei_22_2022` para a Lei n.º 22/2022, art. 17
|
||||
|
||||
## 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 |
|
||||
| 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.
|
||||
|
||||
|
||||
@@ -4,17 +4,17 @@ Estado: **validado tecnicamente para desenvolvimento local**, por revisão do re
|
||||
|
||||
## Decisões adoptadas
|
||||
|
||||
| ID | Decisão e fundamento confirmado | Execução e limites |
|
||||
| --- | --- | --- |
|
||||
| D-EDU-01 / B02 | O catálogo público usa `opportunities.id` bigint; os domínios novos usam UUID. Preservar ambos, com uma associação explícita, sem converter IDs existentes. | `education_investment_proposals` tem UUID e referências a `labour_demands` e `opportunities`. Uma ficha inicial por procura; restrições únicas e bloqueio transaccional evitam duplicação. `education_opportunities` conserva a semântica de oferta calendarizada. |
|
||||
| D-EDU-02 / B03 | Reutilizar sessão, email verificado e `manage-portal` para preparação interna. O registo público não concede acesso. | As rotas novas estão no grupo de gestão, atrás de `EducationEnabled`. Não criar perfis institucionais ou OAuth/OIDC sem mandatos/issuer reais. A revisão por outro gestor continua distinta de validação oficial. |
|
||||
| D-EDU-03 / B06 | Vagas formativas não equivalem a trabalhadores disponíveis. Fontes desconhecidas não equivalem a zero. A versão revista da procura é requisito para preparar uma proposta. | Comparação existente preservada; proposta referencia a versão revista e vigente. Alteração de versão produz conflito na criação e aviso na consulta da ficha anterior. Nenhuma soma de vagas é apresentada como défice laboral. |
|
||||
| D-EDU-04 / B01 | A preparação de uma resposta pode ocorrer antes da decisão institucional; a publicação exige evidência que ainda não foi fornecida. | Criar apenas rascunhos, com fonte, riscos e intervenção obrigatórios; nenhum montante, instituição, capacidade ou projecto é inventado. A gestão geral também rejeita publicação de fichas educativas. Não chamar este estado de lacuna validada. |
|
||||
| D-EDU-05 / B04 | Reutilizar a identidade do catálogo e o fluxo de interesse/CRM existente. Não duplicar investidores, contactos ou leads. | Ficha aponta para a oportunidade canónica. O encaminhamento educativo externo ainda não é activado: `suitecrm:probe` devolveu `disabled`; sem CRM homologado não declarar sincronização concluída. |
|
||||
| D-EDU-06 / B05 | Nesta etapa não recolher beneficiários, dados académicos individuais ou localizações pessoais. | Ficha usa necessidades agregadas; campos de texto devem conter fontes e responsáveis institucionais, não listas de pessoas. Auditoria partilhada não replica o texto das fontes, riscos ou planos. |
|
||||
| D-EDU-07 / B07 | Correspondência por regras não é IA. Não escolher fornecedor nem enviar dados a um modelo sem configuração e critérios aprovados. | IA permanece por configurar; sem inferência externa, decisão automática ou alegação de precisão. |
|
||||
| D-EDU-08 / B08 | Indicadores internos devem medir apenas o que os dados sustentam. | Contagens de declarações, procura por rever e fontes expiradas, rotuladas como globais e sem efeitos dos filtros. Sem metas fictícias, taxas de emprego ou agregação indevida de trabalhadores. |
|
||||
| D-EDU-09 | Alteração aditiva e reversão sem perda. | Nova migration não altera tabelas existentes; rollback recusa apagar fichas existentes. Reversão operacional: desactivar `EDUCATION_ENABLED`, preservar as tabelas e reconciliar antes de qualquer restauro. |
|
||||
| ID | Decisão e fundamento confirmado | Execução e limites |
|
||||
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| D-EDU-01 / B02 | O catálogo público usa `opportunities.id` bigint; os domínios novos usam UUID. Preservar ambos, com uma associação explícita, sem converter IDs existentes. | `education_investment_proposals` tem UUID e referências a `labour_demands` e `opportunities`. Uma ficha inicial por procura; restrições únicas e bloqueio transaccional evitam duplicação. `education_opportunities` conserva a semântica de oferta calendarizada. |
|
||||
| D-EDU-02 / B03 | Reutilizar sessão, email verificado e `manage-portal` para preparação interna. O registo público não concede acesso. | As rotas novas estão no grupo de gestão, atrás de `EducationEnabled`. Não criar perfis institucionais ou OAuth/OIDC sem mandatos/issuer reais. A revisão por outro gestor continua distinta de validação oficial. |
|
||||
| D-EDU-03 / B06 | Vagas formativas não equivalem a trabalhadores disponíveis. Fontes desconhecidas não equivalem a zero. A versão revista da procura é requisito para preparar uma proposta. | Comparação existente preservada; proposta referencia a versão revista e vigente. Alteração de versão produz conflito na criação e aviso na consulta da ficha anterior. Nenhuma soma de vagas é apresentada como défice laboral. |
|
||||
| D-EDU-04 / B01 | A preparação de uma resposta pode ocorrer antes da decisão institucional; a publicação exige evidência que ainda não foi fornecida. | Criar apenas rascunhos, com fonte, riscos e intervenção obrigatórios; nenhum montante, instituição, capacidade ou projecto é inventado. A gestão geral também rejeita publicação de fichas educativas. Não chamar este estado de lacuna validada. |
|
||||
| D-EDU-05 / B04 | Reutilizar a identidade do catálogo e o fluxo de interesse/CRM existente. Não duplicar investidores, contactos ou leads. | Ficha aponta para a oportunidade canónica. O encaminhamento educativo externo ainda não é activado: `suitecrm:probe` devolveu `disabled`; sem CRM homologado não declarar sincronização concluída. |
|
||||
| D-EDU-06 / B05 | Nesta etapa não recolher beneficiários, dados académicos individuais ou localizações pessoais. | Ficha usa necessidades agregadas; campos de texto devem conter fontes e responsáveis institucionais, não listas de pessoas. Auditoria partilhada não replica o texto das fontes, riscos ou planos. |
|
||||
| D-EDU-07 / B07 | Correspondência por regras não é IA. Não escolher fornecedor nem enviar dados a um modelo sem configuração e critérios aprovados. | IA permanece por configurar; sem inferência externa, decisão automática ou alegação de precisão. |
|
||||
| D-EDU-08 / B08 | Indicadores internos devem medir apenas o que os dados sustentam. | Contagens de declarações, procura por rever e fontes expiradas, rotuladas como globais e sem efeitos dos filtros. Sem metas fictícias, taxas de emprego ou agregação indevida de trabalhadores. |
|
||||
| D-EDU-09 | Alteração aditiva e reversão sem perda. | Nova migration não altera tabelas existentes; rollback recusa apagar fichas existentes. Reversão operacional: desactivar `EDUCATION_ENABLED`, preservar as tabelas e reconciliar antes de qualquer restauro. |
|
||||
|
||||
## Contratos preservados e análise de impacto
|
||||
|
||||
|
||||
@@ -23,69 +23,69 @@ As rotas continuam internas, sob `/management/education`, com sessão, verifica
|
||||
|
||||
“Parcial” significa código existente com limitações concretas; nenhum RF abaixo foi declarado integralmente cumprido. Os critérios completos mantêm-se em `requirements.csv`.
|
||||
|
||||
| RF-EDU | Estado funcional | Evidência e trabalho por concluir |
|
||||
| --- | --- | --- |
|
||||
| 001 | Parcial | Cadastro de código/nome; faltam taxonomia, sinónimos, níveis, fontes e revisão de versões. B01/B06. |
|
||||
| 002 | Parcial | Procura por projecto, ocupação, competência principal, território e período; faltam múltiplas competências, contexto produtivo completo e mandatos. B01/B03. |
|
||||
| 003 | Parcial | Instituições, cursos e capacidade declarada; faltam cobertura, documentação e vínculo institucional verificado. B01/B03. |
|
||||
| 004 | Parcial | Calculador e correspondência determinística; faltam disponibilidade comprovada de trabalhadores e alocação sem dupla contagem. B06. |
|
||||
| 005 | Bloqueado | Confirmar extensão do catálogo canónico; `education_opportunities` representa oferta calendarizada, não investimento educativo. B02. |
|
||||
| 006 | Parcial | Navegação interna e análise por procura; falta navegação projecto/cluster/corredor com relações confirmadas. B01/B02. |
|
||||
| 007 | Bloqueado | Não atribuir acreditação por estado de cadastro; faltam autoridade, evidência e mandato. B01/B03. |
|
||||
| 008 | Bloqueado | Fluxo geral de interesse existe no portal; associação educativa depende de oportunidade canónica aprovada. B02/B05. |
|
||||
| 009 | Parcial | Empregador declarado como texto; falta identidade institucional e distinção entre estimativa e compromisso. B01/B03. |
|
||||
| 010 | Bloqueado | Plano formativo permanece proposta; exige lacuna validada e responsáveis. B01/B02/B06. |
|
||||
| 011 | Parcial | Critérios apresentados: ocupação, competência, qualificação textual, território, validade e conclusão; faltam equivalências e revisão da correspondência. B06. |
|
||||
| 012 | Fase 2 | Resultados educativos fora da execução actual; requerem tratamento autorizado. B05. |
|
||||
| 013 | Fase 2 | Bolsas e campanhas permanecem no roteiro. B05. |
|
||||
| 014 | Bloqueado | Sem serviço de IA Education activado; fornecedor/modelo, fontes e métricas pendentes. B07. |
|
||||
| 015 | Parcial | Listagens paginadas; faltam pesquisa, filtros e painel reconciliado. B01/B06/B08. |
|
||||
| 016 | Bloqueado | Rotas web não equivalem à API proposta; faltam identidade, contratos e eventos aprovados. B03. |
|
||||
| 017 | Parcial | Cadastro mínimo de ocupações; faltam perfis, relações com competências e níveis validados. B06. |
|
||||
| 018 | Parcial | Datas e território declarados; sem previsão, mobilidade ou ligação contextual completa. B01/B06. |
|
||||
| 019 | Parcial | Vagas por curso/período; faltam recursos partilhados, capacidade instalada e utilização. B06. |
|
||||
| 020 | Bloqueado | Revisão de declarações é interna; não existe ciclo institucional de validação, encerramento e reabertura da lacuna. B01/B03/B06. |
|
||||
| 021 | Bloqueado | CRM geral reutilizável; vínculo educativo e sincronização dependem de B02/B04/B05. |
|
||||
| 022 | Bloqueado | Necessidades de equipamento/instalações dependem da resposta formativa aprovada. B02/B06. |
|
||||
| 023 | Bloqueado | Necessidades de formadores dependem da metodologia; dados individuais apenas na fase autorizada. B05/B06. |
|
||||
| 024 | Bloqueado | Estimativas e sustentabilidade dependem de fontes e modelo de oportunidade aprovado. B02/B06. |
|
||||
| 025 | Fase 2 | Parcerias no roteiro; responsabilidades e contratos por validar. |
|
||||
| 026 | Fase 2 | Estágios e aprendizagem no roteiro; sem contratação automática. |
|
||||
| 027 | Fase 2 | Turmas e beneficiários não recolhidos. B05. |
|
||||
| 028 | Fases 2/3 | Conclusão e certificação externa dependem de fontes competentes; portal não certifica. |
|
||||
| 029 | Fase 2 | Inserção e absorção exigem evidência autorizada e confirmação do empregador. B05. |
|
||||
| 030 | Fase 2 | Retenção exige períodos, seguimento e base de tratamento definidos. B05. |
|
||||
| 031 | Bloqueado | Referência territorial existente; mapa/relatórios Education requerem fontes geográficas e divulgação autorizadas. B01/B05/B06. |
|
||||
| 032 | Parcial | Gate de gestão, revisão independente e auditoria partilhada; faltam mandatos e matriz de permissões institucionais. B03. |
|
||||
| 033 | Parcial | Validação de datas, fontes, referências e concorrência; migração de fontes reais, reconciliação e retenção pendentes. B05/B06/B08. |
|
||||
| RF-EDU | Estado funcional | Evidência e trabalho por concluir |
|
||||
| ------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 001 | Parcial | Cadastro de código/nome; faltam taxonomia, sinónimos, níveis, fontes e revisão de versões. B01/B06. |
|
||||
| 002 | Parcial | Procura por projecto, ocupação, competência principal, território e período; faltam múltiplas competências, contexto produtivo completo e mandatos. B01/B03. |
|
||||
| 003 | Parcial | Instituições, cursos e capacidade declarada; faltam cobertura, documentação e vínculo institucional verificado. B01/B03. |
|
||||
| 004 | Parcial | Calculador e correspondência determinística; faltam disponibilidade comprovada de trabalhadores e alocação sem dupla contagem. B06. |
|
||||
| 005 | Bloqueado | Confirmar extensão do catálogo canónico; `education_opportunities` representa oferta calendarizada, não investimento educativo. B02. |
|
||||
| 006 | Parcial | Navegação interna e análise por procura; falta navegação projecto/cluster/corredor com relações confirmadas. B01/B02. |
|
||||
| 007 | Bloqueado | Não atribuir acreditação por estado de cadastro; faltam autoridade, evidência e mandato. B01/B03. |
|
||||
| 008 | Bloqueado | Fluxo geral de interesse existe no portal; associação educativa depende de oportunidade canónica aprovada. B02/B05. |
|
||||
| 009 | Parcial | Empregador declarado como texto; falta identidade institucional e distinção entre estimativa e compromisso. B01/B03. |
|
||||
| 010 | Bloqueado | Plano formativo permanece proposta; exige lacuna validada e responsáveis. B01/B02/B06. |
|
||||
| 011 | Parcial | Critérios apresentados: ocupação, competência, qualificação textual, território, validade e conclusão; faltam equivalências e revisão da correspondência. B06. |
|
||||
| 012 | Fase 2 | Resultados educativos fora da execução actual; requerem tratamento autorizado. B05. |
|
||||
| 013 | Fase 2 | Bolsas e campanhas permanecem no roteiro. B05. |
|
||||
| 014 | Bloqueado | Sem serviço de IA Education activado; fornecedor/modelo, fontes e métricas pendentes. B07. |
|
||||
| 015 | Parcial | Listagens paginadas; faltam pesquisa, filtros e painel reconciliado. B01/B06/B08. |
|
||||
| 016 | Bloqueado | Rotas web não equivalem à API proposta; faltam identidade, contratos e eventos aprovados. B03. |
|
||||
| 017 | Parcial | Cadastro mínimo de ocupações; faltam perfis, relações com competências e níveis validados. B06. |
|
||||
| 018 | Parcial | Datas e território declarados; sem previsão, mobilidade ou ligação contextual completa. B01/B06. |
|
||||
| 019 | Parcial | Vagas por curso/período; faltam recursos partilhados, capacidade instalada e utilização. B06. |
|
||||
| 020 | Bloqueado | Revisão de declarações é interna; não existe ciclo institucional de validação, encerramento e reabertura da lacuna. B01/B03/B06. |
|
||||
| 021 | Bloqueado | CRM geral reutilizável; vínculo educativo e sincronização dependem de B02/B04/B05. |
|
||||
| 022 | Bloqueado | Necessidades de equipamento/instalações dependem da resposta formativa aprovada. B02/B06. |
|
||||
| 023 | Bloqueado | Necessidades de formadores dependem da metodologia; dados individuais apenas na fase autorizada. B05/B06. |
|
||||
| 024 | Bloqueado | Estimativas e sustentabilidade dependem de fontes e modelo de oportunidade aprovado. B02/B06. |
|
||||
| 025 | Fase 2 | Parcerias no roteiro; responsabilidades e contratos por validar. |
|
||||
| 026 | Fase 2 | Estágios e aprendizagem no roteiro; sem contratação automática. |
|
||||
| 027 | Fase 2 | Turmas e beneficiários não recolhidos. B05. |
|
||||
| 028 | Fases 2/3 | Conclusão e certificação externa dependem de fontes competentes; portal não certifica. |
|
||||
| 029 | Fase 2 | Inserção e absorção exigem evidência autorizada e confirmação do empregador. B05. |
|
||||
| 030 | Fase 2 | Retenção exige períodos, seguimento e base de tratamento definidos. B05. |
|
||||
| 031 | Bloqueado | Referência territorial existente; mapa/relatórios Education requerem fontes geográficas e divulgação autorizadas. B01/B05/B06. |
|
||||
| 032 | Parcial | Gate de gestão, revisão independente e auditoria partilhada; faltam mandatos e matriz de permissões institucionais. B03. |
|
||||
| 033 | Parcial | Validação de datas, fontes, referências e concorrência; migração de fontes reais, reconciliação e retenção pendentes. B05/B06/B08. |
|
||||
|
||||
## 3. Validação local
|
||||
|
||||
Ambiente de teste novo e isolado: PostgreSQL/PostGIS local, porta 55440, base `investmoz_education_test`; PHP 8.5.11 com driver PostgreSQL. Não foi usada a base da aplicação para migrations ou testes. Não foram inseridos dados institucionais reais.
|
||||
|
||||
| Verificação | Evidência |
|
||||
| --- | --- |
|
||||
| Migrations | Aplicadas com sucesso na base temporária inicialmente vazia. |
|
||||
| PHPUnit | 107 testes, 884 verificações, todos aprovados. Inclui 9 testes Education. |
|
||||
| Exemplos e consistência documental | 11 testes Node aprovados; não equivalem a conformidade funcional integral. |
|
||||
| TypeScript | `tsc --noEmit` aprovado. |
|
||||
| Build | `npm run build` aprovado. |
|
||||
| Análise estática | PHPStan nível 7, zero erros nos seis ficheiros Education/middleware analisados. |
|
||||
| Formatação PHP | Pint aprovado nos cinco ficheiros PHP alterados. |
|
||||
| Confirmação final Education | 9 testes, 136 verificações aprovadas após a última correcção do controller. |
|
||||
| Verificação | Evidência |
|
||||
| ---------------------------------- | ------------------------------------------------------------------------------- |
|
||||
| Migrations | Aplicadas com sucesso na base temporária inicialmente vazia. |
|
||||
| PHPUnit | 107 testes, 884 verificações, todos aprovados. Inclui 9 testes Education. |
|
||||
| Exemplos e consistência documental | 11 testes Node aprovados; não equivalem a conformidade funcional integral. |
|
||||
| TypeScript | `tsc --noEmit` aprovado. |
|
||||
| Build | `npm run build` aprovado. |
|
||||
| Análise estática | PHPStan nível 7, zero erros nos seis ficheiros Education/middleware analisados. |
|
||||
| Formatação PHP | Pint aprovado nos cinco ficheiros PHP alterados. |
|
||||
| Confirmação final Education | 9 testes, 136 verificações aprovadas após a última correcção do controller. |
|
||||
|
||||
Os testes Education usam transacções revertidas e verificam que o destino termina em `_test`. A suite preexistente usa também recriação do esquema, exclusivamente na base descartável. Não foram efectuados ensaios de carga, auditoria independente de segurança, validação visual em navegador, testes end-to-end ou homologação institucional. Não transferir a aprovação destes testes para esses controlos.
|
||||
|
||||
## 4. Decisões necessárias para concluir o MVP
|
||||
|
||||
| Decisão concreta | Proposta para revisão | Responsável | Condição de avanço |
|
||||
| --- | --- | --- | --- |
|
||||
| Identidade da oportunidade educativa | Estender o domínio canónico de oportunidades, mantendo a oferta calendarizada separada; preservar IDs e contratos existentes. | Produto e Arquitectura | Aprovar mapeamento de identidade, campos, vínculo CRM e compatibilidade. B02. |
|
||||
| Autoridade e permissões | Gestão interna não concede mandato institucional. Vincular validação a organização, âmbito e validade do mandato; manter autor diferente do revisor. | Segurança e entidades competentes | Aprovar matriz e comprovar OAuth/OIDC antes de API institucional. B03. |
|
||||
| Dados e metodologia | Comparar pessoas disponíveis com procura; capacidade formativa é uma resposta potencial, sujeita a calendário, conclusão e recursos partilhados. | Secretariado e entidades competentes | Validar fontes, taxonomia, unidade, cobertura, actualidade e regra de alocação. B01/B06. |
|
||||
| Piloto | Até três clusters e até três corredores, seleccionados pela matriz da especificação. Nenhum seleccionado artificialmente. | Patrocinador e Produto | Identificar projectos, instituições, responsáveis e fontes autorizadas. B01. |
|
||||
| CRM e IA | Reutilizar integração existente após contratos; IA apenas recomendação revista, sem estados oficiais. | Responsável CRM e Comité de IA e Dados | Homologar sincronização, fornecedor/modelo e métricas. B04/B07. |
|
||||
| Operação e dados pessoais | Manter beneficiários fora desta implementação; definir retenção, divulgação, suporte, backup e restauro. | Dados, Segurança e Operações | Resolver B05/B08 e executar gates da especificação. |
|
||||
| Decisão concreta | Proposta para revisão | Responsável | Condição de avanço |
|
||||
| ------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| Identidade da oportunidade educativa | Estender o domínio canónico de oportunidades, mantendo a oferta calendarizada separada; preservar IDs e contratos existentes. | Produto e Arquitectura | Aprovar mapeamento de identidade, campos, vínculo CRM e compatibilidade. B02. |
|
||||
| Autoridade e permissões | Gestão interna não concede mandato institucional. Vincular validação a organização, âmbito e validade do mandato; manter autor diferente do revisor. | Segurança e entidades competentes | Aprovar matriz e comprovar OAuth/OIDC antes de API institucional. B03. |
|
||||
| Dados e metodologia | Comparar pessoas disponíveis com procura; capacidade formativa é uma resposta potencial, sujeita a calendário, conclusão e recursos partilhados. | Secretariado e entidades competentes | Validar fontes, taxonomia, unidade, cobertura, actualidade e regra de alocação. B01/B06. |
|
||||
| Piloto | Até três clusters e até três corredores, seleccionados pela matriz da especificação. Nenhum seleccionado artificialmente. | Patrocinador e Produto | Identificar projectos, instituições, responsáveis e fontes autorizadas. B01. |
|
||||
| CRM e IA | Reutilizar integração existente após contratos; IA apenas recomendação revista, sem estados oficiais. | Responsável CRM e Comité de IA e Dados | Homologar sincronização, fornecedor/modelo e métricas. B04/B07. |
|
||||
| Operação e dados pessoais | Manter beneficiários fora desta implementação; definir retenção, divulgação, suporte, backup e restauro. | Dados, Segurança e Operações | Resolver B05/B08 e executar gates da especificação. |
|
||||
|
||||
O texto de origem exige: «Quando faltar informação do repositório ou da arquitectura existente, produz uma especificação, interface ou pseudocódigo marcado como Pendente de validação, e não código executável apresentado como definitivo». Exige ainda que conflitos sejam assinalados como Bloqueio. Por isso, este diagnóstico não cria tabelas, estados oficiais, integrações ou identidades para contornar as decisões acima.
|
||||
|
||||
|
||||
@@ -15,32 +15,32 @@ Executado o anexo de tabelas recebido em 30/09/2026, adaptado aos mestres existe
|
||||
|
||||
As seguintes tabelas são extensões com **PK também FK obrigatória** para o mestre. Não geram um segundo identificador da entidade. Nomes/códigos e outros campos indicados continuam a ser lidos no mestre; não foram copiados.
|
||||
|
||||
| Extensão em skills_edu | Mestre em public | Informação mantida no mestre |
|
||||
|---|---|---|
|
||||
| skill_details | skills | UUID, código e nome PT |
|
||||
| occupation_details | occupations | UUID, código e nome PT |
|
||||
| program_details | courses | UUID, código, título PT, instituição/provider e estado |
|
||||
| human_resource_need_details | labour_demands | UUID, projecto, localização, ocupação, quantidade, qualificação, período, fonte e estado |
|
||||
| Extensão em skills_edu | Mestre em public | Informação mantida no mestre |
|
||||
| ------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------ |
|
||||
| skill_details | skills | UUID, código e nome PT |
|
||||
| occupation_details | occupations | UUID, código e nome PT |
|
||||
| program_details | courses | UUID, código, título PT, instituição/provider e estado |
|
||||
| human_resource_need_details | labour_demands | UUID, projecto, localização, ocupação, quantidade, qualificação, período, fonte e estado |
|
||||
| educational_opportunity_details | education_investment_proposals | UUID da proposta; título, descrição, valor e moeda continuam na oportunidade ligada por opportunity_id |
|
||||
| qualification_document_details | documents | UUID, nome do ficheiro, MIME, storage_key e SHA-256 |
|
||||
| consent_details | consents | UUID, finalidade, estado, concessão e retirada |
|
||||
| qualification_document_details | documents | UUID, nome do ficheiro, MIME, storage_key e SHA-256 |
|
||||
| consent_details | consents | UUID, finalidade, estado, concessão e retirada |
|
||||
|
||||
Os estados destas entidades seguem o mestre existente: por exemplo, consentimento retirado usa `withdrawn`, não uma segunda coluna `revoked`. A unicidade de códigos de competências/ocupações/cursos conserva as regras actuais dos mestres, incluindo soft delete. A versão do grupo documental na extensão refere-se ao comprovativo; `documents.version_no` continua a ser a versão do registo mestre.
|
||||
|
||||
O consentimento deve ter `public.consents.subject_ref` igual ao UUID do perfil. O trigger da extensão valida esta ligação e que a expiração é posterior à concessão. Não converte consentimento de outra finalidade nem muda o mestre.
|
||||
|
||||
| Referência do anexo | Mestre e tipo confirmados |
|
||||
|---|---|
|
||||
| user_master_id e campos de actores *_by/*_user_id | public.users.id — bigint |
|
||||
| sector_master_id | public.sectors.id — bigint |
|
||||
| company_master_id / organisation_master_id | public.organizations.id — UUID |
|
||||
| institution_master_id | public.training_providers.id — UUID |
|
||||
| program_master_id | public.courses.id — UUID |
|
||||
| project_master_id | public.projects.id — UUID |
|
||||
| cluster_master_id | public.economic_clusters.id — UUID |
|
||||
| corridor_master_id | public.development_corridors.id — UUID |
|
||||
| zee_master_id / zfi_master_id | public.investment_zones.id — UUID |
|
||||
| location_master_id | public.geo_locations.id — UUID |
|
||||
| Referência do anexo | Mestre e tipo confirmados |
|
||||
| -------------------------------------------------- | -------------------------------------- |
|
||||
| user_master_id e campos de actores _\_by/__user_id | public.users.id — bigint |
|
||||
| sector_master_id | public.sectors.id — bigint |
|
||||
| company_master_id / organisation_master_id | public.organizations.id — UUID |
|
||||
| institution_master_id | public.training_providers.id — UUID |
|
||||
| program_master_id | public.courses.id — UUID |
|
||||
| project_master_id | public.projects.id — UUID |
|
||||
| cluster_master_id | public.economic_clusters.id — UUID |
|
||||
| corridor_master_id | public.development_corridors.id — UUID |
|
||||
| zee_master_id / zfi_master_id | public.investment_zones.id — UUID |
|
||||
| location_master_id | public.geo_locations.id — UUID |
|
||||
|
||||
As FK novas para mestres usam DELETE RESTRICT. Relações internas mantêm CASCADE/SET NULL/RESTRICT definidos no anexo. Não há conversão bigint–UUID. A escolha ZEE versus ZFI exige ainda validação de categoria na futura aplicação: a FK confirma a existência da zona, não a sua categoria.
|
||||
|
||||
|
||||
@@ -20,30 +20,30 @@ A autorização do utilizador para executar está registada na conversa. Não se
|
||||
|
||||
## Conservar–Melhorar–Substituir–Descontinuar
|
||||
|
||||
| Decisão | Componentes | Justificação |
|
||||
|---|---|---|
|
||||
| Conservar | users, organizations, sectors, projects, economic_clusters, development_corridors, investment_zones, geo_locations | Identidades mestre; não converter bigint em UUID nem duplicar taxonomias. |
|
||||
| Conservar | skills, occupations, courses, training_providers, consents, documents e extensões skills_edu | Reutilizar PK/FK e estados do mestre; novos dados por migrations aditivas. |
|
||||
| Conservar | Fluxos internos, revisão independente, cálculo de lacunas e oportunidade canónica | Compatibilidade; capacidade de formação não equivale a oferta laboral. |
|
||||
| Melhorar | labour_demands e serviços de procura | Rascunhos, arquivo, várias competências, contexto, versões e autorização por organização. |
|
||||
| Melhorar | Interfaces e pesquisa | Painéis por actor, filtros combinados, PT/EN, acessibilidade e precisão territorial autorizada. |
|
||||
| Melhorar | Consentimento, auditoria e documentos | Finalidades separadas, revogação, scanner real, quarentena e autorização em cada leitura. |
|
||||
| Substituir | Nenhum componente nesta fase | Não há evidência que justifique reconstrução integral. |
|
||||
| Descontinuar | Nenhum contrato nesta fase | Preservar consumidores e dados existentes. |
|
||||
| Decisão | Componentes | Justificação |
|
||||
| ------------ | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- |
|
||||
| Conservar | users, organizations, sectors, projects, economic_clusters, development_corridors, investment_zones, geo_locations | Identidades mestre; não converter bigint em UUID nem duplicar taxonomias. |
|
||||
| Conservar | skills, occupations, courses, training_providers, consents, documents e extensões skills_edu | Reutilizar PK/FK e estados do mestre; novos dados por migrations aditivas. |
|
||||
| Conservar | Fluxos internos, revisão independente, cálculo de lacunas e oportunidade canónica | Compatibilidade; capacidade de formação não equivale a oferta laboral. |
|
||||
| Melhorar | labour_demands e serviços de procura | Rascunhos, arquivo, várias competências, contexto, versões e autorização por organização. |
|
||||
| Melhorar | Interfaces e pesquisa | Painéis por actor, filtros combinados, PT/EN, acessibilidade e precisão territorial autorizada. |
|
||||
| Melhorar | Consentimento, auditoria e documentos | Finalidades separadas, revogação, scanner real, quarentena e autorização em cada leitura. |
|
||||
| Substituir | Nenhum componente nesta fase | Não há evidência que justifique reconstrução integral. |
|
||||
| Descontinuar | Nenhum contrato nesta fase | Preservar consumidores e dados existentes. |
|
||||
|
||||
## Plano por ficheiro — proposto, ainda não executado
|
||||
|
||||
| Ordem | Caminhos | Alteração e verificação necessária |
|
||||
|---|---|---|
|
||||
| 1 | `docs/investmoz/education/requirements.csv`, nova matriz funcional | Relacionar RF-CAN, CA/CT, RF-EDU/CRM/EII/GEO e A.31 com entidades, APIs, ecrãs, evidências e responsáveis. Não atribuir IDs normativos sem conferir a fonte. |
|
||||
| 2 | `config/education.php`, `app/Http/Middleware/EducationEnabled.php` | Flags independentes; desligar sem apagar dados. Testar acesso negado e contratos actuais. |
|
||||
| 3 | Nova migration Laravel posterior a `100600` | Evoluir rascunho/arquivo, concorrência e escopos conforme desenho aprovado; não reescrever SQL aplicado. Testar ida/volta em base isolada e recusa de perda de dados. |
|
||||
| 4 | Novos serviços em `app/Services/Education/`, requests e policies | Escritas transaccionais, segregação, organização vigente, validação contextual e auditoria sem conteúdo pessoal. |
|
||||
| 5 | Novos controllers e rotas versionadas | Perfis, necessidades, instituições, consentimentos e documentos; paginação, limites, conflitos de versão e respostas sem enumeração. Manter rotas internas. |
|
||||
| 6 | `resources/js/pages/education/` e componentes partilhados | Formulários por actor, rascunho/retoma, confirmação, revisão e estados; PT/EN, teclado e móvel. |
|
||||
| 7 | Serviços de pesquisa/correspondência e jobs | Autorização antes de resultados; condições aprovadas antes da pontuação; versão e explicação; revogação verificada também na execução de jobs. Sem encaminhamento automático. |
|
||||
| 8 | `app/Integrations/SuiteCrm/`, contratos e reconciliação | Só campos e finalidades aprovados; deduplicação, idempotência e recuperação; alternativa manual enquanto contrato externo pendente. |
|
||||
| 9 | `tests/Feature/`, `tests/Unit/`, ferramentas de navegador e contratos | CT-01–CT-16: fluxos, fronteiras, acesso, documentos maliciosos, revogação, concorrência, PT/EN, recuperação e carga com linha de base real. |
|
||||
| Ordem | Caminhos | Alteração e verificação necessária |
|
||||
| ----- | --------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 1 | `docs/investmoz/education/requirements.csv`, nova matriz funcional | Relacionar RF-CAN, CA/CT, RF-EDU/CRM/EII/GEO e A.31 com entidades, APIs, ecrãs, evidências e responsáveis. Não atribuir IDs normativos sem conferir a fonte. |
|
||||
| 2 | `config/education.php`, `app/Http/Middleware/EducationEnabled.php` | Flags independentes; desligar sem apagar dados. Testar acesso negado e contratos actuais. |
|
||||
| 3 | Nova migration Laravel posterior a `100600` | Evoluir rascunho/arquivo, concorrência e escopos conforme desenho aprovado; não reescrever SQL aplicado. Testar ida/volta em base isolada e recusa de perda de dados. |
|
||||
| 4 | Novos serviços em `app/Services/Education/`, requests e policies | Escritas transaccionais, segregação, organização vigente, validação contextual e auditoria sem conteúdo pessoal. |
|
||||
| 5 | Novos controllers e rotas versionadas | Perfis, necessidades, instituições, consentimentos e documentos; paginação, limites, conflitos de versão e respostas sem enumeração. Manter rotas internas. |
|
||||
| 6 | `resources/js/pages/education/` e componentes partilhados | Formulários por actor, rascunho/retoma, confirmação, revisão e estados; PT/EN, teclado e móvel. |
|
||||
| 7 | Serviços de pesquisa/correspondência e jobs | Autorização antes de resultados; condições aprovadas antes da pontuação; versão e explicação; revogação verificada também na execução de jobs. Sem encaminhamento automático. |
|
||||
| 8 | `app/Integrations/SuiteCrm/`, contratos e reconciliação | Só campos e finalidades aprovados; deduplicação, idempotência e recuperação; alternativa manual enquanto contrato externo pendente. |
|
||||
| 9 | `tests/Feature/`, `tests/Unit/`, ferramentas de navegador e contratos | CT-01–CT-16: fluxos, fronteiras, acesso, documentos maliciosos, revogação, concorrência, PT/EN, recuperação e carga com linha de base real. |
|
||||
|
||||
## Regras de desenho propostas para revisão
|
||||
|
||||
@@ -57,15 +57,15 @@ Sem mudança da ligação principal de dados nesta etapa. Antes de futuras migra
|
||||
|
||||
**Não concluído. Não iniciar desenvolvimento funcional sob este prompt até resolver os bloqueadores materiais expressamente definidos nele.**
|
||||
|
||||
| Elemento | Evidência disponível / falta concreta |
|
||||
|---|---|
|
||||
| Autorização para executar | Recebida; não precisa de repetição. |
|
||||
| Rastreabilidade | Há `requirements.csv`, `requirements-audit.csv` e especificação anterior. Há referências a A.31, mas não foi identificada nesta pesquisa a secção normativa integral com versão e correspondência ao novo anexo. |
|
||||
| Âmbito | O novo pedido inclui indivíduos, instituições e correspondência; a decisão antiga D-EDU-06 excluía dados individuais. Actualizar formalmente o registo de âmbito com o novo pedido, sem tratar a autorização de desenvolvimento como consentimento dos candidatos. |
|
||||
| Responsabilidade e tratamento | Faltam identificação do responsável funcional e política aplicável de finalidade, retenção, menores e destinatários. Papéis genéricos não são designação de pessoas/entidades. |
|
||||
| Correspondência | Faltam regras, pesos e conjunto de referência aprovados; não adoptar exemplos fictícios como dados operacionais. |
|
||||
| Desempenho | Falta linha de base da Fase Zero; não criar limites arbitrários para declarar CT-14 aprovado. |
|
||||
| Integração | Falta contrato específico de candidatos/consentimento com CRM e confirmação da aplicabilidade de AsyncAPI. |
|
||||
| Aceite | UAT com quatro perfis e decisões dos responsáveis não foram realizados; nenhuma aprovação fabricada. |
|
||||
| Elemento | Evidência disponível / falta concreta |
|
||||
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Autorização para executar | Recebida; não precisa de repetição. |
|
||||
| Rastreabilidade | Há `requirements.csv`, `requirements-audit.csv` e especificação anterior. Há referências a A.31, mas não foi identificada nesta pesquisa a secção normativa integral com versão e correspondência ao novo anexo. |
|
||||
| Âmbito | O novo pedido inclui indivíduos, instituições e correspondência; a decisão antiga D-EDU-06 excluía dados individuais. Actualizar formalmente o registo de âmbito com o novo pedido, sem tratar a autorização de desenvolvimento como consentimento dos candidatos. |
|
||||
| Responsabilidade e tratamento | Faltam identificação do responsável funcional e política aplicável de finalidade, retenção, menores e destinatários. Papéis genéricos não são designação de pessoas/entidades. |
|
||||
| Correspondência | Faltam regras, pesos e conjunto de referência aprovados; não adoptar exemplos fictícios como dados operacionais. |
|
||||
| Desempenho | Falta linha de base da Fase Zero; não criar limites arbitrários para declarar CT-14 aprovado. |
|
||||
| Integração | Falta contrato específico de candidatos/consentimento com CRM e confirmação da aplicabilidade de AsyncAPI. |
|
||||
| Aceite | UAT com quatro perfis e decisões dos responsáveis não foram realizados; nenhuma aprovação fabricada. |
|
||||
|
||||
Próxima entrada necessária: localização da versão normativa de A.31 e do pacote de decisões da Fase Zero, incluindo os responsáveis e políticas acima. Se esses documentos ainda não existirem, é necessário resolver a divergência entre a ordem de implementar e a proibição explícita de iniciar desenvolvimento contida neste anexo.
|
||||
|
||||
@@ -14,20 +14,20 @@ Fase executada: descoberta e enquadramento. Todas as etapas estão autorizadas p
|
||||
|
||||
## Diagnóstico confirmado
|
||||
|
||||
| Componente | Evidência no repositório | Aplicação ao mercado |
|
||||
| --- | --- | --- |
|
||||
| Stack | composer.lock: Laravel 13.30.1, Fortify 1.39.0, Inertia Laravel 3.3.3; instalados React 19.2.8, TypeScript 5.9.3, Vite 8.2.2 | Compatível; sem nova dependência |
|
||||
| Runtime e BD | Inventário apenas de leitura: PHP 8.5.11, PostgreSQL 17.11, PostGIS 3.6.2 | Reutilizar; desenvolvimento local não comprova prontidão de produção |
|
||||
| Identidade | users, organizations, memberships; Fortify e manage-portal | Identidade da conta não comprova titularidade nem representação; falta mandato por anúncio |
|
||||
| Catálogo | InvestmentCatalogue, investment_items e mapeamento de oportunidades | Preservar IDs; não converter oportunidade em negócio sem revisão |
|
||||
| Workspace | InvestmentWorkspace, InvestmentSubmissionController | Cifragem, snapshots, consentimento, mensagens e notificações reutilizáveis; sem workflow MNE |
|
||||
| Documentos | documents, anexos privados, download autenticado com checksum | Não equivale a data room: faltam grants temporais, NDA validado, watermark e scanner comprovado |
|
||||
| CRM interno | ConvertInterestToLead, CrmTask, leads, interest_requests | Conversão transaccional, consentimento e próxima acção; falta deduplicação negócio/modalidade/período |
|
||||
| CRM externo | SuiteCrmClient, config/suitecrm.php, testes com doubles | Timeout 10s, ligação 3s, retries de leitura 2 e circuito configurados; nenhuma sincronização MNE real comprovada |
|
||||
| Territórios | Clusters/corredores/projectos, zonas, PostGIS | Reutilizar relações; GIS transversal ainda incompleto, conforme ../gis/README.md |
|
||||
| Notificações | investment_notifications, outbox_events/inbox_events | Notificações internas e infraestrutura; não presumir envio externo ou SLA contratado |
|
||||
| Pagamentos/facturação | Nenhum módulo operacional identificado nas áreas inspeccionadas | Fora do piloto; sem taxas, comissões, impostos ou cobrança inventados |
|
||||
| Jurídico | Fontes de zonas e documentos; não foi encontrado motor de elegibilidade MNE | Não reutilizar validação de ZEE como análise jurídica de venda/cessão |
|
||||
| Componente | Evidência no repositório | Aplicação ao mercado |
|
||||
| --------------------- | ---------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| Stack | composer.lock: Laravel 13.30.1, Fortify 1.39.0, Inertia Laravel 3.3.3; instalados React 19.2.8, TypeScript 5.9.3, Vite 8.2.2 | Compatível; sem nova dependência |
|
||||
| Runtime e BD | Inventário apenas de leitura: PHP 8.5.11, PostgreSQL 17.11, PostGIS 3.6.2 | Reutilizar; desenvolvimento local não comprova prontidão de produção |
|
||||
| Identidade | users, organizations, memberships; Fortify e manage-portal | Identidade da conta não comprova titularidade nem representação; falta mandato por anúncio |
|
||||
| Catálogo | InvestmentCatalogue, investment_items e mapeamento de oportunidades | Preservar IDs; não converter oportunidade em negócio sem revisão |
|
||||
| Workspace | InvestmentWorkspace, InvestmentSubmissionController | Cifragem, snapshots, consentimento, mensagens e notificações reutilizáveis; sem workflow MNE |
|
||||
| Documentos | documents, anexos privados, download autenticado com checksum | Não equivale a data room: faltam grants temporais, NDA validado, watermark e scanner comprovado |
|
||||
| CRM interno | ConvertInterestToLead, CrmTask, leads, interest_requests | Conversão transaccional, consentimento e próxima acção; falta deduplicação negócio/modalidade/período |
|
||||
| CRM externo | SuiteCrmClient, config/suitecrm.php, testes com doubles | Timeout 10s, ligação 3s, retries de leitura 2 e circuito configurados; nenhuma sincronização MNE real comprovada |
|
||||
| Territórios | Clusters/corredores/projectos, zonas, PostGIS | Reutilizar relações; GIS transversal ainda incompleto, conforme ../gis/README.md |
|
||||
| Notificações | investment_notifications, outbox_events/inbox_events | Notificações internas e infraestrutura; não presumir envio externo ou SLA contratado |
|
||||
| Pagamentos/facturação | Nenhum módulo operacional identificado nas áreas inspeccionadas | Fora do piloto; sem taxas, comissões, impostos ou cobrança inventados |
|
||||
| Jurídico | Fontes de zonas e documentos; não foi encontrado motor de elegibilidade MNE | Não reutilizar validação de ZEE como análise jurídica de venda/cessão |
|
||||
|
||||
Inventário agregado local em 28/09/2026: 103 tabelas, 38 migrations, 6 oportunidades, 6 investment_items, 16 projectos, 29 dossiers, 8 clusters, 6 corredores. Zero documentos, organizações, geometrias, localizações, zonas, pedidos de interesse e leads. Há 1 investidor; não foi lido o seu conteúdo. As 6 oportunidades não têm opportunity_profiles com source_reference; este indicador não prova ausência de todas as fontes possíveis. Nenhum destes registos foi reclassificado como negócio existente.
|
||||
|
||||
@@ -35,14 +35,14 @@ Não se encontrou cadastro dedicado de anúncios MNE, mandatos, titularidades, l
|
||||
|
||||
## Conservar–Melhorar–Substituir–Descontinuar
|
||||
|
||||
| Decisão | Componente | Acção proposta |
|
||||
| --- | --- | --- |
|
||||
| Conservar | Laravel/React/PostGIS, identidade, documentos canónicos, outbox e IDs mestres | Adaptadores versionados; manter contratos existentes |
|
||||
| Melhorar | Autorização | Função e atributo por anúncio, mandatário, revisor e aprovador; MFA privilegiado; segregação em dois níveis |
|
||||
| Melhorar | Documentos e CRM | Quarentena/scanner, grants revogáveis, escopo por anúncio; idempotência e reconciliação MNE |
|
||||
| Melhorar | Catálogo, pesquisa e GIS | Projecção pública mínima separada; indexar só atributos autorizados; testar reidentificação |
|
||||
| Substituir, apenas no novo fluxo | Autodeclaração como prova de legitimidade | Evidência documental e revisão humana; preservar declarações históricas |
|
||||
| Descontinuar | Nenhum componente existente nesta fase | Nenhuma remoção; retirar práticas de divulgação não autorizada apenas após análise de impacto |
|
||||
| Decisão | Componente | Acção proposta |
|
||||
| -------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
||||
| Conservar | Laravel/React/PostGIS, identidade, documentos canónicos, outbox e IDs mestres | Adaptadores versionados; manter contratos existentes |
|
||||
| Melhorar | Autorização | Função e atributo por anúncio, mandatário, revisor e aprovador; MFA privilegiado; segregação em dois níveis |
|
||||
| Melhorar | Documentos e CRM | Quarentena/scanner, grants revogáveis, escopo por anúncio; idempotência e reconciliação MNE |
|
||||
| Melhorar | Catálogo, pesquisa e GIS | Projecção pública mínima separada; indexar só atributos autorizados; testar reidentificação |
|
||||
| Substituir, apenas no novo fluxo | Autodeclaração como prova de legitimidade | Evidência documental e revisão humana; preservar declarações históricas |
|
||||
| Descontinuar | Nenhum componente existente nesta fase | Nenhuma remoção; retirar práticas de divulgação não autorizada apenas após análise de impacto |
|
||||
|
||||
## Jornada proposta
|
||||
|
||||
@@ -59,16 +59,16 @@ Estados solicitados: rascunho, submetido, em triagem, em revisão, pendente de i
|
||||
|
||||
## RACI proposto — atribuições nominais pendentes
|
||||
|
||||
| Actividade | Responsável pela execução (R) | Responsável final (A) | Consultados (C) | Informados (I) |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Escopo, piloto e política | Produto | Dono do Produto | Jurídico, Dados, Segurança | Operações |
|
||||
| Modelo e contratos | Engenharia/Arquitectura | Arquitectura | Dados, CRM, Segurança | Produto |
|
||||
| Legitimidade, direitos, NDA, KYC/AML aplicável | Jurídico/Conformidade | Responsável jurídico designado | Titular, entidade competente quando autorizada | Produto |
|
||||
| Triagem e revisão documental | Operações/revisor | Gestor de validação | Jurídico, Dados | Anunciante |
|
||||
| Aprovação/publicação | Aprovadores de níveis 1 e 2 | Responsável editorial designado | Jurídico, Segurança | CRM |
|
||||
| Sala segura e incidentes | Segurança/Operações | Responsável de segurança | Jurídico, Dados | Produto |
|
||||
| UAT e recuperação | QA/Operações | Dono do Produto | Arquitectura, Segurança | Suporte |
|
||||
| Comissões/facturação futura | Finanças | Responsável financeiro | Jurídico, Auditoria | Produto |
|
||||
| Actividade | Responsável pela execução (R) | Responsável final (A) | Consultados (C) | Informados (I) |
|
||||
| ---------------------------------------------- | ----------------------------- | ------------------------------- | ---------------------------------------------- | -------------- |
|
||||
| Escopo, piloto e política | Produto | Dono do Produto | Jurídico, Dados, Segurança | Operações |
|
||||
| Modelo e contratos | Engenharia/Arquitectura | Arquitectura | Dados, CRM, Segurança | Produto |
|
||||
| Legitimidade, direitos, NDA, KYC/AML aplicável | Jurídico/Conformidade | Responsável jurídico designado | Titular, entidade competente quando autorizada | Produto |
|
||||
| Triagem e revisão documental | Operações/revisor | Gestor de validação | Jurídico, Dados | Anunciante |
|
||||
| Aprovação/publicação | Aprovadores de níveis 1 e 2 | Responsável editorial designado | Jurídico, Segurança | CRM |
|
||||
| Sala segura e incidentes | Segurança/Operações | Responsável de segurança | Jurídico, Dados | Produto |
|
||||
| UAT e recuperação | QA/Operações | Dono do Produto | Arquitectura, Segurança | Suporte |
|
||||
| Comissões/facturação futura | Finanças | Responsável financeiro | Jurídico, Auditoria | Produto |
|
||||
|
||||
Papéis propostos não concedem permissões. Ninguém foi nomeado por inferência. Autor da submissão não pode aprovar a própria publicação; os dois níveis exigem segregação a especificar e testar.
|
||||
|
||||
@@ -76,39 +76,39 @@ Papéis propostos não concedem permissões. Ninguém foi nomeado por inferênci
|
||||
|
||||
Esta matriz é de **questões para validação**, não parecer ou afirmação de admissibilidade legal. Não foram inventadas leis, termos de uso ou autoridades competentes. Regra, fonte oficial, versão, entidade, parecer e validade ficam pendentes.
|
||||
|
||||
| Grupo de modalidades solicitadas | Objecto a distinguir | Verificação e documento necessário | Responsável proposto / estado |
|
||||
| --- | --- | --- | --- |
|
||||
| Venda 100%, parcial, aumento de capital, investidor financeiro, sócio estratégico, comprador | Empresa, participação, mandato e avaliação | Titularidade/representação, encargos, consentimentos e requisitos da operação | Jurídico + titular / pendente |
|
||||
| Cessão de posição, franchising, joint venture | Contrato, licença de uso, posição contratual | Restrições, consentimentos de terceiros, validade e análise da cessão | Jurídico / pendente |
|
||||
| Venda/arrendamento de activos, transferência de unidade, sucessão | Activo físico, unidade, contratos e obrigações | Cadeia documental, legitimidade, passivos e autorizações | Jurídico + diligência especializada / pendente |
|
||||
| Recuperação, expansão, internacionalização | Pedido de capital e finalidade | Informação financeira rotulada, riscos e verificação independente | Revisor + assessor / pendente |
|
||||
| Imobiliário, propriedades, benfeitorias, investimentos agrícolas | Imóvel/unidade, investimento, direito de uso/ocupação e contrato separados | Área/limites, ocupação, encargos, litígios, finalidade, prazo, autorizações e transmissibilidade | Jurídico fundiário + entidade competente por confirmar / pendente |
|
||||
| Licenças mineiras/florestais e outras | Licença, titular declarado, entidade emissora e actividade | Fonte competente autorizada, validade, restrições, obrigações e possibilidade de transmissão | Jurídico sectorial + entidade por confirmar / pendente |
|
||||
| Grupo de modalidades solicitadas | Objecto a distinguir | Verificação e documento necessário | Responsável proposto / estado |
|
||||
| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- |
|
||||
| Venda 100%, parcial, aumento de capital, investidor financeiro, sócio estratégico, comprador | Empresa, participação, mandato e avaliação | Titularidade/representação, encargos, consentimentos e requisitos da operação | Jurídico + titular / pendente |
|
||||
| Cessão de posição, franchising, joint venture | Contrato, licença de uso, posição contratual | Restrições, consentimentos de terceiros, validade e análise da cessão | Jurídico / pendente |
|
||||
| Venda/arrendamento de activos, transferência de unidade, sucessão | Activo físico, unidade, contratos e obrigações | Cadeia documental, legitimidade, passivos e autorizações | Jurídico + diligência especializada / pendente |
|
||||
| Recuperação, expansão, internacionalização | Pedido de capital e finalidade | Informação financeira rotulada, riscos e verificação independente | Revisor + assessor / pendente |
|
||||
| Imobiliário, propriedades, benfeitorias, investimentos agrícolas | Imóvel/unidade, investimento, direito de uso/ocupação e contrato separados | Área/limites, ocupação, encargos, litígios, finalidade, prazo, autorizações e transmissibilidade | Jurídico fundiário + entidade competente por confirmar / pendente |
|
||||
| Licenças mineiras/florestais e outras | Licença, titular declarado, entidade emissora e actividade | Fonte competente autorizada, validade, restrições, obrigações e possibilidade de transmissão | Jurídico sectorial + entidade por confirmar / pendente |
|
||||
|
||||
Termos de uso, base de tratamento, consentimento granular, retenção, conflitos de interesse, prevenção de fraude, denúncias, critérios AML/KYC quando aplicáveis e NDA: todos pendentes de aprovação especializada. Não recolher prova de fundos como obrigação geral nem presumir validade de assinatura electrónica. Política proposta: ausência de evidência bloqueia publicação/acesso; nenhum rótulo indica certificação da Tempo.
|
||||
|
||||
## Classificação e ameaças
|
||||
|
||||
| Dados | Classificação inicial proposta | Controlos e teste de aceitação |
|
||||
| --- | --- | --- |
|
||||
| Teaser aprovado, modalidade, sector, região indicativa, valor rotulado | Público apenas após autorização | Allowlist na API/índice/exportação; teste de reidentificação |
|
||||
| Triagem, tarefas, estados, origens CRM | Interno | Menor privilégio; auditoria sem conteúdo reservado |
|
||||
| Empresa, financeiros, contratos, clientes/fornecedores, propostas | Confidencial | Cifragem, escopo por anúncio, grants temporais; teste de acesso cruzado |
|
||||
| Dados | Classificação inicial proposta | Controlos e teste de aceitação |
|
||||
| --------------------------------------------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| Teaser aprovado, modalidade, sector, região indicativa, valor rotulado | Público apenas após autorização | Allowlist na API/índice/exportação; teste de reidentificação |
|
||||
| Triagem, tarefas, estados, origens CRM | Interno | Menor privilégio; auditoria sem conteúdo reservado |
|
||||
| Empresa, financeiros, contratos, clientes/fornecedores, propostas | Confidencial | Cifragem, escopo por anúncio, grants temporais; teste de acesso cruzado |
|
||||
| Identidade, títulos/licenças integrais, coordenadas exactas, segredos e denúncias | Restrito por defeito até classificação aprovada | Sem índice público; URLs não públicas, revogação/expiração, logs minimizados |
|
||||
|
||||
Finalidade, base, dono, retenção, eliminação e divulgação precisam de decisão por categoria; nenhum prazo foi inventado. Cifragem existente depende da chave Laravel e não substitui gestão de chaves, mandatos ou política de retenção.
|
||||
|
||||
| Ameaça | Controlo planeado | Prova exigida |
|
||||
| --- | --- | --- |
|
||||
| Anunciante falso/mandato revogado | Verificação separada de conta, identidade, representação e mandato; revalidação | Publicação negada quando ausente/expirado/revogado |
|
||||
| IDOR entre negócios ou data rooms | Autorização por utilizador, negócio, documento, versão e grant | Leitura/download/listagem cruzados negados |
|
||||
| Auto-publicação e corrida de versões | Dupla aprovação, lock transaccional e versão esperada | Auto-aprovação negada; versão obsoleta 409 |
|
||||
| Reidentificação no mapa/pesquisa | Projecção mínima e política GIS comum | Nenhuma coordenada restrita por filtros, cache ou exportação |
|
||||
| Malware/ficheiro disfarçado | Quarentena, análise e validação de conteúdo | Nenhum download antes do resultado autorizado; falha do scanner bloqueia |
|
||||
| Revogação com URL/cache persistente | Revalidar cada acesso, no-store, invalidação e TTL | URL partilhado/expirado não concede acesso |
|
||||
| Duplicação/replay CRM e webhooks | Chave de idempotência, outbox/inbox, autenticação e reconciliação | Replay não duplica lead; falha externa recuperável |
|
||||
| Fraude, discriminação ou matching opaco | Sinais explicáveis, fila humana, conflitos e denúncias | Sem contacto/recusa definitiva automática |
|
||||
| Pipeline contabilizado como receita | Valores por natureza e eventos financeiros aprovados | Comissão/cobrança bloqueadas sem contrato/regra/evento |
|
||||
| Ameaça | Controlo planeado | Prova exigida |
|
||||
| --------------------------------------- | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| Anunciante falso/mandato revogado | Verificação separada de conta, identidade, representação e mandato; revalidação | Publicação negada quando ausente/expirado/revogado |
|
||||
| IDOR entre negócios ou data rooms | Autorização por utilizador, negócio, documento, versão e grant | Leitura/download/listagem cruzados negados |
|
||||
| Auto-publicação e corrida de versões | Dupla aprovação, lock transaccional e versão esperada | Auto-aprovação negada; versão obsoleta 409 |
|
||||
| Reidentificação no mapa/pesquisa | Projecção mínima e política GIS comum | Nenhuma coordenada restrita por filtros, cache ou exportação |
|
||||
| Malware/ficheiro disfarçado | Quarentena, análise e validação de conteúdo | Nenhum download antes do resultado autorizado; falha do scanner bloqueia |
|
||||
| Revogação com URL/cache persistente | Revalidar cada acesso, no-store, invalidação e TTL | URL partilhado/expirado não concede acesso |
|
||||
| Duplicação/replay CRM e webhooks | Chave de idempotência, outbox/inbox, autenticação e reconciliação | Replay não duplica lead; falha externa recuperável |
|
||||
| Fraude, discriminação ou matching opaco | Sinais explicáveis, fila humana, conflitos e denúncias | Sem contacto/recusa definitiva automática |
|
||||
| Pipeline contabilizado como receita | Valores por natureza e eventos financeiros aprovados | Comissão/cobrança bloqueadas sem contrato/regra/evento |
|
||||
|
||||
## Backlog e escopo piloto proposto
|
||||
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
{
|
||||
"date": "2026-09-29T06:39:38.960Z",
|
||||
"scope": "Synthetic private create/save and responsive layout in isolated QA; institutional review covered by integration tests",
|
||||
"observations": [
|
||||
{
|
||||
"scenario": "private_create_and_save",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"scenario": "private_submit_with_review_consent",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"breadcrumb": true,
|
||||
"supportVisible": true,
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"breadcrumb": true,
|
||||
"supportVisible": true,
|
||||
"overflow": false
|
||||
}
|
||||
],
|
||||
"errors": []
|
||||
"date": "2026-09-29T06:39:38.960Z",
|
||||
"scope": "Synthetic private create/save and responsive layout in isolated QA; institutional review covered by integration tests",
|
||||
"observations": [
|
||||
{
|
||||
"scenario": "private_create_and_save",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"scenario": "private_submit_with_review_consent",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"width": 390,
|
||||
"breadcrumb": true,
|
||||
"supportVisible": true,
|
||||
"overflow": false
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"breadcrumb": true,
|
||||
"supportVisible": true,
|
||||
"overflow": false
|
||||
}
|
||||
],
|
||||
"errors": []
|
||||
}
|
||||
|
||||
@@ -90,9 +90,7 @@
|
||||
"type": "text",
|
||||
"max": 500,
|
||||
"when": {
|
||||
"acting_capacity": [
|
||||
"representative"
|
||||
]
|
||||
"acting_capacity": ["representative"]
|
||||
}
|
||||
},
|
||||
"mandate_document_reference": {
|
||||
@@ -102,9 +100,7 @@
|
||||
"type": "text",
|
||||
"max": 500,
|
||||
"when": {
|
||||
"acting_capacity": [
|
||||
"representative"
|
||||
]
|
||||
"acting_capacity": ["representative"]
|
||||
}
|
||||
},
|
||||
"mandate_valid_until": {
|
||||
@@ -114,9 +110,7 @@
|
||||
"type": "date",
|
||||
"max": 10,
|
||||
"when": {
|
||||
"acting_capacity": [
|
||||
"representative"
|
||||
]
|
||||
"acting_capacity": ["representative"]
|
||||
}
|
||||
},
|
||||
"disclosure_scope": {
|
||||
@@ -126,9 +120,7 @@
|
||||
"type": "textarea",
|
||||
"max": 2000,
|
||||
"when": {
|
||||
"acting_capacity": [
|
||||
"representative"
|
||||
]
|
||||
"acting_capacity": ["representative"]
|
||||
}
|
||||
},
|
||||
"legitimacy_declaration": {
|
||||
@@ -209,9 +201,7 @@
|
||||
"type": "text",
|
||||
"max": 200,
|
||||
"when": {
|
||||
"project_stage": [
|
||||
"other"
|
||||
]
|
||||
"project_stage": ["other"]
|
||||
}
|
||||
},
|
||||
"availability": {
|
||||
@@ -233,9 +223,7 @@
|
||||
"type": "date",
|
||||
"max": 10,
|
||||
"when": {
|
||||
"availability": [
|
||||
"from_date"
|
||||
]
|
||||
"availability": ["from_date"]
|
||||
}
|
||||
},
|
||||
"valid_until": {
|
||||
@@ -252,10 +240,7 @@
|
||||
"type": "decimal",
|
||||
"max": 24,
|
||||
"when": {
|
||||
"value_status": [
|
||||
"defined",
|
||||
"indicative"
|
||||
]
|
||||
"value_status": ["defined", "indicative"]
|
||||
}
|
||||
},
|
||||
"value_status": {
|
||||
@@ -283,10 +268,7 @@
|
||||
"financing_sought": "Financiamento procurado"
|
||||
},
|
||||
"when": {
|
||||
"value_status": [
|
||||
"defined",
|
||||
"indicative"
|
||||
]
|
||||
"value_status": ["defined", "indicative"]
|
||||
}
|
||||
},
|
||||
"value_nature": {
|
||||
@@ -301,10 +283,7 @@
|
||||
"appraised": "Avaliado, sujeito a prova"
|
||||
},
|
||||
"when": {
|
||||
"value_status": [
|
||||
"defined",
|
||||
"indicative"
|
||||
]
|
||||
"value_status": ["defined", "indicative"]
|
||||
}
|
||||
},
|
||||
"currency": {
|
||||
@@ -314,10 +293,7 @@
|
||||
"type": "text",
|
||||
"max": 3,
|
||||
"when": {
|
||||
"value_status": [
|
||||
"defined",
|
||||
"indicative"
|
||||
]
|
||||
"value_status": ["defined", "indicative"]
|
||||
}
|
||||
},
|
||||
"amount_secured": {
|
||||
@@ -327,9 +303,7 @@
|
||||
"type": "decimal",
|
||||
"max": 24,
|
||||
"when": {
|
||||
"opportunity_type": [
|
||||
"investment_project"
|
||||
]
|
||||
"opportunity_type": ["investment_project"]
|
||||
}
|
||||
},
|
||||
"investment_total": {
|
||||
@@ -339,9 +313,7 @@
|
||||
"type": "decimal",
|
||||
"max": 24,
|
||||
"when": {
|
||||
"opportunity_type": [
|
||||
"investment_project"
|
||||
]
|
||||
"opportunity_type": ["investment_project"]
|
||||
}
|
||||
},
|
||||
"amount_sought": {
|
||||
@@ -351,10 +323,7 @@
|
||||
"type": "decimal",
|
||||
"max": 24,
|
||||
"when": {
|
||||
"opportunity_type": [
|
||||
"investment_project",
|
||||
"financing_request"
|
||||
]
|
||||
"opportunity_type": ["investment_project", "financing_request"]
|
||||
}
|
||||
},
|
||||
"equity_percentage": {
|
||||
@@ -383,10 +352,7 @@
|
||||
"assets": "Activos"
|
||||
},
|
||||
"when": {
|
||||
"opportunity_type": [
|
||||
"company_sale",
|
||||
"asset_sale"
|
||||
]
|
||||
"opportunity_type": ["company_sale", "asset_sale"]
|
||||
}
|
||||
},
|
||||
"partnership_contribution": {
|
||||
@@ -404,9 +370,7 @@
|
||||
"other": "Outra"
|
||||
},
|
||||
"when": {
|
||||
"opportunity_type": [
|
||||
"business_partnership"
|
||||
]
|
||||
"opportunity_type": ["business_partnership"]
|
||||
}
|
||||
},
|
||||
"asset_modality": {
|
||||
@@ -421,9 +385,7 @@
|
||||
"other": "Outra"
|
||||
},
|
||||
"when": {
|
||||
"opportunity_type": [
|
||||
"asset_sale"
|
||||
]
|
||||
"opportunity_type": ["asset_sale"]
|
||||
}
|
||||
},
|
||||
"concession_entity": {
|
||||
@@ -433,9 +395,7 @@
|
||||
"type": "text",
|
||||
"max": 300,
|
||||
"when": {
|
||||
"opportunity_type": [
|
||||
"concession_opportunity"
|
||||
]
|
||||
"opportunity_type": ["concession_opportunity"]
|
||||
}
|
||||
},
|
||||
"concession_reference": {
|
||||
@@ -445,9 +405,7 @@
|
||||
"type": "textarea",
|
||||
"max": 2000,
|
||||
"when": {
|
||||
"opportunity_type": [
|
||||
"concession_opportunity"
|
||||
]
|
||||
"opportunity_type": ["concession_opportunity"]
|
||||
}
|
||||
},
|
||||
"supply_description": {
|
||||
@@ -457,9 +415,7 @@
|
||||
"type": "textarea",
|
||||
"max": 2000,
|
||||
"when": {
|
||||
"opportunity_type": [
|
||||
"supply_opportunity"
|
||||
]
|
||||
"opportunity_type": ["supply_opportunity"]
|
||||
}
|
||||
},
|
||||
"conditions": {
|
||||
@@ -489,10 +445,7 @@
|
||||
"type": "territory",
|
||||
"max": 36,
|
||||
"when": {
|
||||
"geography_scope": [
|
||||
"specific",
|
||||
"multiple"
|
||||
]
|
||||
"geography_scope": ["specific", "multiple"]
|
||||
}
|
||||
},
|
||||
"district_id": {
|
||||
@@ -502,10 +455,7 @@
|
||||
"type": "territory",
|
||||
"max": 36,
|
||||
"when": {
|
||||
"geography_scope": [
|
||||
"specific",
|
||||
"multiple"
|
||||
]
|
||||
"geography_scope": ["specific", "multiple"]
|
||||
}
|
||||
},
|
||||
"admin_post_id": {
|
||||
@@ -515,10 +465,7 @@
|
||||
"type": "territory",
|
||||
"max": 36,
|
||||
"when": {
|
||||
"geography_scope": [
|
||||
"specific",
|
||||
"multiple"
|
||||
]
|
||||
"geography_scope": ["specific", "multiple"]
|
||||
}
|
||||
},
|
||||
"locality_id": {
|
||||
@@ -528,10 +475,7 @@
|
||||
"type": "territory",
|
||||
"max": 36,
|
||||
"when": {
|
||||
"geography_scope": [
|
||||
"specific",
|
||||
"multiple"
|
||||
]
|
||||
"geography_scope": ["specific", "multiple"]
|
||||
}
|
||||
},
|
||||
"location_notes": {
|
||||
@@ -552,10 +496,7 @@
|
||||
"no": "Não"
|
||||
},
|
||||
"when": {
|
||||
"geography_scope": [
|
||||
"specific",
|
||||
"multiple"
|
||||
]
|
||||
"geography_scope": ["specific", "multiple"]
|
||||
}
|
||||
},
|
||||
"disclosure_precision": {
|
||||
@@ -570,10 +511,7 @@
|
||||
"administrative": "Apenas território administrativo"
|
||||
},
|
||||
"when": {
|
||||
"geography_scope": [
|
||||
"specific",
|
||||
"multiple"
|
||||
]
|
||||
"geography_scope": ["specific", "multiple"]
|
||||
}
|
||||
},
|
||||
"asset_notes": {
|
||||
|
||||
@@ -28,16 +28,16 @@ Não existe publicação neste incremento. A constraint da BD permite apenas `dr
|
||||
|
||||
## Matriz das fases e decisões
|
||||
|
||||
| Fase | Executado | Lacunas / decisão |
|
||||
| --- | --- | --- |
|
||||
| 0 — descoberta | Diagnóstico, ameaças, RACI, matriz jurídica, baseline | Política/base/equipa/piloto não homologados; No-Go institucional mantém-se |
|
||||
| 1 — modelo | Negócio, objectos tipados separados, mandato pendente, classificação, versões, histórico, rollback | Relações documentais/territoriais detalhadas, titularidade comprovada, revisão de mandatos e retenção homologada pendentes; Go condicionado técnico |
|
||||
| 2 — submissão/administração | UI e API de rascunho, submissão, consentimento, revisão independente, pedido de informação, retirada | Upload/scanner, verificações documentais, dois níveis de aprovação e publicação ainda não implementados; núcleo privado utilizável, fase parcial |
|
||||
| 3 — catálogo público | Nenhuma ficha reservada enviada ao catálogo público | Projecção anonimizada, revisão de reidentificação, filtros e mapas MNE pendentes; No-Go para divulgação |
|
||||
| 4 — CRM/matching | CRM existente preservado; nenhum lead MNE enviado | Deduplicação interessado/negócio/modalidade/período, matching revisto e integrações MNE pendentes |
|
||||
| 5 — sala segura | Referências declarativas e cifragem privadas | NDA juridicamente aprovado, grants temporais, malware, watermark e diligência não implementados; sem sala segura anunciada |
|
||||
| 6 — negociação/monetização | Valores reservados rotulados por natureza | Workflow negocial, contrato/regra financeira, facturação e reconciliação pendentes; nenhuma cobrança |
|
||||
| 7 — piloto/operação | Backup/restauro, migrations locais, reconciliação, testes e build | Piloto real, UAT institucional, carga, recuperação MNE completa, PT/EN completo e decisão formal de produção pendentes |
|
||||
| Fase | Executado | Lacunas / decisão |
|
||||
| --------------------------- | ---------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 0 — descoberta | Diagnóstico, ameaças, RACI, matriz jurídica, baseline | Política/base/equipa/piloto não homologados; No-Go institucional mantém-se |
|
||||
| 1 — modelo | Negócio, objectos tipados separados, mandato pendente, classificação, versões, histórico, rollback | Relações documentais/territoriais detalhadas, titularidade comprovada, revisão de mandatos e retenção homologada pendentes; Go condicionado técnico |
|
||||
| 2 — submissão/administração | UI e API de rascunho, submissão, consentimento, revisão independente, pedido de informação, retirada | Upload/scanner, verificações documentais, dois níveis de aprovação e publicação ainda não implementados; núcleo privado utilizável, fase parcial |
|
||||
| 3 — catálogo público | Nenhuma ficha reservada enviada ao catálogo público | Projecção anonimizada, revisão de reidentificação, filtros e mapas MNE pendentes; No-Go para divulgação |
|
||||
| 4 — CRM/matching | CRM existente preservado; nenhum lead MNE enviado | Deduplicação interessado/negócio/modalidade/período, matching revisto e integrações MNE pendentes |
|
||||
| 5 — sala segura | Referências declarativas e cifragem privadas | NDA juridicamente aprovado, grants temporais, malware, watermark e diligência não implementados; sem sala segura anunciada |
|
||||
| 6 — negociação/monetização | Valores reservados rotulados por natureza | Workflow negocial, contrato/regra financeira, facturação e reconciliação pendentes; nenhuma cobrança |
|
||||
| 7 — piloto/operação | Backup/restauro, migrations locais, reconciliação, testes e build | Piloto real, UAT institucional, carga, recuperação MNE completa, PT/EN completo e decisão formal de produção pendentes |
|
||||
|
||||
Não se declara conclusão integral do prompt. Aprovação para desenvolver não torna disponíveis os documentos, mandatos ou regras que faltam. O próximo trabalho técnico é a evidência documental e a governação da validação, antes da publicação; não é necessário repetir autorização de desenvolvimento.
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,12 +20,12 @@ erDiagram
|
||||
|
||||
## Dicionário implementado
|
||||
|
||||
| Tabela | Campos e invariantes |
|
||||
| --- | --- |
|
||||
| existing_businesses | UUID, code único permanente, source_system/legacy_id conjuntamente nulos ou presentes e únicos, owner_user_id obrigatório, organization_id opcional, investment_item_id opcional e único, modality, status exclusivamente draft, classification internal/confidential/restricted, version_no positivo, payload cifrado, timestamps e deleted_at |
|
||||
| existing_business_objects | UUID, business_id, kind distinguindo company/participation/asset/property/unit/improvement/agricultural_investment/land_right/contract/license/authorization/encumbrance/valuation/capital_request, classification confidential/restricted, verification_status exclusivamente pending, payload cifrado, timestamps |
|
||||
| existing_business_mandates | UUID, business_id, representative_user_id, document_id, valid_from/valid_until coerentes, verification_status exclusivamente pending, payload cifrado, timestamps. Documento anexado não comprova legitimidade. Nenhum acesso é concedido pelo mandato pendente. |
|
||||
| existing_business_revisions | UUID, business_id/version_no únicos, actor_id, action, reason cifrada, snapshot cifrado, SHA-256 do snapshot e created_at. Trigger rejeita UPDATE/DELETE. |
|
||||
| Tabela | Campos e invariantes |
|
||||
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| existing_businesses | UUID, code único permanente, source_system/legacy_id conjuntamente nulos ou presentes e únicos, owner_user_id obrigatório, organization_id opcional, investment_item_id opcional e único, modality, status exclusivamente draft, classification internal/confidential/restricted, version_no positivo, payload cifrado, timestamps e deleted_at |
|
||||
| existing_business_objects | UUID, business_id, kind distinguindo company/participation/asset/property/unit/improvement/agricultural_investment/land_right/contract/license/authorization/encumbrance/valuation/capital_request, classification confidential/restricted, verification_status exclusivamente pending, payload cifrado, timestamps |
|
||||
| existing_business_mandates | UUID, business_id, representative_user_id, document_id, valid_from/valid_until coerentes, verification_status exclusivamente pending, payload cifrado, timestamps. Documento anexado não comprova legitimidade. Nenhum acesso é concedido pelo mandato pendente. |
|
||||
| existing_business_revisions | UUID, business_id/version_no únicos, actor_id, action, reason cifrada, snapshot cifrado, SHA-256 do snapshot e created_at. Trigger rejeita UPDATE/DELETE. |
|
||||
|
||||
Modalidades implementadas como vocabulário técnico, sem declarar admissibilidade: venda de empresa/participação, aumento de capital, investidor financeiro, parceiro estratégico, comprador, cessão, franchise, venda/arrendamento de activo, unidade, joint venture, sucessão, recuperação, expansão, internacionalização, imobiliário, investimento agrícola, direito fundiário e licenças mineira/florestal. As modalidades não fazem aprovação jurídica automática.
|
||||
|
||||
|
||||
@@ -35,13 +35,13 @@ O prompt não fornece uma sequência numerada RF-MNE; `traceability.csv` usa ide
|
||||
|
||||
## Gate
|
||||
|
||||
| Critério obrigatório da Fase 0 | Evidência | Resultado |
|
||||
| --- | --- | --- |
|
||||
| Dono do produto/autorização de desenvolvimento | Instrução explícita do utilizador | Confirmado para desenvolvimento |
|
||||
| Política de publicação, termos e base legal | Não fornecidos/validados nesta sessão | Pendente |
|
||||
| Equipa e mandatos de revisão | Apenas RACI proposto; sem nomeações | Pendente |
|
||||
| Ambiente seguro homologado | Testes isolados aprovados; revisão de segurança/UAT pendente | Parcial |
|
||||
| Carteira piloto e titulares confirmados | Nenhuma carteira MNE validada fornecida | Pendente |
|
||||
| Critério obrigatório da Fase 0 | Evidência | Resultado |
|
||||
| ---------------------------------------------- | ------------------------------------------------------------ | ------------------------------- |
|
||||
| Dono do produto/autorização de desenvolvimento | Instrução explícita do utilizador | Confirmado para desenvolvimento |
|
||||
| Política de publicação, termos e base legal | Não fornecidos/validados nesta sessão | Pendente |
|
||||
| Equipa e mandatos de revisão | Apenas RACI proposto; sem nomeações | Pendente |
|
||||
| Ambiente seguro homologado | Testes isolados aprovados; revisão de segurança/UAT pendente | Parcial |
|
||||
| Carteira piloto e titulares confirmados | Nenhuma carteira MNE validada fornecida | Pendente |
|
||||
|
||||
**No-Go para homologar o gate.** Artefactos de descoberta entregues; Fase 1 não iniciada, conforme regra explícita de paragem do prompt. Próxima fase proposta: modelo canónico e governação, com evidências e classificação por entidade.
|
||||
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
{
|
||||
"passed": true,
|
||||
"observations": [
|
||||
{
|
||||
"scenario": "autosave_and_reload",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"scenario": "responsive_summary",
|
||||
"width": 390,
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"scenario": "responsive_summary",
|
||||
"width": 1440,
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"scenario": "private_submission",
|
||||
"passed": true
|
||||
}
|
||||
],
|
||||
"errors": []
|
||||
"passed": true,
|
||||
"observations": [
|
||||
{
|
||||
"scenario": "autosave_and_reload",
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"scenario": "responsive_summary",
|
||||
"width": 390,
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"scenario": "responsive_summary",
|
||||
"width": 1440,
|
||||
"passed": true
|
||||
},
|
||||
{
|
||||
"scenario": "private_submission",
|
||||
"passed": true
|
||||
}
|
||||
],
|
||||
"errors": []
|
||||
}
|
||||
|
||||
@@ -20,16 +20,16 @@
|
||||
16. **Riscos residuais:** restantes campos e relações A; identidade normalizada; coordenadas/geometria; CRM MNE; scanner/upload; redis em operação; consentimentos separados; comprovativo/notificações; SLA; aprovações/publicação; tradução integral; testes de acessibilidade, carga e serviços externos. A conta de BD local ainda é superuser. Nenhuma alegação de prontidão para produção.
|
||||
17. **Go/No-Go:** Go para testar preparação privada local; **No-Go para entrega integral A–C ou Go-Live**. Os fluxos incompletos não foram expostos como funcionais.
|
||||
|
||||
| Requisito | Implementação actual | Estado |
|
||||
|---|---|---|
|
||||
| A: proponente/representação | Campos cifrados, mandato condicional, declaração e validação | Parcial; sem verificação de identidade/documentos |
|
||||
| A: modalidade/sector/valor | Taxonomia existente, FK, limites, moeda e natureza na submissão | Parcial; faltam intervalos e estruturas completas |
|
||||
| A: localização/contexto | Descrição privada | Geografia estruturada e ligações pendentes |
|
||||
| A: activos/direitos | Editor tipado existente; objecto aplicável exigido por modalidade | Parcial |
|
||||
| A: fontes/documentos | Referências declaradas | Upload/scanner pendentes |
|
||||
| A: rascunho/retoma/resumo | Autosave, versão, oito etapas, confirmação no servidor | Testado |
|
||||
| B: interesse/CRM | Serviço geral existente preservado | Integração MNE pendente |
|
||||
| C: revisão | Revisão privada e histórico existentes | Tarefas/SLA/dupla aprovação pendentes |
|
||||
| Requisito | Implementação actual | Estado |
|
||||
| --------------------------- | ----------------------------------------------------------------- | ------------------------------------------------- |
|
||||
| A: proponente/representação | Campos cifrados, mandato condicional, declaração e validação | Parcial; sem verificação de identidade/documentos |
|
||||
| A: modalidade/sector/valor | Taxonomia existente, FK, limites, moeda e natureza na submissão | Parcial; faltam intervalos e estruturas completas |
|
||||
| A: localização/contexto | Descrição privada | Geografia estruturada e ligações pendentes |
|
||||
| A: activos/direitos | Editor tipado existente; objecto aplicável exigido por modalidade | Parcial |
|
||||
| A: fontes/documentos | Referências declaradas | Upload/scanner pendentes |
|
||||
| A: rascunho/retoma/resumo | Autosave, versão, oito etapas, confirmação no servidor | Testado |
|
||||
| B: interesse/CRM | Serviço geral existente preservado | Integração MNE pendente |
|
||||
| C: revisão | Revisão privada e histórico existentes | Tarefas/SLA/dupla aprovação pendentes |
|
||||
|
||||
```powershell
|
||||
php artisan migrate --path=database/migrations/2026_09_29_200000_existing_business_form_sector.php
|
||||
|
||||
@@ -29,14 +29,14 @@ O dono do produto autorizou a progressão automática por todas as fases nesta s
|
||||
|
||||
## Estado por fase
|
||||
|
||||
| Fase | Estado e recomendação | Evidência / condição |
|
||||
| --- | --- | --- |
|
||||
| 0 — inventário | Parcial; Go condicionado para artefactos preparatórios | Código, índices, mapas e controlos inventariados abaixo. Excel localizado e inspeccionado após indicação do utilizador; documento original A.31 ainda não localizado. |
|
||||
| 1 — modelo e regras | Proposta de reconciliação; Go condicionado | Reutilizar o cadastro existente, sem uma segunda base GIS. Reconciliar a proposta com A.31 antes de fechar o modelo. |
|
||||
| 2 — serviços e importação | Auditor de XLSX implementado; restante pendente | Auditor local sem ligação à BD, com checksum, autorização de inspecção, limites e quarentena. Não é importador. |
|
||||
| 3 — mapas e CRM | Base preexistente parcial; No-Go para declarar conformidade GIS | Catálogo e zonas têm mapas/listas; autorização geral, precisão e origem completa mapa→CRM ainda incompletas. |
|
||||
| 4 — piloto e UAT | Testes sintéticos do auditor aprovados; No-Go para piloto real | Excel inspeccionado; faltam proveniência, classificação, mapeamento e amostras oficiais. |
|
||||
| 5 — staging/produção | No-Go | Sem UAT, carga, matriz de mandatos, fontes e ensaio operacional do módulo completo. Nenhum deploy realizado. |
|
||||
| Fase | Estado e recomendação | Evidência / condição |
|
||||
| ------------------------- | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 0 — inventário | Parcial; Go condicionado para artefactos preparatórios | Código, índices, mapas e controlos inventariados abaixo. Excel localizado e inspeccionado após indicação do utilizador; documento original A.31 ainda não localizado. |
|
||||
| 1 — modelo e regras | Proposta de reconciliação; Go condicionado | Reutilizar o cadastro existente, sem uma segunda base GIS. Reconciliar a proposta com A.31 antes de fechar o modelo. |
|
||||
| 2 — serviços e importação | Auditor de XLSX implementado; restante pendente | Auditor local sem ligação à BD, com checksum, autorização de inspecção, limites e quarentena. Não é importador. |
|
||||
| 3 — mapas e CRM | Base preexistente parcial; No-Go para declarar conformidade GIS | Catálogo e zonas têm mapas/listas; autorização geral, precisão e origem completa mapa→CRM ainda incompletas. |
|
||||
| 4 — piloto e UAT | Testes sintéticos do auditor aprovados; No-Go para piloto real | Excel inspeccionado; faltam proveniência, classificação, mapeamento e amostras oficiais. |
|
||||
| 5 — staging/produção | No-Go | Sem UAT, carga, matriz de mandatos, fontes e ensaio operacional do módulo completo. Nenhum deploy realizado. |
|
||||
|
||||
Não foram executadas migrations, importações, alterações de dados ou publicação de mapas nesta entrega GIS. O avanço técnico independente foi executado; as operações dependentes de fontes ausentes continuam pendentes, sem presumir os respectivos valores.
|
||||
|
||||
@@ -58,18 +58,18 @@ Pesquisaram-se nomes de ficheiros DOCX/XLSX/PDF no repositório (incluindo ignor
|
||||
|
||||
Os identificadores TECH abaixo são **provisórios locais**, não transcrição da A.31. Não se criou uma segunda sequência RF-GIS.
|
||||
|
||||
| RF-GEO | TECH provisório | Modelo / serviço previsto | API / evento previsto | Frontend e teste necessário | Estado |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 001 | TECH-GEO-CAD | Adaptar geo_locations, proveniência, validade, revisão imutável | locations GET/POST/PATCH; location.revised | Cadastro; códigos duplicados, ciclos e concorrência | Base parcial |
|
||||
| 002 | TECH-GEO-REL | Adaptar project_locations, papel, período, natureza | Relações explicitadas no contrato de localização | Vários projectos sem duplicar geometria | Base parcial |
|
||||
| 003 | TECH-GEO-MULTI | Localização principal por período; secundárias | Ficha do projecto/localizações | Projecto multiterritorial; unicidade principal | Parcial |
|
||||
| 004 | TECH-GEO-VALID | Geometria versionada, CRS original, método de transformação | geometries; validate; geometry.validated | Painel; SRID ausente, inválida, vazia e territorialmente divergente | PostGIS/zonas parcial |
|
||||
| 005 | TECH-GEO-AUTH | geo_authorizations, documento, entidade e validade | authorize-publication; authorization.revoked | Painel de autorização; expiração/revogação em todos os caminhos | Geral em falta |
|
||||
| 006 | TECH-GEO-PRECISION | Representação autorizada separada da geometria interna | Serviço comum a mapas, pesquisa, exportação e cache | Não reconstrução por bbox, raio, exportação ou respostas combinadas | Em falta |
|
||||
| 007 | TECH-GEO-INSTITUTION | map_validation_requests, protocolo, anexos, decisões | validation.requested/decided | Painel interno; contestação impede publicação | Em falta |
|
||||
| 008 | TECH-GEO-SUSPEND | Suspensão transaccional e tarefa de revalidação | suspend; map.suspended | Revogação imediata; cache e links históricos | Zonas parcial |
|
||||
| 009 | TECH-GEO-SEARCH | GiST, filtros aplicados à representação autorizada | maps/search, projects, corridors, clusters, nearby | Lista sincronizada; limites, paginação, teclado e carga | Parcial |
|
||||
| 010 | TECH-GEO-IMPORT | Batch, checksum, manifesto, relatório por linha, idempotência | import-jobs, errors, reconcile; import.reconciled | Upload privado; ZIP/XML hostil, duplicados e reconciliação | Auditor preparatório implementado |
|
||||
| RF-GEO | TECH provisório | Modelo / serviço previsto | API / evento previsto | Frontend e teste necessário | Estado |
|
||||
| ------ | -------------------- | --------------------------------------------------------------- | --------------------------------------------------- | ------------------------------------------------------------------- | --------------------------------- |
|
||||
| 001 | TECH-GEO-CAD | Adaptar geo_locations, proveniência, validade, revisão imutável | locations GET/POST/PATCH; location.revised | Cadastro; códigos duplicados, ciclos e concorrência | Base parcial |
|
||||
| 002 | TECH-GEO-REL | Adaptar project_locations, papel, período, natureza | Relações explicitadas no contrato de localização | Vários projectos sem duplicar geometria | Base parcial |
|
||||
| 003 | TECH-GEO-MULTI | Localização principal por período; secundárias | Ficha do projecto/localizações | Projecto multiterritorial; unicidade principal | Parcial |
|
||||
| 004 | TECH-GEO-VALID | Geometria versionada, CRS original, método de transformação | geometries; validate; geometry.validated | Painel; SRID ausente, inválida, vazia e territorialmente divergente | PostGIS/zonas parcial |
|
||||
| 005 | TECH-GEO-AUTH | geo_authorizations, documento, entidade e validade | authorize-publication; authorization.revoked | Painel de autorização; expiração/revogação em todos os caminhos | Geral em falta |
|
||||
| 006 | TECH-GEO-PRECISION | Representação autorizada separada da geometria interna | Serviço comum a mapas, pesquisa, exportação e cache | Não reconstrução por bbox, raio, exportação ou respostas combinadas | Em falta |
|
||||
| 007 | TECH-GEO-INSTITUTION | map_validation_requests, protocolo, anexos, decisões | validation.requested/decided | Painel interno; contestação impede publicação | Em falta |
|
||||
| 008 | TECH-GEO-SUSPEND | Suspensão transaccional e tarefa de revalidação | suspend; map.suspended | Revogação imediata; cache e links históricos | Zonas parcial |
|
||||
| 009 | TECH-GEO-SEARCH | GiST, filtros aplicados à representação autorizada | maps/search, projects, corridors, clusters, nearby | Lista sincronizada; limites, paginação, teclado e carga | Parcial |
|
||||
| 010 | TECH-GEO-IMPORT | Batch, checksum, manifesto, relatório por linha, idempotência | import-jobs, errors, reconcile; import.reconciled | Upload privado; ZIP/XML hostil, duplicados e reconciliação | Auditor preparatório implementado |
|
||||
|
||||
## Modelo, contratos e autorização propostos — Fase 1
|
||||
|
||||
|
||||
@@ -51,18 +51,18 @@ Relatórios indicam ordinal do registo CSV e códigos de erro, sem repetir o con
|
||||
|
||||
Validação executada: suite PHP completa com **168 testes e 1.707 verificações aprovadas** (32,524 s), incluindo seis novos testes unitários de CSV; PHPStan sem erros e Pint aprovado nos ficheiros PHP alterados. A extracção do PDF real terminou com 666 linhas de revisão. Não houve alteração de frontend nem alegação de testes visuais nesta fase.
|
||||
|
||||
| Critério | Situação e motivo | Impacto / próxima acção |
|
||||
| --- | --- | --- |
|
||||
| Original, checksum e revisão reproduzível | Executado | Conservar pacote privado |
|
||||
| Auditoria estrutural CSV | Executada | Não substitui carga ou aprovação |
|
||||
| Seed MZ, 11 províncias e capitais em staging | Por implementar | Criar seed idempotente com proveniência, sem localidades fictícias |
|
||||
| Batch persistente e carga atómica | Por implementar | Preservar rejeições e impedir alteração silenciosa de dados aprovados |
|
||||
| CSV final extraído do PDF | Colunas ambíguas | Reconciliar visualmente e validar antes de converter |
|
||||
| Vigência, licença e totais oficiais | Não comprovados | Obter evidência documental antes da publicação |
|
||||
| Selectores em cascata e consumidores | Por implementar | Criar componente acessível e validar UUID/hierarquia no servidor |
|
||||
| Atribuições FK, versões de limites e serviços espaciais | Por implementar | Completar schema e serviços sem duplicar cadastro |
|
||||
| OIDC, OpenAPI integral, p95 e carga | Por implementar | Completar integração e ensaios; sessão local continua com limites documentados |
|
||||
| UI editorial, acessibilidade e UAT | Por implementar | Entregar e testar jornadas no navegador |
|
||||
| Go-live integral | Não concluído | Resolver itens anteriores e verificar critérios do anexo |
|
||||
| Critério | Situação e motivo | Impacto / próxima acção |
|
||||
| ------------------------------------------------------- | ----------------- | ------------------------------------------------------------------------------ |
|
||||
| Original, checksum e revisão reproduzível | Executado | Conservar pacote privado |
|
||||
| Auditoria estrutural CSV | Executada | Não substitui carga ou aprovação |
|
||||
| Seed MZ, 11 províncias e capitais em staging | Por implementar | Criar seed idempotente com proveniência, sem localidades fictícias |
|
||||
| Batch persistente e carga atómica | Por implementar | Preservar rejeições e impedir alteração silenciosa de dados aprovados |
|
||||
| CSV final extraído do PDF | Colunas ambíguas | Reconciliar visualmente e validar antes de converter |
|
||||
| Vigência, licença e totais oficiais | Não comprovados | Obter evidência documental antes da publicação |
|
||||
| Selectores em cascata e consumidores | Por implementar | Criar componente acessível e validar UUID/hierarquia no servidor |
|
||||
| Atribuições FK, versões de limites e serviços espaciais | Por implementar | Completar schema e serviços sem duplicar cadastro |
|
||||
| OIDC, OpenAPI integral, p95 e carga | Por implementar | Completar integração e ensaios; sessão local continua com limites documentados |
|
||||
| UI editorial, acessibilidade e UAT | Por implementar | Entregar e testar jornadas no navegador |
|
||||
| Go-live integral | Não concluído | Resolver itens anteriores e verificar critérios do anexo |
|
||||
|
||||
Nenhuma migration ou novo endpoint neste incremento; nenhum território importado, publicado ou alterado. Reversão: retirar os componentes acrescentados, preservando os pacotes de auditoria; não há rollback de dados. O núcleo territorial anterior está documentado em [territorial-mvp.md](territorial-mvp.md). Os itens pendentes são trabalho restante, não novos pedidos de autorização.
|
||||
|
||||
@@ -1,95 +1,95 @@
|
||||
{
|
||||
"date": "2026-09-29T11:28:43.868Z",
|
||||
"observations": [
|
||||
"create_save_reopen_full_path",
|
||||
"validation_preserves_selection",
|
||||
"edit_save_reopen_and_descendant_reset",
|
||||
"network_failure_and_retry",
|
||||
"empty_search",
|
||||
"rapid_parent_change_cancels_and_clears_children"
|
||||
],
|
||||
"requests": [
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/management/opportunities",
|
||||
"payload": {
|
||||
"province_id": "10000000-0000-4000-8000-000000000002",
|
||||
"district_id": "10000000-0000-4000-8000-000000000003",
|
||||
"admin_post_id": "10000000-0000-4000-8000-000000000004",
|
||||
"locality_id": "10000000-0000-4000-8000-000000000005",
|
||||
"territory_version": "0",
|
||||
"reference": "geo-browser-1790681317445"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/management/opportunities/53",
|
||||
"payload": {
|
||||
"province_id": "10000000-0000-4000-8000-000000000002",
|
||||
"district_id": "10000000-0000-4000-8000-000000000003",
|
||||
"admin_post_id": "10000000-0000-4000-8000-000000000004",
|
||||
"locality_id": "10000000-0000-4000-8000-000000000005",
|
||||
"territory_version": "1",
|
||||
"reference": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/management/opportunities/53",
|
||||
"payload": {
|
||||
"province_id": "10000000-0000-4000-8000-000000000002",
|
||||
"district_id": "10000000-0000-4000-8000-000000000003",
|
||||
"admin_post_id": "",
|
||||
"locality_id": "",
|
||||
"territory_version": "1",
|
||||
"reference": "geo-browser-1790681317445"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": [
|
||||
{
|
||||
"path": "/management/opportunities/create",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/management/opportunities",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/management/opportunities/53/edit",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/management/opportunities/53/coordinates",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/management/opportunities/53/edit",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/management/opportunities",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/management/opportunities/53/edit",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/management/opportunities/53/coordinates",
|
||||
"status": 200
|
||||
}
|
||||
],
|
||||
"widths": [
|
||||
{
|
||||
"width": 390,
|
||||
"noOverflow": true
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"noOverflow": true
|
||||
}
|
||||
],
|
||||
"javascript_errors": 0,
|
||||
"scope": "Synthetic isolated QA"
|
||||
"date": "2026-09-29T11:28:43.868Z",
|
||||
"observations": [
|
||||
"create_save_reopen_full_path",
|
||||
"validation_preserves_selection",
|
||||
"edit_save_reopen_and_descendant_reset",
|
||||
"network_failure_and_retry",
|
||||
"empty_search",
|
||||
"rapid_parent_change_cancels_and_clears_children"
|
||||
],
|
||||
"requests": [
|
||||
{
|
||||
"method": "POST",
|
||||
"path": "/management/opportunities",
|
||||
"payload": {
|
||||
"province_id": "10000000-0000-4000-8000-000000000002",
|
||||
"district_id": "10000000-0000-4000-8000-000000000003",
|
||||
"admin_post_id": "10000000-0000-4000-8000-000000000004",
|
||||
"locality_id": "10000000-0000-4000-8000-000000000005",
|
||||
"territory_version": "0",
|
||||
"reference": "geo-browser-1790681317445"
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/management/opportunities/53",
|
||||
"payload": {
|
||||
"province_id": "10000000-0000-4000-8000-000000000002",
|
||||
"district_id": "10000000-0000-4000-8000-000000000003",
|
||||
"admin_post_id": "10000000-0000-4000-8000-000000000004",
|
||||
"locality_id": "10000000-0000-4000-8000-000000000005",
|
||||
"territory_version": "1",
|
||||
"reference": ""
|
||||
}
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/management/opportunities/53",
|
||||
"payload": {
|
||||
"province_id": "10000000-0000-4000-8000-000000000002",
|
||||
"district_id": "10000000-0000-4000-8000-000000000003",
|
||||
"admin_post_id": "",
|
||||
"locality_id": "",
|
||||
"territory_version": "1",
|
||||
"reference": "geo-browser-1790681317445"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": [
|
||||
{
|
||||
"path": "/management/opportunities/create",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/management/opportunities",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/management/opportunities/53/edit",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/management/opportunities/53/coordinates",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/management/opportunities/53/edit",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/management/opportunities",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/management/opportunities/53/edit",
|
||||
"status": 200
|
||||
},
|
||||
{
|
||||
"path": "/management/opportunities/53/coordinates",
|
||||
"status": 200
|
||||
}
|
||||
],
|
||||
"widths": [
|
||||
{
|
||||
"width": 390,
|
||||
"noOverflow": true
|
||||
},
|
||||
{
|
||||
"width": 1440,
|
||||
"noOverflow": true
|
||||
}
|
||||
],
|
||||
"javascript_errors": 0,
|
||||
"scope": "Synthetic isolated QA"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"sha256": null,
|
||||
"owner": null,
|
||||
"version": null,
|
||||
"received_at": null,
|
||||
"classification": null,
|
||||
"inspection_authorization": null,
|
||||
"original_crs": null,
|
||||
"publication_authorization": null,
|
||||
"column_mapping": null
|
||||
"sha256": null,
|
||||
"owner": null,
|
||||
"version": null,
|
||||
"received_at": null,
|
||||
"classification": null,
|
||||
"inspection_authorization": null,
|
||||
"original_crs": null,
|
||||
"publication_authorization": null,
|
||||
"column_mapping": null
|
||||
}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"source_sha256": "1e36c769c4767420b10f1ff5ee0c2955cdb1b03c98179c5cad549e832ffb1f15",
|
||||
"ddl_cells": "SQL_DDL!A1:A14",
|
||||
"insert_cells": "SQL_INSERTS!B2:B21415",
|
||||
"counts": {
|
||||
"provincia": 11,
|
||||
"distrito": 150,
|
||||
"posto_administrativo": 451,
|
||||
"localidade": 1377,
|
||||
"bairro_urbano": 2838,
|
||||
"aldeia_quarteirao": 16583
|
||||
},
|
||||
"total": 21410,
|
||||
"auxiliary_parents_from_source": 1075,
|
||||
"quality_rows": 1147,
|
||||
"has_coordinates": false,
|
||||
"warning": "1075 parents explicitly marked NAO INFORMADO by the workbook; these are unresolved references, not authenticated administrative designations."
|
||||
"source_sha256": "1e36c769c4767420b10f1ff5ee0c2955cdb1b03c98179c5cad549e832ffb1f15",
|
||||
"ddl_cells": "SQL_DDL!A1:A14",
|
||||
"insert_cells": "SQL_INSERTS!B2:B21415",
|
||||
"counts": {
|
||||
"provincia": 11,
|
||||
"distrito": 150,
|
||||
"posto_administrativo": 451,
|
||||
"localidade": 1377,
|
||||
"bairro_urbano": 2838,
|
||||
"aldeia_quarteirao": 16583
|
||||
},
|
||||
"total": 21410,
|
||||
"auxiliary_parents_from_source": 1075,
|
||||
"quality_rows": 1147,
|
||||
"has_coordinates": false,
|
||||
"warning": "1075 parents explicitly marked NAO INFORMADO by the workbook; these are unresolved references, not authenticated administrative designations."
|
||||
}
|
||||
|
||||
@@ -12,13 +12,13 @@ O índice legado de código/tipo é global, mais restritivo que código de prov
|
||||
|
||||
Migration `2026_09_28_150000_territorial_registry.php` acrescenta cinco tabelas:
|
||||
|
||||
| Tabela | Finalidade e controlos |
|
||||
| --- | --- |
|
||||
| geo_sources | Fonte, entidade, referência, licença, checksum, versão, data, CRS original e estado; checksum/versão únicos; editor/revisor e evidência da decisão |
|
||||
| geo_territory_profiles | FK única para geo_locations; fonte, ISO2/ISO3 opcionais únicos, capital por FK, vigência, estado, editor/revisor/publicador |
|
||||
| geo_names | Nomes/idiomas por FK; nome normalizado e oficial; unicidade por entidade/idioma/nome e um oficial por idioma |
|
||||
| geo_territory_revisions | Snapshot e motivo cifrados, actor, versão e checksum; UPDATE/DELETE recusados pelo trigger |
|
||||
| geo_write_receipts | Actor, chave UUID, escopo, hash do pedido e resposta cifrada; unicidade e serialização contra duplicação concorrente |
|
||||
| Tabela | Finalidade e controlos |
|
||||
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| geo_sources | Fonte, entidade, referência, licença, checksum, versão, data, CRS original e estado; checksum/versão únicos; editor/revisor e evidência da decisão |
|
||||
| geo_territory_profiles | FK única para geo_locations; fonte, ISO2/ISO3 opcionais únicos, capital por FK, vigência, estado, editor/revisor/publicador |
|
||||
| geo_names | Nomes/idiomas por FK; nome normalizado e oficial; unicidade por entidade/idioma/nome e um oficial por idioma |
|
||||
| geo_territory_revisions | Snapshot e motivo cifrados, actor, versão e checksum; UPDATE/DELETE recusados pelo trigger |
|
||||
| geo_write_receipts | Actor, chave UUID, escopo, hash do pedido e resposta cifrada; unicidade e serialização contra duplicação concorrente |
|
||||
|
||||
Hierarquia validada no serviço: país sem pai, província→país, distrito→província, posto→distrito e localidade→posto. Tipo é imutável; pai apagado, salto de nível e código/nome duplicado sob o mesmo pai são rejeitados. Alterações são serializadas, com versão esperada, snapshot e invalidação de publicação. Excepções de localidades sem posto ainda não têm workflow; não se cria pai fictício.
|
||||
|
||||
|
||||
+1166
-1188
File diff suppressed because it is too large
Load Diff
@@ -31,15 +31,15 @@ A página agora oferece os percursos “Tenho um projecto definido” e “Tenho
|
||||
## Evidência
|
||||
|
||||
- Browser Chromium (Playwright), desktop e viewport móvel de 390 × 844:
|
||||
- `/oportunidades`: HTTP 200, 6 oportunidades publicadas no ambiente;
|
||||
- `/carteira?kind=cluster`: HTTP 200, 8 dossiers, abertura de ficha sem erro SQL;
|
||||
- filtros legados `/clusters`, `/corredores` e `/projectos-ancora` redireccionam correctamente; a selecção de uma província aplicou o filtro. A província escolhida não tinha dossiers de cluster publicados e mostrou o estado vazio;
|
||||
- pesquisa deliberadamente sem correspondências: estado vazio útil;
|
||||
- `/carteira?kind=project`: HTTP 200, 15 dossiers; a ficha informa quando o estado não está confirmado e não sugere concurso;
|
||||
- `/zonas?category=zee`: HTTP 200, 0 registos publicados, sem dados demonstrativos;
|
||||
- `/negocios`: HTTP 200, 0 anúncios, sem erro React após corrigir a paginação;
|
||||
- `/como-investir`: ambos os percursos, selecção da ideia e contexto de actividade renderizam;
|
||||
- menu móvel expande, foco por teclado avança e não foram observados erros JavaScript.
|
||||
- `/oportunidades`: HTTP 200, 6 oportunidades publicadas no ambiente;
|
||||
- `/carteira?kind=cluster`: HTTP 200, 8 dossiers, abertura de ficha sem erro SQL;
|
||||
- filtros legados `/clusters`, `/corredores` e `/projectos-ancora` redireccionam correctamente; a selecção de uma província aplicou o filtro. A província escolhida não tinha dossiers de cluster publicados e mostrou o estado vazio;
|
||||
- pesquisa deliberadamente sem correspondências: estado vazio útil;
|
||||
- `/carteira?kind=project`: HTTP 200, 15 dossiers; a ficha informa quando o estado não está confirmado e não sugere concurso;
|
||||
- `/zonas?category=zee`: HTTP 200, 0 registos publicados, sem dados demonstrativos;
|
||||
- `/negocios`: HTTP 200, 0 anúncios, sem erro React após corrigir a paginação;
|
||||
- `/como-investir`: ambos os percursos, selecção da ideia e contexto de actividade renderizam;
|
||||
- menu móvel expande, foco por teclado avança e não foram observados erros JavaScript.
|
||||
- Evidência automatizada: [JSON do browser](pilot/investmoz-catalogue-guide-browser-2026-10-04.json) e [captura móvel](pilot/investmoz-guide-mobile-2026-10-04.png).
|
||||
- `npm.cmd run types:check`: passou.
|
||||
- `npm.cmd run build`: passou. Avisos: dependência opcional `fontaine` ausente e chunk `services` acima de 500 kB.
|
||||
@@ -57,6 +57,7 @@ A página agora oferece os percursos “Tenho um projecto definido” e “Tenho
|
||||
- No catálogo real carregado, há seis oportunidades provenientes do `OpportunitySeeder`, todas sem `last_validated_at`, e zero anúncios de negócio. Filtros por Nampula e agronegócio devolveram um cartão cada; o CTA anónimo encaminha para login. A origem seed não prova autorização comercial. O catálogo e detalhe agora avisam quando a data de validação documental não está registada. Evidência: [JSON](pilot/authorized-opportunity-filter-browser-2026-10-04.json).
|
||||
- `npm.cmd run types:check`, `npm.cmd run build` e `node --test tests/frontend/*.test.mjs`: passaram (139/139). Avisos de build: dependência opcional `fontaine` ausente e chunk `services` acima de 500 kB.
|
||||
- Os resultados e decisões de fonte para as 50 relações, 15 projectos âncora e catálogos constam em [revisão documental](catalogue-review-2026-10-04.md).
|
||||
|
||||
## Pendências e limites
|
||||
|
||||
1. **50 relações candidatas:** falta atribuir revisor interno e registar fonte/evidência individual com vigência. A decisão pendente é a natureza de cada ligação (oficial, influência, proximidade, potencial, histórica) ou remoção. Não se exige certificador externo; relações sem prova ficam fora de pesquisas oficiais. Os catálogos e fichas continuam acessíveis.
|
||||
@@ -65,6 +66,7 @@ A página agora oferece os percursos “Tenho um projecto definido” e “Tenho
|
||||
4. **Negócios e oportunidades:** os seis cartões disponíveis são seeds sem data de validação; o catálogo de negócios tem 0 anúncios. Falta ao menos um registo autorizado com fonte e data para verificar anúncio e pretensão real. O catálogo continua consultável com aviso; não há confirmação de oferta.
|
||||
5. **ZFE:** a sigla não tem definição/fonte encontrada; pedir a referência original antes de criar categoria.
|
||||
6. **Dispositivo físico:** falta ensaio num telemóvel físico; viewport Chromium não substitui compatibilidade real.
|
||||
|
||||
## Diff lógico nesta entrega
|
||||
|
||||
- `database/migrations/2026_10_03_180000_govern_investment_structure_relations.php`: migration aditiva pendente executada nas três tabelas; não foi editada.
|
||||
@@ -85,4 +87,4 @@ A página agora oferece os percursos “Tenho um projecto definido” e “Tenho
|
||||
|
||||
## Gate
|
||||
|
||||
**Proposta: pendente com decisões delimitadas, sem bloquear o portal globalmente.** A associação privada passou no teste browser autenticado e no teste PHP. As decisões humanas em falta são: rever os 50 vínculos com fonte/evidência; confirmar documentalmente os 15 projectos; fornecer fonte legal e registo real de zona; fornecer anúncios autorizados para ensaiar uma pretensão real; esclarecer ZFE; realizar ensaio num telemóvel físico. Os percursos públicos já verificados continuam disponíveis com os avisos registados.
|
||||
**Proposta: pendente com decisões delimitadas, sem bloquear o portal globalmente.** A associação privada passou no teste browser autenticado e no teste PHP. As decisões humanas em falta são: rever os 50 vínculos com fonte/evidência; confirmar documentalmente os 15 projectos; fornecer fonte legal e registo real de zona; fornecer anúncios autorizados para ensaiar uma pretensão real; esclarecer ZFE; realizar ensaio num telemóvel físico. Os percursos públicos já verificados continuam disponíveis com os avisos registados.
|
||||
|
||||
@@ -9,17 +9,17 @@ O acervo disponível permite identificar diplomas e pontos de revisão, mas não
|
||||
|
||||
## Matriz de fontes e efeitos
|
||||
|
||||
| Matéria | Diploma ou indicação | Fonte consultada e qualidade | Vigência/estado nesta triagem | Área do produto | Teste/controlo | Aprovação |
|
||||
| --- | --- | --- | --- | --- | --- | --- |
|
||||
| Lei de Terras | Lei n.º 19/97, de 1 de Outubro; a reprodução consultada contém, entre outros, arts. 12–15 (aquisição, titulação, registo e prova), art. 19 (plano de exploração) e art. 24 (participação comunitária) | PDF “Lei de terra” na página normativa do PDUL, programa governamental. É reprodução útil para localizar dispositivos, ainda sem confronto página a página com o BR | Lei-base histórica identificada. Consolidação/alterações e vigência artigo a artigo por confirmar no BR e com revisão jurídica | Avisos, categorias de pedido, direitos adquiridos por ocupação e formulação de campos | Testar que ausência de título/registo não é tratada pelo portal como inexistência de direito; nenhuma inferência jurídica a partir de mapa | Pendente de confirmação por jurista fundiário |
|
||||
| Regulamento rural | Decreto n.º 66/98, de 8 de Dezembro | PDF “Regulamento da Lei de Terra” na página normativa do PDUL; reprodução governamental. Fontes secundárias consultadas indicam alterações pelos Decretos n.º 1/2003, 50/2007 e 43/2010; falta conferir os diplomas e artigos no BR | Texto-base identificado; consolidação e vigência por dispositivo pendentes | Checklist por modalidade, consulta comunitária, plano e processo rural | Não tornar campos ou scans em obrigação até completar a matriz artigo→campo→evidência | Pendente |
|
||||
| Solo urbano | Decreto n.º 60/2006, de 26 de Dezembro, Regulamento do Solo Urbano | PDF ligado pela página normativa do PDUL; reprodução governamental | Identificado; relação com actos posteriores, regulamentos locais e reformas de ordenamento a validar por jurisdição | Ramificação urbana distinta; não reutilizar automaticamente procedimentos rurais | Caso de teste urbano não deve herdar checklist rural por omissão | Pendente |
|
||||
| Taxas fundiárias | Diploma Ministerial n.º 47/2020; o sumário do BR n.º 176 (14-09-2020) diz que actualiza taxas do Decreto 66/98 e Decreto 77/99 e revoga o DM n.º 144/2010 | Sumário da Imprensa Nacional de Moçambique (fonte oficial), sem o texto integral/quadros revistos nesta revisão | Existência da actualização confirmada pelo sumário; valor, moeda, âmbito, aplicação temporal e eventual alteração posterior não confirmados | Biblioteca informativa; não calcular nem pedir pagamento no formulário | As taxas não podem ter fallback demonstrativo ou valor de 2010; assegurar ausência de cálculo activo até fonte integral aprovada | Pendente |
|
||||
| Proposta de nova Lei de Terras | Proposta de Revisão da Lei de Terras, com PDF “Lei de Terras” na página do Parlamento | Parlamento de Moçambique lista a matéria na secção “Projectos e Propostas de Lei”; comunicado do Conselho de Ministros de 28-10-2025 anuncia submissão de proposta à AR | **Proposta, não regra activa.** A pesquisa dirigida não localizou promulgação/publicação no BR. A confirmação do estado legislativo deve ser repetida antes de qualquer actualização de produção | Registo histórico e comparação futura, sem condicionar o formulário activo | Teste documental de que conteúdo proposto não altera regras publicadas nem snapshots | Não aprovada para execução |
|
||||
| Regulamento de ordenamento territorial | Lei n.º 19/2007 e Decreto n.º 23/2008; a actividade governamental “Nosso Território” também identifica o Decreto n.º 60/2006 no pacote em revisão | Página oficial da actividade do MTA sobre revisão do pacote; cópias normativas PDUL são referências secundárias governamentais | A página evidencia processo de revisão, não entrada em vigor de projecto. Diplomas, alterações e incidência concreta no DUAT precisam de confronto com BR | Triagem territorial e avisos de jurisdição; nenhuma decisão cartográfica | Tratar intersecção como alerta, nunca como decisão de titularidade/disponibilidade | Pendente |
|
||||
| Metodologia de regularização por ocupação | Diploma Ministerial n.º 2/2020, referido no Guião de Gestão de Terras Urbanas do PDUL | Guia governamental de gestão urbana (fonte administrativa/técnica, não substitui o diploma); cópia integral do DM e âmbito ainda não revistos aqui | Não activar requisitos ou prazo de ocupação com base no guia apenas | Campos de antecedentes e apoio a reconhecimento/regularização | Requer fonte primária, artigo, jurisdição, vigência e aprovação antes de checklist | Pendente |
|
||||
| Formulários digitalizados do utilizador | Sete PDFs nas subpastas `Formulario`, `exploracao` e `Consulta` | Inventário local, nomes e hashes no [source-inventory.md](source-inventory.md); classificação vem apenas da pasta | Conteúdo, emissor, data, jurisdição e actualidade por OCR e conferência visual | Referência interna reservada, sem download público nem preenchimento automático | OCR em cópia separada; cada campo conferido contra imagem; ilegível fica pendente | Pendente |
|
||||
| Guia Nacional mencionado | Não localizado como documento legível e autenticado no acervo disponível | Pasta local indicada foi pesquisada; `DUAT.docx` separado estava bloqueado por outro processo e não foi lido | Não confirmado | Sem checklist jurídica | Não preencher lacunas por inferência | Pendente |
|
||||
| Matéria | Diploma ou indicação | Fonte consultada e qualidade | Vigência/estado nesta triagem | Área do produto | Teste/controlo | Aprovação |
|
||||
| ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------- |
|
||||
| Lei de Terras | Lei n.º 19/97, de 1 de Outubro; a reprodução consultada contém, entre outros, arts. 12–15 (aquisição, titulação, registo e prova), art. 19 (plano de exploração) e art. 24 (participação comunitária) | PDF “Lei de terra” na página normativa do PDUL, programa governamental. É reprodução útil para localizar dispositivos, ainda sem confronto página a página com o BR | Lei-base histórica identificada. Consolidação/alterações e vigência artigo a artigo por confirmar no BR e com revisão jurídica | Avisos, categorias de pedido, direitos adquiridos por ocupação e formulação de campos | Testar que ausência de título/registo não é tratada pelo portal como inexistência de direito; nenhuma inferência jurídica a partir de mapa | Pendente de confirmação por jurista fundiário |
|
||||
| Regulamento rural | Decreto n.º 66/98, de 8 de Dezembro | PDF “Regulamento da Lei de Terra” na página normativa do PDUL; reprodução governamental. Fontes secundárias consultadas indicam alterações pelos Decretos n.º 1/2003, 50/2007 e 43/2010; falta conferir os diplomas e artigos no BR | Texto-base identificado; consolidação e vigência por dispositivo pendentes | Checklist por modalidade, consulta comunitária, plano e processo rural | Não tornar campos ou scans em obrigação até completar a matriz artigo→campo→evidência | Pendente |
|
||||
| Solo urbano | Decreto n.º 60/2006, de 26 de Dezembro, Regulamento do Solo Urbano | PDF ligado pela página normativa do PDUL; reprodução governamental | Identificado; relação com actos posteriores, regulamentos locais e reformas de ordenamento a validar por jurisdição | Ramificação urbana distinta; não reutilizar automaticamente procedimentos rurais | Caso de teste urbano não deve herdar checklist rural por omissão | Pendente |
|
||||
| Taxas fundiárias | Diploma Ministerial n.º 47/2020; o sumário do BR n.º 176 (14-09-2020) diz que actualiza taxas do Decreto 66/98 e Decreto 77/99 e revoga o DM n.º 144/2010 | Sumário da Imprensa Nacional de Moçambique (fonte oficial), sem o texto integral/quadros revistos nesta revisão | Existência da actualização confirmada pelo sumário; valor, moeda, âmbito, aplicação temporal e eventual alteração posterior não confirmados | Biblioteca informativa; não calcular nem pedir pagamento no formulário | As taxas não podem ter fallback demonstrativo ou valor de 2010; assegurar ausência de cálculo activo até fonte integral aprovada | Pendente |
|
||||
| Proposta de nova Lei de Terras | Proposta de Revisão da Lei de Terras, com PDF “Lei de Terras” na página do Parlamento | Parlamento de Moçambique lista a matéria na secção “Projectos e Propostas de Lei”; comunicado do Conselho de Ministros de 28-10-2025 anuncia submissão de proposta à AR | **Proposta, não regra activa.** A pesquisa dirigida não localizou promulgação/publicação no BR. A confirmação do estado legislativo deve ser repetida antes de qualquer actualização de produção | Registo histórico e comparação futura, sem condicionar o formulário activo | Teste documental de que conteúdo proposto não altera regras publicadas nem snapshots | Não aprovada para execução |
|
||||
| Regulamento de ordenamento territorial | Lei n.º 19/2007 e Decreto n.º 23/2008; a actividade governamental “Nosso Território” também identifica o Decreto n.º 60/2006 no pacote em revisão | Página oficial da actividade do MTA sobre revisão do pacote; cópias normativas PDUL são referências secundárias governamentais | A página evidencia processo de revisão, não entrada em vigor de projecto. Diplomas, alterações e incidência concreta no DUAT precisam de confronto com BR | Triagem territorial e avisos de jurisdição; nenhuma decisão cartográfica | Tratar intersecção como alerta, nunca como decisão de titularidade/disponibilidade | Pendente |
|
||||
| Metodologia de regularização por ocupação | Diploma Ministerial n.º 2/2020, referido no Guião de Gestão de Terras Urbanas do PDUL | Guia governamental de gestão urbana (fonte administrativa/técnica, não substitui o diploma); cópia integral do DM e âmbito ainda não revistos aqui | Não activar requisitos ou prazo de ocupação com base no guia apenas | Campos de antecedentes e apoio a reconhecimento/regularização | Requer fonte primária, artigo, jurisdição, vigência e aprovação antes de checklist | Pendente |
|
||||
| Formulários digitalizados do utilizador | Sete PDFs nas subpastas `Formulario`, `exploracao` e `Consulta` | Inventário local, nomes e hashes no [source-inventory.md](source-inventory.md); classificação vem apenas da pasta | Conteúdo, emissor, data, jurisdição e actualidade por OCR e conferência visual | Referência interna reservada, sem download público nem preenchimento automático | OCR em cópia separada; cada campo conferido contra imagem; ilegível fica pendente | Pendente |
|
||||
| Guia Nacional mencionado | Não localizado como documento legível e autenticado no acervo disponível | Pasta local indicada foi pesquisada; `DUAT.docx` separado estava bloqueado por outro processo e não foi lido | Não confirmado | Sem checklist jurídica | Não preencher lacunas por inferência | Pendente |
|
||||
|
||||
## Salvaguardas funcionais que já podem permanecer
|
||||
|
||||
|
||||
@@ -2,25 +2,25 @@
|
||||
|
||||
**Revisão:** 2026-10-03. Esta matriz descreve o código presente no repositório; não certifica a validade jurídica de modelos nem a aprovação da funcionalidade para submissão oficial.
|
||||
|
||||
| Requisito do prompt | Fonte disponível/estado | Campo ou percurso | Validação/evidência | Teste/estado |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Guia público e limites do InvestMoz | `resources/js/pages/land-assistance/guide.tsx`; não depende de requisito jurídico específico | `/terra-duat` | Aviso de que não atribui DUAT nem confirma disponibilidade ou direitos | `LandAssistanceTest`: guia público 200; coberto |
|
||||
| Formulário em branco público | Modelo InvestMoz `land-assistance-v1`; scans oficiais não identificados | `/terra-duat/formulario-em-branco` | Impressão/guardar como PDF; explicitamente não oficial e não preenchível | `LandAssistanceTest`: rota pública 200; coberto como modelo imprimível |
|
||||
| Formulário progressivo, rascunho, retoma e versão | Modelo interno `land-assistance-v1` | `/investidor/terra-duat/novo`, edição e acompanhamento | Autenticação, cifra do payload, versão optimista, histórico cifrado e eventos sem conteúdo pessoal | Testes de feature cobrem criação, propriedade, concorrência e suporte; sem evidência de ensaio browser DUAT nesta revisão |
|
||||
| Categorias de apoio e actos posteriores | Vocabulário de produto do prompt; modalidades e efeitos legais pendentes | `payload.support_kind`, `request_description` | Valores delimitados no FormRequest; UI não declara elegibilidade ou aprovação | Parcial: classificação jurídica detalhada depende das fontes oficiais |
|
||||
| Requerente, contacto, representação e minimização | Requisitos do prompt; fundamento de recolha por acto concreto ainda carece de fonte | `applicant_category`, contacto, representação, NUIT | Campos opcionais no rascunho; apoio exige um contacto utilizável e consentimento | Email ou telefone isolado coberto por `LandAssistanceTest`; não é criado endereço sintético |
|
||||
| Localização, várias parcelas e hierarquia territorial | Catálogo territorial da aplicação; não é cadastro fundiário | `parcels[]`, province/district/admin_post/locality, área e geometria declaradas | UUID, tipos e sequência pai/filho; até 10 parcelas; catálogo/mapa não confirmam direitos | Validação servidor implementada; cenário visual de cascata e retoma do DUAT sem teste browser registado |
|
||||
| Ocupação, usos, antecedentes e conflito | Declarações do requerente, não inferências do portal | `known_occupants`, `known_improvements`, `prior_right_status`, `conflict_status` | Opções incluem “não sei” e estados qualificados; sem conclusão cadastral | Coberto pelo formulário, sem regra fundiária automática |
|
||||
| Checklist personalizada por modalidade/jurisdição/fase | **Pendente:** guia, Lei/regulamentos e procedimentos vigentes não foram obtidos em cópia legível/reconciliada | Não há checklist jurídica executável | Não tornar itens dos scans documentos obrigatórios; não inventar fonte, prazo, responsável oficial ou evidência | **Bloqueado por fonte/revisão jurídica** |
|
||||
| Plano de exploração | Scans `exploracao/scan0003.pdf` e `scan0004.pdf`, classificados só pela pasta; OCR e conferência pendentes | `exploration_plan.activity_rows[]` | Linhas facultativas; período livre, existente/planeado, unidade; não soma áreas anuais | Validação estrutural no FormRequest; OCR não reconciliado |
|
||||
| Consulta comunitária e pareceres | Scans `Consulta/scan*.pdf`, ainda sem OCR/revisão | `consultation.participants[]`, intervenções, oposição, acordos, documentos referidos | Até 200 participantes; estados comunicados/preparatórios; não gera parecer, assinatura ou acta | Cobertura do formulário; sem validação de execução/interpretação legal |
|
||||
| Anexos e documentos privados | Armazenamento privado e registo `documents` existente | Documentos no caso | Tipos/tamanho limitados; acesso autenticado/proprietário; quarentena não descarregável | Implementado com bloqueio seguro; **antimalware não configurado**, sem libertação automática |
|
||||
| Cópia preenchida, histórico e referência | Modelo InvestMoz `land-assistance-v1` | `/imprimir` | Reflecte payload guardado e versão; referência interna; sem assinatura/número oficial simulado | HTML imprimível, não PDF electrónico; correspondência visual/browser ainda sem evidência nesta revisão |
|
||||
| Apoio e CRM | `InterestSubmission`; migration aditiva `2026_10_03_210000_make_interest_request_email_nullable.php` | `solicitar-apoio` | Email ou telefone; consentimento, transacção, idempotência e recibo interno | Telefone isolado persiste email `NULL`; `LandAssistanceTest` cobre percurso e rollback protegido. Consumidores downstream com email obrigatório ainda carecem de validação operacional |
|
||||
| Acompanhamento externo e gestão | Nenhuma API pública de DUAT confirmada | Área privada e gestão | Estados internos; factos externos apenas informados ou sustentados por evidência; encaminhamento preparado não é envio | Implementado sem simular integração pública |
|
||||
| Direitos, disponibilidade e intersecções | Não confirmáveis por dados/catálogos locais | Localização e documentos | Ausência de registo/intersecção não prova ausência de direitos; conflitos não são decididos | Salvaguarda de interface presente; confirmação cabe à entidade competente |
|
||||
| Segurança, auditoria e reversão | PostgreSQL e padrões locais | Migrations `2026_10_03_200000_create_land_assistance_cases.php` e `2026_10_03_210000_make_interest_request_email_nullable.php` | Payload/revisões cifrados; controlo de propriedade; concorrência optimista; eventos sem payload; upload privado em quarentena; rollback protegido | PostgreSQL 17 QA descartável: `LandAssistanceTest` 6/6 (46 asserções); nenhuma migration aplicada à base operacional |
|
||||
| Fontes e versões históricas | Inventário read-only em `source-inventory.md` | `land-assistance-v1` | SHA-256 dos sete PDFs; originais fora de `public/`; snapshots preservam versões | Identidade dos ficheiros verificada anteriormente; conteúdo/actualidade pendentes |
|
||||
| Requisito do prompt | Fonte disponível/estado | Campo ou percurso | Validação/evidência | Teste/estado |
|
||||
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Guia público e limites do InvestMoz | `resources/js/pages/land-assistance/guide.tsx`; não depende de requisito jurídico específico | `/terra-duat` | Aviso de que não atribui DUAT nem confirma disponibilidade ou direitos | `LandAssistanceTest`: guia público 200; coberto |
|
||||
| Formulário em branco público | Modelo InvestMoz `land-assistance-v1`; scans oficiais não identificados | `/terra-duat/formulario-em-branco` | Impressão/guardar como PDF; explicitamente não oficial e não preenchível | `LandAssistanceTest`: rota pública 200; coberto como modelo imprimível |
|
||||
| Formulário progressivo, rascunho, retoma e versão | Modelo interno `land-assistance-v1` | `/investidor/terra-duat/novo`, edição e acompanhamento | Autenticação, cifra do payload, versão optimista, histórico cifrado e eventos sem conteúdo pessoal | Testes de feature cobrem criação, propriedade, concorrência e suporte; sem evidência de ensaio browser DUAT nesta revisão |
|
||||
| Categorias de apoio e actos posteriores | Vocabulário de produto do prompt; modalidades e efeitos legais pendentes | `payload.support_kind`, `request_description` | Valores delimitados no FormRequest; UI não declara elegibilidade ou aprovação | Parcial: classificação jurídica detalhada depende das fontes oficiais |
|
||||
| Requerente, contacto, representação e minimização | Requisitos do prompt; fundamento de recolha por acto concreto ainda carece de fonte | `applicant_category`, contacto, representação, NUIT | Campos opcionais no rascunho; apoio exige um contacto utilizável e consentimento | Email ou telefone isolado coberto por `LandAssistanceTest`; não é criado endereço sintético |
|
||||
| Localização, várias parcelas e hierarquia territorial | Catálogo territorial da aplicação; não é cadastro fundiário | `parcels[]`, province/district/admin_post/locality, área e geometria declaradas | UUID, tipos e sequência pai/filho; até 10 parcelas; catálogo/mapa não confirmam direitos | Validação servidor implementada; cenário visual de cascata e retoma do DUAT sem teste browser registado |
|
||||
| Ocupação, usos, antecedentes e conflito | Declarações do requerente, não inferências do portal | `known_occupants`, `known_improvements`, `prior_right_status`, `conflict_status` | Opções incluem “não sei” e estados qualificados; sem conclusão cadastral | Coberto pelo formulário, sem regra fundiária automática |
|
||||
| Checklist personalizada por modalidade/jurisdição/fase | **Pendente:** guia, Lei/regulamentos e procedimentos vigentes não foram obtidos em cópia legível/reconciliada | Não há checklist jurídica executável | Não tornar itens dos scans documentos obrigatórios; não inventar fonte, prazo, responsável oficial ou evidência | **Bloqueado por fonte/revisão jurídica** |
|
||||
| Plano de exploração | Scans `exploracao/scan0003.pdf` e `scan0004.pdf`, classificados só pela pasta; OCR e conferência pendentes | `exploration_plan.activity_rows[]` | Linhas facultativas; período livre, existente/planeado, unidade; não soma áreas anuais | Validação estrutural no FormRequest; OCR não reconciliado |
|
||||
| Consulta comunitária e pareceres | Scans `Consulta/scan*.pdf`, ainda sem OCR/revisão | `consultation.participants[]`, intervenções, oposição, acordos, documentos referidos | Até 200 participantes; estados comunicados/preparatórios; não gera parecer, assinatura ou acta | Cobertura do formulário; sem validação de execução/interpretação legal |
|
||||
| Anexos e documentos privados | Armazenamento privado e registo `documents` existente | Documentos no caso | Tipos/tamanho limitados; acesso autenticado/proprietário; quarentena não descarregável | Implementado com bloqueio seguro; **antimalware não configurado**, sem libertação automática |
|
||||
| Cópia preenchida, histórico e referência | Modelo InvestMoz `land-assistance-v1` | `/imprimir` | Reflecte payload guardado e versão; referência interna; sem assinatura/número oficial simulado | HTML imprimível, não PDF electrónico; correspondência visual/browser ainda sem evidência nesta revisão |
|
||||
| Apoio e CRM | `InterestSubmission`; migration aditiva `2026_10_03_210000_make_interest_request_email_nullable.php` | `solicitar-apoio` | Email ou telefone; consentimento, transacção, idempotência e recibo interno | Telefone isolado persiste email `NULL`; `LandAssistanceTest` cobre percurso e rollback protegido. Consumidores downstream com email obrigatório ainda carecem de validação operacional |
|
||||
| Acompanhamento externo e gestão | Nenhuma API pública de DUAT confirmada | Área privada e gestão | Estados internos; factos externos apenas informados ou sustentados por evidência; encaminhamento preparado não é envio | Implementado sem simular integração pública |
|
||||
| Direitos, disponibilidade e intersecções | Não confirmáveis por dados/catálogos locais | Localização e documentos | Ausência de registo/intersecção não prova ausência de direitos; conflitos não são decididos | Salvaguarda de interface presente; confirmação cabe à entidade competente |
|
||||
| Segurança, auditoria e reversão | PostgreSQL e padrões locais | Migrations `2026_10_03_200000_create_land_assistance_cases.php` e `2026_10_03_210000_make_interest_request_email_nullable.php` | Payload/revisões cifrados; controlo de propriedade; concorrência optimista; eventos sem payload; upload privado em quarentena; rollback protegido | PostgreSQL 17 QA descartável: `LandAssistanceTest` 6/6 (46 asserções); nenhuma migration aplicada à base operacional |
|
||||
| Fontes e versões históricas | Inventário read-only em `source-inventory.md` | `land-assistance-v1` | SHA-256 dos sete PDFs; originais fora de `public/`; snapshots preservam versões | Identidade dos ficheiros verificada anteriormente; conteúdo/actualidade pendentes |
|
||||
|
||||
## Bloqueios antes de activar checklist legal ou canal oficial
|
||||
|
||||
@@ -38,9 +38,11 @@
|
||||
## Adenda — contacto telefónico sem email (2026-10-03)
|
||||
|
||||
A regra anterior desta matriz que marcava o email como obrigatório foi substituída no código: o pedido de apoio aceita email ou telefone. Email ausente é persistido como `NULL`, sem endereço inventado. Uma migration aditiva torna a coluna anulável; o rollback recusa-se a remover essa compatibilidade enquanto existirem pedidos só telefónicos. A conversão CRM para lead continua a exigir confirmação de email. Os testes de feature foram escritos, mas não executados porque a base isolada configurada em `127.0.0.1:55441` estava encerrada; não foi tentada ligação à porta operacional 5432.
|
||||
|
||||
### Evidência posterior — migration e percurso telefónico (2026-10-03)
|
||||
|
||||
A nota anterior de tentativa bloqueada foi superada numa execução posterior: a nova base PostgreSQL 17 foi criada em pasta temporária exclusiva e usada através da porta QA 55441. `LandAssistanceTest` passou 4/4 (42 asserções) e a suite PHP completa passou 251/251 (2.586 asserções); isto executou a migration aditiva em QA. O cluster foi parado. Nenhuma migration foi executada na base da aplicação e a porta 5432 não foi usada.
|
||||
|
||||
### Actualização dos testes de reversibilidade — 2026-10-03
|
||||
|
||||
`LandAssistanceTest` agora passa 6/6 (46 asserções), incluindo reversão normal da coluna para não nula quando não existem pedidos telefónicos e bloqueio do rollback quando existem. A suite PHP completa após esses testes passou 253/253 (2.590 asserções) no cluster PostgreSQL QA descartável. O cluster foi parado; nenhuma conexão de teste foi aberta na porta 5432. Nenhuma migration foi aplicada à base local ou de produção.
|
||||
|
||||
@@ -4,28 +4,28 @@
|
||||
|
||||
Pasta observada: `C:\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\Scan\Terras`. A classificação abaixo deriva apenas do nome da subpasta, não da leitura do conteúdo. Os PDFs não apresentam camada de texto detectável nesta verificação; OCR e conferência página a página continuam pendentes. Não foram copiados para `public/` nem usados para criar requisitos automáticos.
|
||||
|
||||
| Categoria provisória | Ficheiro original | Bytes | SHA-256 | Tratamento |
|
||||
| --- | --- | ---: | --- | --- |
|
||||
| Consulta comunitária/pareceres | `Consulta/scan0001.pdf` | 140871 | `3C1143FD565D92403E0C175ADFBEA293AFC3F187FD6A201BA26E1D45A998D2CA` | Referência reservada; conteúdo por OCR/revisão |
|
||||
| Consulta comunitária/pareceres | `Consulta/scan0002.pdf` | 88860 | `B6A0CCAC07EEF71294089DDE653F6ED3E600196B75836D69C0E99687E199D6CC` | Referência reservada; conteúdo por OCR/revisão |
|
||||
| Consulta comunitária/pareceres | `Consulta/scan.pdf` | 174964 | `732FF341BBD4C0C05E8ACCDB6919FA6151D18E50E0C3ED0CD618A4FD69B9018F` | Referência reservada; conteúdo por OCR/revisão |
|
||||
| Plano de exploração | `exploracao/scan0003.pdf` | 144995 | `F1F5778426D16FAD122F1DF32F40DF1B2CD66F5C56CA41A9EA56499F10ED8D60` | Referência reservada; conteúdo por OCR/revisão |
|
||||
| Plano de exploração | `exploracao/scan0004.pdf` | 150054 | `54EE525982C78450611A3C4B93F77ECC00493DECDBA4CF982BBC45E52DE1CF01` | Referência reservada; conteúdo por OCR/revisão |
|
||||
| Formulário/requisitos | `Formulario/scan0006.pdf` | 243123 | `A45CA527E0581B95939F0A35BF107BF78F24E183513E5CE4AD81C0FE6FEC3E53` | Referência reservada; conteúdo por OCR/revisão |
|
||||
| Formulário/requisitos | `Formulario/scan0005.pdf` | 286614 | `D994A036EFFB269A1BF808164518FC251458379CC15F1A4D5730E8BD746A9A37` | Referência reservada; conteúdo por OCR/revisão |
|
||||
| Categoria provisória | Ficheiro original | Bytes | SHA-256 | Tratamento |
|
||||
| ------------------------------ | ------------------------- | -----: | ------------------------------------------------------------------ | ---------------------------------------------- |
|
||||
| Consulta comunitária/pareceres | `Consulta/scan0001.pdf` | 140871 | `3C1143FD565D92403E0C175ADFBEA293AFC3F187FD6A201BA26E1D45A998D2CA` | Referência reservada; conteúdo por OCR/revisão |
|
||||
| Consulta comunitária/pareceres | `Consulta/scan0002.pdf` | 88860 | `B6A0CCAC07EEF71294089DDE653F6ED3E600196B75836D69C0E99687E199D6CC` | Referência reservada; conteúdo por OCR/revisão |
|
||||
| Consulta comunitária/pareceres | `Consulta/scan.pdf` | 174964 | `732FF341BBD4C0C05E8ACCDB6919FA6151D18E50E0C3ED0CD618A4FD69B9018F` | Referência reservada; conteúdo por OCR/revisão |
|
||||
| Plano de exploração | `exploracao/scan0003.pdf` | 144995 | `F1F5778426D16FAD122F1DF32F40DF1B2CD66F5C56CA41A9EA56499F10ED8D60` | Referência reservada; conteúdo por OCR/revisão |
|
||||
| Plano de exploração | `exploracao/scan0004.pdf` | 150054 | `54EE525982C78450611A3C4B93F77ECC00493DECDBA4CF982BBC45E52DE1CF01` | Referência reservada; conteúdo por OCR/revisão |
|
||||
| Formulário/requisitos | `Formulario/scan0006.pdf` | 243123 | `A45CA527E0581B95939F0A35BF107BF78F24E183513E5CE4AD81C0FE6FEC3E53` | Referência reservada; conteúdo por OCR/revisão |
|
||||
| Formulário/requisitos | `Formulario/scan0005.pdf` | 286614 | `D994A036EFFB269A1BF808164518FC251458379CC15F1A4D5730E8BD746A9A37` | Referência reservada; conteúdo por OCR/revisão |
|
||||
|
||||
Também foi localizado `C:\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\prompt\por executar\DUAT.docx`. Estava aberto/bloqueado por outro processo e não foi lido nem copiado. O Guia Nacional para Obtenção e Regularização do DUAT continua por localizar como cópia identificada e legível.
|
||||
|
||||
### Fontes normativas digitais localizadas em 2026-10-03
|
||||
|
||||
| Material | Fonte e natureza | Uso permitido nesta fase | Estado |
|
||||
| --- | --- | --- | --- |
|
||||
| Lei n.º 19/97, de 1 de Outubro | [PDUL — Documentos Normativos](https://www.pdul.gov.mz/Anuncios/Documentos-Normativos), ligação “Lei de terra” para PDF; compilação alojada num programa governamental, reprodução digital do diploma | Identificar estrutura/artigos para revisão; não substituir conferência com o Boletim da República | Texto pesquisável obtido; cópia do BR e reconciliação de alterações ainda pendentes |
|
||||
| Decreto n.º 66/98, de 8 de Dezembro | Mesma página PDUL, ligação “Regulamento da Lei de Terra” | Referência de trabalho para revisão do regulamento | Texto digital localizado; verificar alterações em conjunto (Decretos n.º 1/2003, 50/2007 e 43/2010 mencionados nas fontes consultadas) |
|
||||
| Decreto n.º 60/2006, de 26 de Dezembro | Mesma página PDUL, ligação “Regulamento do solo urbano” | Identificar percurso urbano separado do rural | PDF localizado; texto extraído pela pesquisa não foi integralmente confrontado com o BR |
|
||||
| Diploma Ministerial n.º 47/2020 | [Imprensa Nacional de Moçambique — sumário do BR n.º 176, 14-09-2020](https://www.inm.gov.mz/pt-br/content/sum%C3%A1rio-br-n%C2%BA-176-de-140920-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-1439) | Registar que houve actualização de taxas DUAT e revogação expressa do DM n.º 144/2010; não publicar valores sem ler o diploma integral e confirmar aplicação actual | Sumário oficial localizado; anexo/texto integral e vigência actual por verificar |
|
||||
| Proposta de revisão da Lei de Terras | [Parlamento — Projectos e Propostas de Lei](https://www.parlamento.mz/?page_id=15236), secção “Proposta de Revisão da Lei de Terras”; [texto disponibilizado](https://www.parlamento.mz/wp-content/uploads/2026/03/LEI-DE-TERRAS.pdf) | Comparação e arquivo separados da regra activa | O portal parlamentar classifica o material como proposta. Não tratar o PDF como lei promulgada ou vigente |
|
||||
| Comunicado do Conselho de Ministros, 28-10-2025 | [Portal do Governo](https://portaldogoverno.gov.mz/wp-content/uploads/2025/10/COMUNICADO-DA-36.a-SOCM2025.pdf) | Confirma apenas que o Governo aprovou uma proposta para submeter à Assembleia da República | Comunicado secundário ao diploma; não demonstra aprovação parlamentar, promulgação ou entrada em vigor |
|
||||
| Material | Fonte e natureza | Uso permitido nesta fase | Estado |
|
||||
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Lei n.º 19/97, de 1 de Outubro | [PDUL — Documentos Normativos](https://www.pdul.gov.mz/Anuncios/Documentos-Normativos), ligação “Lei de terra” para PDF; compilação alojada num programa governamental, reprodução digital do diploma | Identificar estrutura/artigos para revisão; não substituir conferência com o Boletim da República | Texto pesquisável obtido; cópia do BR e reconciliação de alterações ainda pendentes |
|
||||
| Decreto n.º 66/98, de 8 de Dezembro | Mesma página PDUL, ligação “Regulamento da Lei de Terra” | Referência de trabalho para revisão do regulamento | Texto digital localizado; verificar alterações em conjunto (Decretos n.º 1/2003, 50/2007 e 43/2010 mencionados nas fontes consultadas) |
|
||||
| Decreto n.º 60/2006, de 26 de Dezembro | Mesma página PDUL, ligação “Regulamento do solo urbano” | Identificar percurso urbano separado do rural | PDF localizado; texto extraído pela pesquisa não foi integralmente confrontado com o BR |
|
||||
| Diploma Ministerial n.º 47/2020 | [Imprensa Nacional de Moçambique — sumário do BR n.º 176, 14-09-2020](https://www.inm.gov.mz/pt-br/content/sum%C3%A1rio-br-n%C2%BA-176-de-140920-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-1439) | Registar que houve actualização de taxas DUAT e revogação expressa do DM n.º 144/2010; não publicar valores sem ler o diploma integral e confirmar aplicação actual | Sumário oficial localizado; anexo/texto integral e vigência actual por verificar |
|
||||
| Proposta de revisão da Lei de Terras | [Parlamento — Projectos e Propostas de Lei](https://www.parlamento.mz/?page_id=15236), secção “Proposta de Revisão da Lei de Terras”; [texto disponibilizado](https://www.parlamento.mz/wp-content/uploads/2026/03/LEI-DE-TERRAS.pdf) | Comparação e arquivo separados da regra activa | O portal parlamentar classifica o material como proposta. Não tratar o PDF como lei promulgada ou vigente |
|
||||
| Comunicado do Conselho de Ministros, 28-10-2025 | [Portal do Governo](https://portaldogoverno.gov.mz/wp-content/uploads/2025/10/COMUNICADO-DA-36.a-SOCM2025.pdf) | Confirma apenas que o Governo aprovou uma proposta para submeter à Assembleia da República | Comunicado secundário ao diploma; não demonstra aprovação parlamentar, promulgação ou entrada em vigor |
|
||||
|
||||
O conteúdo de [legal-source-review-2026-10-03.md](legal-source-review-2026-10-03.md) é uma triagem de fontes, não parecer jurídico. A Lei n.º 19/97 obtida no PDUL não deve ser apresentada como consolidação oficial; o acesso ao portal da Imprensa Nacional confirmou sumários, mas não a cópia integral do BR de 1997. A pesquisa dirigida não encontrou publicação/promulgação da proposta até à data desta revisão; isso não substitui uma certidão ou pesquisa exaustiva do BR.
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ Os três ficheiros passaram pelo ClamAV 1.5.4, com assinaturas de 06/10/2026 08:
|
||||
|
||||
## Matriz dos documentos importados
|
||||
|
||||
| Documento e versão | Módulos e informação sustentada | Estado documental | Revisão/pendências |
|
||||
|---|---|---|---|
|
||||
| Lei n.º 22/2022, republicada no 2.º Suplemento do BR 250 | `simulator` como evidência documental; tema IVA. A revisão documental cita o art. 17(1). | Ficheiro descarregado do catálogo oficial AT e comparado byte a byte com o arquivo: 1.834.284 bytes; SHA-256 `EA1B0DC6F5CF6E595F6D9A19E7042F398999E0087CAEF1785F6E8FAC571B5F53`. Origem oficial confirmada. Situação registada como `amended`; não é apresentado como consolidação integral actual. | Direitos da composição PDF aguardam revisão administrativa; classificação privada e publicação pendente. A taxa ou regra não é activada pelo registo documental. |
|
||||
| BR n.º 248/2025, Suplemento n.º 1 | `simulator` como evidência; temas ICE, aduaneiro, ISPC, IVA, IRPS e IRPC, sem afirmar uma obrigação por tema. | Ficheiro descarregado do endpoint WebDAV oficial AT e comparado byte a byte com o arquivo: 440.018 bytes; SHA-256 `CCF2A8DEECA820F2DD076B4B2141B1EFE3F6A806568C3A587CA59A8E92F4C251`. Origem oficial confirmada. Situação jurídica registada como `unknown`, pois a edição contém vários diplomas cuja vigência deve ser analisada individualmente. | Direitos da composição PDF aguardam revisão administrativa; classificação privada e publicação pendente. Os artigos e a vigência de cada lei não ficam aprovados pela associação temática. |
|
||||
| BR n.º 89, I Série, 08/06/2017 — aviso de rectificação da Lei n.º 13/2016 | `simulator` como evidência; tema IVA. A ficha INM identifica o aviso na p. 569. | O hash do PDF local é `11F0CAB15EB6E51010952203FFA24FDC533A24FD642E19525FE4F46B17A1F5E5` e coincide com o exemplar arquivado no repositório. A ficha oficial identifica o acto, mas os bytes locais não foram comparados com um download oficial. Origem `identified`; situação jurídica `unknown`. | Cadeia de custódia, autenticidade binária, anexos e efeito temporal por confirmar. Direitos por confirmar; classificação privada e publicação pendente. |
|
||||
| Documento e versão | Módulos e informação sustentada | Estado documental | Revisão/pendências |
|
||||
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| Lei n.º 22/2022, republicada no 2.º Suplemento do BR 250 | `simulator` como evidência documental; tema IVA. A revisão documental cita o art. 17(1). | Ficheiro descarregado do catálogo oficial AT e comparado byte a byte com o arquivo: 1.834.284 bytes; SHA-256 `EA1B0DC6F5CF6E595F6D9A19E7042F398999E0087CAEF1785F6E8FAC571B5F53`. Origem oficial confirmada. Situação registada como `amended`; não é apresentado como consolidação integral actual. | Direitos da composição PDF aguardam revisão administrativa; classificação privada e publicação pendente. A taxa ou regra não é activada pelo registo documental. |
|
||||
| BR n.º 248/2025, Suplemento n.º 1 | `simulator` como evidência; temas ICE, aduaneiro, ISPC, IVA, IRPS e IRPC, sem afirmar uma obrigação por tema. | Ficheiro descarregado do endpoint WebDAV oficial AT e comparado byte a byte com o arquivo: 440.018 bytes; SHA-256 `CCF2A8DEECA820F2DD076B4B2141B1EFE3F6A806568C3A587CA59A8E92F4C251`. Origem oficial confirmada. Situação jurídica registada como `unknown`, pois a edição contém vários diplomas cuja vigência deve ser analisada individualmente. | Direitos da composição PDF aguardam revisão administrativa; classificação privada e publicação pendente. Os artigos e a vigência de cada lei não ficam aprovados pela associação temática. |
|
||||
| BR n.º 89, I Série, 08/06/2017 — aviso de rectificação da Lei n.º 13/2016 | `simulator` como evidência; tema IVA. A ficha INM identifica o aviso na p. 569. | O hash do PDF local é `11F0CAB15EB6E51010952203FFA24FDC533A24FD642E19525FE4F46B17A1F5E5` e coincide com o exemplar arquivado no repositório. A ficha oficial identifica o acto, mas os bytes locais não foram comparados com um download oficial. Origem `identified`; situação jurídica `unknown`. | Cadeia de custódia, autenticidade binária, anexos e efeito temporal por confirmar. Direitos por confirmar; classificação privada e publicação pendente. |
|
||||
|
||||
As onze associações foram gravadas como evidência (`applicability=evidence`), nunca como requisito obrigatório. A interface da biblioteca apresenta “Onde este documento é utilizado” e liga a associação `simulator` a `/Simulador`; o próprio simulador já consulta recomendações para esse módulo. Não foram criadas associações a DUAT, licenciamento, constituição de empresas, trabalho, imigração, zonas ou procedimentos porque estes três ficheiros não sustentam esses percursos.
|
||||
|
||||
@@ -24,14 +24,14 @@ As onze associações foram gravadas como evidência (`applicability=evidence`),
|
||||
|
||||
As seis linhas de `investment_legal_sources` são referências URL, todas com `validation_status=pending`; não são binários e não foram convertidas em documentos fictícios:
|
||||
|
||||
| Referência existente | Situação nesta migração |
|
||||
|---|---|
|
||||
| Beluluane Industrial Park — Free Zone; Resolução Interna do Conselho de Ministros n.º 15/99; página APIEX | URL existente; diploma/efeito actual e ficheiro integral por confirmar. |
|
||||
| Conselho de Ministros — BR n.º 70, 30/08/2013; Decreto n.º 47/2013 | Página INM existente; texto integral e artigos aplicáveis por confirmar. |
|
||||
| Manga-Mungassa Special Economic Zone; Decreto n.º 22/2012 | Página APIEX existente; diploma e efeito actual por confirmar. |
|
||||
| BR n.º 46, 06/06/2014; Decreto n.º 28/2014 | Página INM existente; texto integral e artigos aplicáveis por confirmar. |
|
||||
| Mocuba Industrial Free Zone; Decreto n.º 29/2014 | Página APIEX existente; diploma e efeito actual por confirmar. |
|
||||
| Nacala Special Economic Zone; Decreto n.º 76/2007 | Página APIEX existente; diploma e efeito actual por confirmar. |
|
||||
| Referência existente | Situação nesta migração |
|
||||
| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
| Beluluane Industrial Park — Free Zone; Resolução Interna do Conselho de Ministros n.º 15/99; página APIEX | URL existente; diploma/efeito actual e ficheiro integral por confirmar. |
|
||||
| Conselho de Ministros — BR n.º 70, 30/08/2013; Decreto n.º 47/2013 | Página INM existente; texto integral e artigos aplicáveis por confirmar. |
|
||||
| Manga-Mungassa Special Economic Zone; Decreto n.º 22/2012 | Página APIEX existente; diploma e efeito actual por confirmar. |
|
||||
| BR n.º 46, 06/06/2014; Decreto n.º 28/2014 | Página INM existente; texto integral e artigos aplicáveis por confirmar. |
|
||||
| Mocuba Industrial Free Zone; Decreto n.º 29/2014 | Página APIEX existente; diploma e efeito actual por confirmar. |
|
||||
| Nacala Special Economic Zone; Decreto n.º 76/2007 | Página APIEX existente; diploma e efeito actual por confirmar. |
|
||||
|
||||
O inventário Terra/DUAT também contém referências URL a Lei n.º 19/97, Decreto n.º 66/98, Decreto n.º 60/2006 e Diploma Ministerial n.º 47/2020. Os ficheiros oficiais correspondentes não foram encontrados no arquivo local indicado e não foram copiados. `docs/investmoz/land-assistance/source-inventory.md` documenta os limites de cada referência.
|
||||
|
||||
|
||||
@@ -1,20 +1,17 @@
|
||||
{
|
||||
"target": "anchor_restore_20261007_test:55441",
|
||||
"passed": true,
|
||||
"partnershipId": 3,
|
||||
"roomId": 3,
|
||||
"draftRecovered": true,
|
||||
"sourceBacklink": true,
|
||||
"moderation": true,
|
||||
"interest": true,
|
||||
"shortlist": true,
|
||||
"privateMessage": true,
|
||||
"bilateralAgreement": true,
|
||||
"milestoneCompleted": true,
|
||||
"roomClosed": true,
|
||||
"viewports": [
|
||||
1440,
|
||||
390
|
||||
],
|
||||
"errors": []
|
||||
}
|
||||
"target": "anchor_restore_20261007_test:55441",
|
||||
"passed": true,
|
||||
"partnershipId": 3,
|
||||
"roomId": 3,
|
||||
"draftRecovered": true,
|
||||
"sourceBacklink": true,
|
||||
"moderation": true,
|
||||
"interest": true,
|
||||
"shortlist": true,
|
||||
"privateMessage": true,
|
||||
"bilateralAgreement": true,
|
||||
"milestoneCompleted": true,
|
||||
"roomClosed": true,
|
||||
"viewports": [1440, 390],
|
||||
"errors": []
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ Não existe entrega externa configurada. Os endpoints públicos são `GET /api/v
|
||||
|
||||
As escritas usam a sessão existente, CSRF e autorização por recurso. Os contratos das rotas estão em `routes/partnerships.php`. Alterações enviam `version`; uma versão desatualizada resulta em 409. Criação de rascunho e mensagens usam `client_key` UUID: repetir a mesma operação devolve o resultado existente; reutilizar a chave com outro conteúdo resulta em 409.
|
||||
|
||||
| Agregado | Estados persistidos | Separação |
|
||||
| --- | --- | --- |
|
||||
| Publicação | draft, submitted, returned, rejected, published, suspended, archived; pending legado | Renovar recupera o rascunho e exige nova submissão. Editar uma proposta publicada não publica a alteração automaticamente. |
|
||||
| Candidatura | submitted, clarification, shortlisted, rejected, withdrawn, negotiation | Negociação de um candidato não encerra outros. A retirada pertence ao candidato; avaliação e decisão ao titular da proposta. |
|
||||
| Sala/acordo | negotiation, agreement_recorded, agreed, execution, closed, cancelled | Registo de termos não confirma as partes. Só a confirmação de ambas permite acompanhar a execução. Encerramento exige os marcos concluídos. Renovação devolve a negociação sem apagar o histórico dos termos. |
|
||||
| Agregado | Estados persistidos | Separação |
|
||||
| ----------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Publicação | draft, submitted, returned, rejected, published, suspended, archived; pending legado | Renovar recupera o rascunho e exige nova submissão. Editar uma proposta publicada não publica a alteração automaticamente. |
|
||||
| Candidatura | submitted, clarification, shortlisted, rejected, withdrawn, negotiation | Negociação de um candidato não encerra outros. A retirada pertence ao candidato; avaliação e decisão ao titular da proposta. |
|
||||
| Sala/acordo | negotiation, agreement_recorded, agreed, execution, closed, cancelled | Registo de termos não confirma as partes. Só a confirmação de ambas permite acompanhar a execução. Encerramento exige os marcos concluídos. Renovação devolve a negociação sem apagar o histórico dos termos. |
|
||||
|
||||
Os eventos persistidos em `partnership_events` e `audit_events` têm envelope v1: `event_id`, `event_type`, `event_version`, `source`, `subject_id`, `occurred_at`, `producer_version`, `correlation_id`, `causation_id`, `data_classification`, `aggregate_id`, `aggregate_version`, `publication_version`, `payload`, `external_delivery`. A sequência do agregado é atribuída com bloqueio transacional; não confunde a versão de publicação com a de cada candidatura ou sala. O autor fica no registo do evento e da auditoria. `external_delivery=not_configured` não afirma transmissão ao CRM. Estes envelopes são internos e não constituem um endpoint público para informação privada.
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user