Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb4e7d8970 | ||
|
|
bc93ccdb9e | ||
|
|
7ccf62adec | ||
|
|
f4d0970917 | ||
|
|
e46f973159 | ||
|
|
1bcf34c0c0 | ||
|
|
ef0c2acd36 | ||
|
|
30aed7cd12 | ||
|
|
95d4cf44d7 | ||
|
|
73c046e104 | ||
|
|
15d82611b6 | ||
|
|
8286d7504d | ||
|
|
aa2b3d05b0 | ||
|
|
97d51db60a | ||
|
|
a1b553372e | ||
|
|
f80d30f478 | ||
|
|
916819eb35 | ||
|
|
1d921c9d03 | ||
|
|
7b1c35793c | ||
|
|
1ec318561c | ||
|
|
9ac623a433 | ||
|
|
5f4eb5cfaa | ||
|
|
042ffbde44 | ||
|
|
064296e1fb | ||
|
|
a1fbda06ad | ||
|
|
52fbb1468a |
@@ -0,0 +1,16 @@
|
||||
.git
|
||||
.idea
|
||||
.agents
|
||||
.codex
|
||||
node_modules
|
||||
vendor
|
||||
public/build
|
||||
storage/logs
|
||||
storage/framework/cache
|
||||
storage/framework/sessions
|
||||
storage/framework/views
|
||||
tests
|
||||
Doc
|
||||
docs
|
||||
*.zip
|
||||
.phpunit.result.cache
|
||||
@@ -39,3 +39,10 @@ Referenciar estas orientações nos requisitos correspondentes da matriz existen
|
||||
- Preservar o ficheiro original, a proveniência, o identificador exacto, a edição/data, o hash e o histórico. Não marcar autenticidade, vigência, direitos ou scan como confirmados sem evidência.
|
||||
- Para fontes fiscais, inventariar também as fontes não analisadas e os diplomas/artigos sem correspondência documental. Parâmetros ausentes ou ambiguidades permanecem indeterminados; não usar zero nem a taxa mais recente como substituto.
|
||||
- Antes de aplicar mudanças em dados activos, confirmar backup/restauro e a acção autenticada que continua necessária. Testes e migrações devem usar exclusivamente uma base QA isolada com guarda contra a base activa.
|
||||
|
||||
|
||||
## Execução das fontes e decisões — orientação permanente da Tempo, 22/10/2026
|
||||
|
||||
A legislação, os formulários e os procedimentos oficiais disponibilizados são fontes válidas para implementar os requisitos. As decisões e procedimentos da Tempo orientam a sua utilização no portal. Traduzir essas fontes em campos, regras, documentos, cálculos e percursos executáveis com rastreabilidade, sem exigir nova validação jurídica genérica ou criar aprovadores, restrições ou condições por interpretação do Codex.
|
||||
|
||||
Uma divergência concreta deve identificar os trechos, períodos e comportamento afectados; prosseguir com os restantes requisitos. Uma decisão de implementação em falta deve ser apresentada especificamente à Tempo, sem a substituir por regra inventada. Rever e remover bloqueios anteriores sem fundamento numa fonte aplicável ou decisão expressa da Tempo, preservando dados e histórico. A rastreabilidade explica a execução e não constitui uma autoridade adicional. Mantêm-se os limites autorizados de QA e das alterações no activo.
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
+342
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -81,31 +81,18 @@ class EditorialArticleController extends Controller
|
||||
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource(request()->user(), $id);
|
||||
$r = DB::table('portal_resources')->find($id);
|
||||
abort_unless($r, 404);
|
||||
$content = $r->editorial_content ? json_decode($r->editorial_content, true) : null;
|
||||
if ($content === null) {
|
||||
$document = DB::table('documents')->find($r->document_id);
|
||||
if ($document && $document->status === 'active' && $document->malware_scan_status === 'clean'
|
||||
&& in_array($document->media_type, ['text/plain', 'application/json'], true)) {
|
||||
$binary = app(VerifiedDocumentBinary::class)->respond($document);
|
||||
$bytes = $binary instanceof \Symfony\Component\HttpFoundation\BinaryFileResponse
|
||||
? file_get_contents($binary->getFile()->getPathname()) : $binary->getContent();
|
||||
$original = json_decode($bytes, true);
|
||||
if (is_array($original) && ($original['format'] ?? '') === 'investmoz-legacy-article-v1') {
|
||||
$html = preg_replace('~</(?:p|div|li|h[1-6])>|<br\s*/?>~i', "\n\n", $original['content_html'] ?? '');
|
||||
$content = ['body' => trim(html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8')), 'image_resource_id' => null];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Inertia::render('portal/article-editor', ['resource' => ['id' => $r->id, 'title' => $r->title, 'metadata_version' => $r->metadata_version],
|
||||
'content' => $content, 'images' => $this->publicResources()->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->get(['r.id', 'r.title']),
|
||||
'revisions' => DB::table('portal_resource_revisions')->where('resource_id', $id)->orderByDesc('revision_no')->get(['revision_no', 'change_type', 'created_at'])]);
|
||||
$reading = app(\App\Services\EditorialReading::class);
|
||||
$original = $reading->original($r);
|
||||
$content = $reading->content($r, $original);
|
||||
return Inertia::render('portal/article-editor', ['resource' => ['id' => $r->id, 'title' => $r->title, 'metadata_version' => $r->metadata_version, 'published' => (bool) $r->published],
|
||||
'content' => $content, 'source' => $original ? array_intersect_key($original, array_flip(['source_url', 'published_at', 'modified_at', 'legacy_id', 'image_source_url'])) : null, 'images' => $this->publicResources()->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->get(['r.id', 'r.title']),
|
||||
'revisions' => DB::table('portal_resource_revisions')->where('resource_id', $id)->orderByDesc('revision_no')->get(['revision_no', 'change_type', 'created_at'])])->toResponse(request())->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function save(Request $request, int $id)
|
||||
{
|
||||
$this->installed();
|
||||
$data = $request->validate(['body' => ['required', 'string', 'max:100000'], 'image_resource_id' => ['nullable', 'integer'], 'metadata_version' => ['required', 'integer', 'min:1']]);
|
||||
$data = $this->validateContent($request);
|
||||
DB::transaction(function () use ($request, $id, $data) {
|
||||
$r = DB::table('portal_resources')->where('id', $id)->lockForUpdate()->first();
|
||||
abort_unless($r, 404);
|
||||
@@ -118,7 +105,7 @@ class EditorialArticleController extends Controller
|
||||
if ($body === '') {
|
||||
throw ValidationException::withMessages(['body' => 'Escreva o corpo do artigo.']);
|
||||
}
|
||||
$content = ['format' => 1, 'body' => $body, 'image_resource_id' => $data['image_resource_id'] ?? null];
|
||||
$content = $this->edition($r, $data);
|
||||
$revision = (int) $r->metadata_version + 1;
|
||||
DB::table('portal_resources')->where('id', $id)->update(['editorial_content' => json_encode($content, JSON_THROW_ON_ERROR), 'published' => false, 'approved_by' => null, 'metadata_version' => $revision, 'last_reviewed_by' => $request->user()->id, 'last_reviewed_at' => now(), 'updated_at' => now()]);
|
||||
$resource = DB::table('portal_resources')->find($id);
|
||||
@@ -130,16 +117,46 @@ class EditorialArticleController extends Controller
|
||||
return redirect('/management/portal-services/resources/'.$id.'/article')->with('success', 'Artigo guardado como rascunho. Publique expressamente no acervo quando estiver pronto.');
|
||||
}
|
||||
|
||||
public function show(int $id)
|
||||
private function validateContent(Request $request): array
|
||||
{
|
||||
return $request->validate(['body' => ['required', 'string', 'max:100000'], 'image_resource_id' => ['nullable', 'integer'], 'image_mode' => ['nullable', 'in:original,selected,none'], 'metadata_version' => ['required', 'integer', 'min:1']]);
|
||||
}
|
||||
|
||||
private function edition(object $resource, array $data): array
|
||||
{
|
||||
$reading = app(\App\Services\EditorialReading::class);
|
||||
$original = $reading->original($resource);
|
||||
$mode = $data['image_mode'] ?? (!empty($data['image_resource_id']) ? 'selected' : 'none');
|
||||
if ($mode === 'selected') abort_unless(!empty($data['image_resource_id']) && $reading->publicResources()->where('r.id', $data['image_resource_id'])->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->exists(), 422, 'Escolha uma imagem pública disponível na biblioteca.');
|
||||
return ['format' => 1, 'body' => trim($data['body']), 'image_mode' => $mode, 'image_resource_id' => $mode === 'selected' ? (int)$data['image_resource_id'] : null,
|
||||
'preserve_original_html' => $original && trim($data['body']) === $reading->plain($original['content_html'])];
|
||||
}
|
||||
|
||||
public function preview(Request $request, int $id)
|
||||
{
|
||||
$this->installed();
|
||||
$r = $this->publicResources()->where('r.id', $id)->first(['r.id', 'r.title', 'r.description', 'r.editorial_content', 'r.document_id', 'd.issuer']);
|
||||
abort_unless($r && $r->editorial_content, 404);
|
||||
$content = json_decode($r->editorial_content, true);
|
||||
// Recheck the original bytes using the same binary gate as the library.
|
||||
$binary = app(VerifiedDocumentBinary::class)->respond($this->publicResources()->where('r.id', $id)->first(['r.*', 'd.*']));
|
||||
$image = ! empty($content['image_resource_id']) ? $this->publicResources()->where('r.id', $content['image_resource_id'])->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp'])->value('r.id') : null;
|
||||
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
||||
$resource = DB::table('portal_resources')->find($id); abort_unless($resource, 404);
|
||||
return Inertia::render('portal/article', ['article' => app(\App\Services\EditorialReading::class)->reading($resource, null, true)])
|
||||
->toResponse($request)->header('Cache-Control', 'private, no-store')->header('X-Robots-Tag', 'noindex, nofollow');
|
||||
}
|
||||
|
||||
return Inertia::render('portal/article', ['article' => ['id' => $id, 'title' => $r->title, 'description' => $r->description, 'author' => $r->issuer, 'body' => $content['body'], 'image_url' => $image ? '/documentos/'.$image.'/view' : null, 'original_url' => '/documentos/'.$id]]);
|
||||
public function previewContent(Request $request, int $id)
|
||||
{
|
||||
$this->installed();
|
||||
app(\App\Services\DocumentManagementAccess::class)->assertEditableResource($request->user(), $id);
|
||||
$resource = DB::table('portal_resources')->find($id); abort_unless($resource, 404);
|
||||
$data = $this->validateContent($request);
|
||||
abort_unless((int)$resource->metadata_version === (int)$data['metadata_version'], 409, 'A revisão mudou; recupere antes de rever a prévia.');
|
||||
return response()->json(['article' => app(\App\Services\EditorialReading::class)->reading($resource, $this->edition($resource, $data), true)])
|
||||
->header('Cache-Control', 'private, no-store')->header('X-Robots-Tag', 'noindex, nofollow');
|
||||
}
|
||||
|
||||
public function show(Request $request, int $id)
|
||||
{
|
||||
$this->installed();
|
||||
$resource = app(\App\Services\EditorialReading::class)->publicResources()->where('r.id', $id)->first(['r.*']);
|
||||
abort_unless($resource, 404);
|
||||
return Inertia::render('portal/article', ['article' => app(\App\Services\EditorialReading::class)->reading($resource)])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use App\Services\CountryCatalogue;
|
||||
use App\Services\IdempotencyConflict;
|
||||
use App\Services\InterestSubmission;
|
||||
use App\Services\LegacyContent;
|
||||
use App\Services\PortalProfileAccess;
|
||||
use App\Services\Malware\MalwareScanner;
|
||||
use App\Services\VerifiedDocumentBinary;
|
||||
use App\Services\VirtualDesk;
|
||||
@@ -274,7 +275,7 @@ class PortalServicesController extends Controller
|
||||
'resource' => $resource,
|
||||
'backUrl' => $backUrl,
|
||||
'downloadUrl' => route('services.download', ['id' => $id]),
|
||||
'articleUrl' => Schema::hasColumn('portal_resources','editorial_content') && DB::table('portal_resources')->where('id',$id)->whereNotNull('editorial_content')->exists() ? '/artigos/'.$id : null,
|
||||
'articleUrl' => $this->articleUrl($id),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -297,6 +298,7 @@ class PortalServicesController extends Controller
|
||||
|
||||
public function manage(Request $request, MalwareScanner $scanner): Response
|
||||
{
|
||||
$managementFilters = $request->validate(['q' => 'nullable|string|max:150', 'state' => 'nullable|in:draft,published,withdrawn', 'intent' => 'nullable|in:document,legislation']);
|
||||
$access = app(\App\Services\DocumentManagementAccess::class);
|
||||
if (! $access->full($request->user())) {
|
||||
$documents = $access->documents($request->user())->where('status', '!=', 'deleted');
|
||||
@@ -304,7 +306,7 @@ class PortalServicesController extends Controller
|
||||
$health['signatures_at'] = $health['signatures_at']?->format(DATE_ATOM);
|
||||
|
||||
$editorial = $access->editorial($request->user());
|
||||
$resources = $editorial ? $access->editorialResources($request->user())->orderByDesc('r.updated_at')
|
||||
$resources = $editorial ? $this->filterManagedResources($access->editorialResources($request->user()), $managementFilters)->orderByDesc('r.updated_at')
|
||||
->get(['r.*', 'd.file_name', 'd.media_type', 'd.size_bytes', 'd.issuer', 'd.status as file_status', 'd.data_classification', 'd.version_no as file_version', 'd.rights_status as file_rights_status', 'd.malware_scan_status', 'd.malware_failure_reason']) : collect();
|
||||
$links = DB::table('portal_resource_links')->whereIn('resource_id', $resources->pluck('id'))->orderBy('id')->get()->groupBy('resource_id');
|
||||
$revisions = DB::table('portal_resource_revisions')->whereIn('resource_id', $resources->pluck('id'))->orderByDesc('revision_no')->get(['resource_id', 'revision_no', 'change_type', 'actor_user_id', 'created_at'])->groupBy('resource_id');
|
||||
@@ -313,6 +315,7 @@ class PortalServicesController extends Controller
|
||||
$resource->revisions = $revisions->get($resource->id, collect())->values();
|
||||
});
|
||||
return Inertia::render('portal/manage', [
|
||||
'managementFilters' => $managementFilters, 'associationOptions' => $this->contentAssociations($request),
|
||||
'documentAccessOnly' => ! $editorial, 'editorialScopeOnly' => $editorial, 'resources' => $resources, 'partnerships' => [], 'territories' => [],
|
||||
'requirementCatalogue' => [], 'requirementAssociationsEnabled' => false,
|
||||
'disclosureAuthorization' => null, 'incompleteCount' => $resources->filter(fn ($r) => ! $r->description || ! $r->issuer || ! $r->source_url || $r->source_confirmation === 'unconfirmed' || $r->legal_status === 'unknown' || $r->rights_status === 'unconfirmed')->count(), 'possibleDuplicates' => $resources->whereNotNull('duplicate_of_resource_id')->count(),
|
||||
@@ -331,7 +334,7 @@ class PortalServicesController extends Controller
|
||||
->where('external_id', 'document-repository-disclosure-authorization-2026-10-06-v1')
|
||||
->where('action', 'public_disclosure_authorized')->first(['external_id', 'occurred_at', 'after_data']);
|
||||
$disclosureAuthorizationData = $disclosureAuthorization ? json_decode($disclosureAuthorization->after_data, true) : null;
|
||||
$resources = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->orderByDesc('r.updated_at')->limit(200)
|
||||
$resources = $this->filterManagedResources(DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->whereNull('d.deleted_at'), $managementFilters)->orderByDesc('r.updated_at')->limit(200)
|
||||
->get(['r.*', 'd.file_name', 'd.media_type', 'd.size_bytes', 'd.issuer', 'd.status as file_status', 'd.data_classification', 'd.version_no as file_version', 'd.rights_status as file_rights_status', 'd.malware_scan_status', 'd.malware_failure_reason']);
|
||||
$links = DB::table('portal_resource_links')->whereIn('resource_id', $resources->pluck('id'))->orderBy('id')->get()->groupBy('resource_id');
|
||||
$resources->each(fn ($resource) => $resource->links = $links->get($resource->id, collect())->values());
|
||||
@@ -342,6 +345,7 @@ class PortalServicesController extends Controller
|
||||
$malwareHealth['signatures_at'] = $malwareHealth['signatures_at']?->format(DATE_ATOM);
|
||||
|
||||
return Inertia::render('portal/manage', ['resources' => $resources,
|
||||
'managementFilters' => $managementFilters, 'associationOptions' => $this->contentAssociations($request),
|
||||
'requirementCatalogue' => $this->requirementEvidenceEnabled()
|
||||
? collect(is_array($catalogue = config('document-requirements.catalogue', [])) ? $catalogue : [])->map(fn ($item, $key) => ['key' => $key, ...$item])->values()
|
||||
: collect(),
|
||||
@@ -367,6 +371,39 @@ class PortalServicesController extends Controller
|
||||
'possibleDuplicates' => DB::table('portal_resources')->whereNotNull('duplicate_of_resource_id')->count()]);
|
||||
}
|
||||
|
||||
private function filterManagedResources(Builder $query, array $filters): Builder
|
||||
{
|
||||
if (! empty($filters['q'])) {
|
||||
$term = '%'.addcslashes($filters['q'], '%_\\').'%';
|
||||
$query->where(fn ($q) => $q->where('r.title', 'ilike', $term)->orWhere('r.document_number', 'ilike', $term)->orWhere('d.issuer', 'ilike', $term)->orWhere('d.file_name', 'ilike', $term));
|
||||
}
|
||||
if (! empty($filters['state'])) {
|
||||
$query->where('r.published', $filters['state'] === 'published');
|
||||
if ($filters['state'] !== 'published') {
|
||||
$method = $filters['state'] === 'withdrawn' ? 'whereExists' : 'whereNotExists';
|
||||
$query->$method(fn ($q) => $q->selectRaw('1')->from('portal_resource_revisions as history')->whereColumn('history.resource_id', 'r.id')->whereIn('history.change_type', ['unpublished', 'withdrawn']));
|
||||
}
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
private function contentAssociations(Request $request): array
|
||||
{
|
||||
$access = app(\App\Services\DocumentManagementAccess::class);
|
||||
$resources = $access->full($request->user()) ? DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->whereNull('d.deleted_at')
|
||||
: ($access->editorial($request->user()) ? $access->editorialResources($request->user()) : null);
|
||||
$documents = $resources ? $resources->orderBy('r.title')->get(['r.id as value', 'r.title as label', 'r.document_type']) : collect();
|
||||
return [
|
||||
'sector' => \App\Models\Sector::choices()->orderBy('name')->get(['id as value', 'name as label']),
|
||||
'territory' => app(\App\Services\InvestmentGeography::class)->publicReferences()->orderBy('name')->get(['id as value', 'name as label']),
|
||||
'opportunity' => app(PortalProfileAccess::class)->scope(\App\Models\Opportunity::query(), $request->user())->orderBy('title')->get(['id as value', 'title as label']),
|
||||
'document' => $documents,
|
||||
'procedure' => $documents->where('document_type', 'procedure')->values(),
|
||||
'stage' => collect(config('virtual-desk.steps', []))->map(fn ($step, $key) => ['value' => $key, 'label' => $step['title']])->values(),
|
||||
'activity' => app(\App\Services\EconomicReferenceCatalog::class)->activities()->map(fn ($row) => ['value' => $row->id, 'label' => $row->display_label])->values(),
|
||||
];
|
||||
}
|
||||
|
||||
public function upload(Request $request): RedirectResponse
|
||||
{
|
||||
$this->validateManagement($request, ['file' => ['required', 'file', 'mimetypes:application/pdf,text/plain,text/csv,image/jpeg,image/png,image/webp,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.oasis.opendocument.text'.(app(LegacyContent::class)->available() ? ',application/json' : ''), 'max:10240'],
|
||||
@@ -399,7 +436,7 @@ class PortalServicesController extends Controller
|
||||
|
||||
ScanDocumentForMalware::dispatch($id)->afterCommit();
|
||||
|
||||
return to_route('services.manage')->with('success', 'Documento recebido em armazenamento privado e quarentena. Não aparece na biblioteca pública até validação e publicação explícitas.');
|
||||
return to_route('services.manage', array_filter(['intent' => in_array($request->input('intent'), ['document', 'legislation'], true) ? $request->input('intent') : null]))->with('uploaded_document_id', $id)->with('success', 'Documento recebido em armazenamento privado e quarentena. Não aparece na biblioteca pública até validação e publicação explícitas.');
|
||||
}
|
||||
|
||||
public function resource(Request $request): RedirectResponse
|
||||
@@ -663,7 +700,7 @@ class PortalServicesController extends Controller
|
||||
'data_classification' => ['nullable', 'in:internal,public'],
|
||||
'source_reference' => ['required', 'string', 'min:10', 'max:2000'],
|
||||
'legislation' => ['required', 'boolean'], 'essential' => ['required', 'boolean'],
|
||||
'associations' => ['nullable', 'array', 'max:40'], 'associations.*.target_type' => ['required', 'in:theme,module,activity,stage,procedure,entity,territory,document,investment_type,legal_source'.($this->requirementEvidenceEnabled() ? ',requirement' : '')],
|
||||
'associations' => ['nullable', 'array', 'max:40'], 'associations.*.target_type' => ['required', 'in:theme,module,activity,stage,procedure,entity,territory,document,investment_type,legal_source,sector,opportunity'.($this->requirementEvidenceEnabled() ? ',requirement' : '')],
|
||||
'associations.*.target_key' => ['required', 'string', 'max:180'],
|
||||
'associations.*.review_status' => ['nullable', 'in:pending,confirmed'],
|
||||
'associations.*.relationship' => ['required', 'in:applies_to,supports,version_of,translation_of,amends,repeals,implements,cites,related_to'],
|
||||
@@ -829,4 +866,17 @@ SQL, [$id]);
|
||||
|
||||
return $ids ?: [$id];
|
||||
}
|
||||
private function articleUrl(int $id): ?string
|
||||
{
|
||||
if (!Schema::hasColumn('portal_resources', 'editorial_content')) return null;
|
||||
$resource = DB::table('portal_resources')->find($id);
|
||||
if (!$resource) return null;
|
||||
if ($resource->editorial_content !== null) return '/artigos/'.$id;
|
||||
try {
|
||||
return app(\App\Services\EditorialReading::class)->original($resource) ? '/artigos/'.$resource->id : null;
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\HttpException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Procurement;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class ProcurementController extends Controller
|
||||
{
|
||||
private const STATE_SQL = "CASE WHEN v.payload->>'procedural_state' IN ('cancelled','results') THEN v.payload->>'procedural_state' WHEN ((v.payload->>'deadline')::timestamp AT TIME ZONE (v.payload->>'timezone'))<=CURRENT_TIMESTAMP THEN 'deadline_ended' WHEN v.payload->>'extended'='true' THEN 'extended' ELSE 'open' END";
|
||||
|
||||
private function query(Procurement $service)
|
||||
{
|
||||
abort_unless($service->available(), 404);
|
||||
|
||||
return DB::table('portal_tenders as t')->join('portal_tender_versions as v', fn ($q) => $q->on('v.id', '=', 't.published_version_id')->on('v.tender_id', '=', 't.id'))->whereNotNull('v.published_at');
|
||||
}
|
||||
|
||||
public function index(Request $request, Procurement $service)
|
||||
{
|
||||
$filters = $request->validate(['q' => 'nullable|string|max:150', 'entity' => 'nullable|uuid', 'sector' => 'nullable|integer', 'territory' => 'nullable|uuid', 'modality' => 'nullable|string|max:200', 'financier' => 'nullable|string|max:200', 'state' => 'nullable|in:open,extended,deadline_ended,cancelled,results', 'deadline_before' => 'nullable|date_format:Y-m-d', 'regime' => 'nullable|in:state,external,private,other']);
|
||||
$filters = array_filter($filters, fn ($value) => $value !== null && $value !== '');
|
||||
$query = $this->query($service);
|
||||
if (! empty($filters['q'])) {
|
||||
$query->where(fn ($q) => $q->whereRaw("v.payload->>'title' ILIKE ?", ['%'.$filters['q'].'%'])->orWhereRaw("v.payload->>'object' ILIKE ?", ['%'.$filters['q'].'%'])->orWhereRaw("v.payload->>'reference' ILIKE ?", ['%'.$filters['q'].'%']));
|
||||
}
|
||||
foreach (['entity' => 'organization_id', 'sector' => 'sector_id', 'modality' => 'modality', 'financier' => 'financier', 'regime' => 'regime'] as $filter => $field) {
|
||||
if (! empty($filters[$filter])) {
|
||||
$query->whereRaw('v.payload->>? = ?', [$field, (string) $filters[$filter]]);
|
||||
}
|
||||
}
|
||||
if (! empty($filters['territory'])) {
|
||||
$query->whereRaw("v.payload->>'territory_id' IN (WITH RECURSIVE scope(id,depth) AS (SELECT id,0 FROM geo_locations WHERE id=? AND deleted_at IS NULL UNION ALL SELECT c.id,s.depth+1 FROM geo_locations c JOIN scope s ON c.parent_id=s.id WHERE c.deleted_at IS NULL AND s.depth<4) SELECT id::text FROM scope)", [$filters['territory']]);
|
||||
}
|
||||
if (! empty($filters['state'])) {
|
||||
$query->whereRaw(self::STATE_SQL.' = ?', [$filters['state']]);
|
||||
}
|
||||
if (! empty($filters['deadline_before'])) {
|
||||
$query->whereRaw("left(v.payload->>'deadline',10)<=?", [$filters['deadline_before']]);
|
||||
}
|
||||
$rows = $query->select('t.id', 'v.id as vid')->orderByDesc('v.published_at')->paginate(20)->withQueryString();
|
||||
$rows->through(fn ($r) => ['id' => $r->id, ...$this->summary($service, $r->vid)]);
|
||||
$facets = $this->query($service)->get(['v.payload'])->map(fn ($r) => json_decode($r->payload, true));
|
||||
$options = [];
|
||||
foreach (['entity' => ['organization_id', 'organization_name'], 'sector' => ['sector_id', 'sector_name'], 'territory' => ['territory_id', 'territory_name'], 'modality' => ['modality', 'modality'], 'financier' => ['financier', 'financier']] as $name => [$value,$label]) {
|
||||
$options[$name] = $facets->filter(fn ($p) => ! empty($p[$value]))->map(fn ($p) => ['id' => (string) $p[$value], 'name' => $p[$label]])->unique('id')->values();
|
||||
}
|
||||
|
||||
return Inertia::render('procurement/index', ['items' => $rows, 'filters' => $filters, 'options' => $options, 'regimes' => config('procurement.regimes'), 'management' => $service->access($request->user())])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
private function summary(Procurement $service, string $vid): array
|
||||
{
|
||||
$v = $service->publishedVersion($vid);
|
||||
|
||||
return ['publication' => $v, 'state' => $service->state($v)];
|
||||
}
|
||||
|
||||
public function show(Request $request, string $id, Procurement $service)
|
||||
{
|
||||
return $this->detail($request, $id, $service, false);
|
||||
}
|
||||
|
||||
public function print(Request $request, string $id, Procurement $service)
|
||||
{
|
||||
return $this->detail($request, $id, $service, true);
|
||||
}
|
||||
|
||||
private function detail(Request $request, string $id, Procurement $service, bool $print)
|
||||
{
|
||||
$row = $this->query($service)->where('t.id', $id)->firstOrFail(['t.id', 'v.id as vid']);
|
||||
$filters = $request->only(['q', 'entity', 'sector', 'territory', 'modality', 'financier', 'state', 'deadline_before', 'regime', 'page']);
|
||||
|
||||
return Inertia::render('procurement/show', ['id' => $id, ...$this->summary($service, $row->vid), 'printMode' => $print, 'backUrl' => '/concursos'.($filters ? '?'.http_build_query($filters) : ''),
|
||||
'history' => DB::table('portal_tender_versions')->where('tender_id', $id)->whereNotNull('published_at')->orderBy('version')->pluck('id')->map(fn ($vid) => $service->publishedVersion($vid)),
|
||||
'watching' => $request->user() && DB::table('portal_tender_watches')->where('tender_id', $id)->where('user_id', $request->user()->id)->exists()])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function manage(Request $request, Procurement $service)
|
||||
{
|
||||
$service->guard($request->user());
|
||||
|
||||
return Inertia::render('procurement/manage', ['items' => DB::table('portal_tenders')->orderByDesc('updated_at')->get(['id', 'draft', 'version', 'published_version_id'])->map(fn ($r) => ['id' => $r->id, 'title' => json_decode($r->draft, true)['title'] ?? 'Rascunho sem título', 'version' => $r->version, 'published' => (bool) $r->published_version_id])])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function edit(Request $request, Procurement $service, ?string $id = null)
|
||||
{
|
||||
$service->guard($request->user());
|
||||
$r = $id ? $service->record($id) : null;
|
||||
|
||||
return Inertia::render('procurement/edit', ['record' => $r ? ['id' => $id, 'version' => $r->version, 'draft' => json_decode($r->draft, true), 'published' => (bool) $r->published_version_id] : null,
|
||||
'references' => $service->references($request->user()), 'history' => $id ? DB::table('portal_tender_versions')->where('tender_id', $id)->orderByDesc('version')->get(['version', 'act', 'reason', 'actor_id', 'published_at', 'created_at']) : []])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function save(Request $request, Procurement $service, ?string $id = null)
|
||||
{
|
||||
$id = $service->save($request->user(), $id, $request->all());
|
||||
|
||||
return redirect('/management/concursos/'.$id)->with('success', 'Rascunho guardado. A publicação anterior permanece intacta.');
|
||||
}
|
||||
|
||||
public function preview(Request $request, string $id, Procurement $service)
|
||||
{
|
||||
return response()->json($service->preview($request->user(), $id, (string) $request->input('act')))->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
|
||||
public function publish(Request $request, string $id, Procurement $service)
|
||||
{
|
||||
$service->publish($request->user(), $id, $request->all());
|
||||
|
||||
return redirect('/management/concursos/'.$id)->with('success', 'Acto publicado, com versão e auditoria. Nenhuma proposta recebida pelo portal.');
|
||||
}
|
||||
|
||||
public function watch(Request $request, string $id, Procurement $service)
|
||||
{
|
||||
$row = $this->query($service)->where('t.id', $id)->firstOrFail(['v.id']);
|
||||
DB::table('portal_tender_watches')->updateOrInsert(['tender_id' => $id, 'user_id' => $request->user()->id], ['seen_version_id' => $row->id, 'created_at' => now(), 'updated_at' => now()]);
|
||||
|
||||
return back()->with('success', 'Concurso guardado para acompanhamento. Isto não apresenta uma proposta.');
|
||||
}
|
||||
|
||||
public function unwatch(Request $request, string $id, Procurement $service)
|
||||
{
|
||||
abort_unless($service->available(), 404);
|
||||
DB::table('portal_tender_watches')->where('tender_id', $id)->where('user_id', $request->user()->id)->delete();
|
||||
|
||||
return back();
|
||||
}
|
||||
|
||||
public function watched(Request $request, Procurement $service)
|
||||
{
|
||||
$rows = $this->query($service)->join('portal_tender_watches as w', 'w.tender_id', '=', 't.id')->where('w.user_id', $request->user()->id)->get(['t.id', 'v.id as vid', 'w.seen_version_id'])->map(fn ($r) => ['id' => $r->id, 'changed' => $r->seen_version_id !== $r->vid, ...$this->summary($service, $r->vid)]);
|
||||
|
||||
return Inertia::render('procurement/manage', ['watched' => true, 'items' => $rows])->toResponse($request)->header('Cache-Control', 'private, no-store');
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ class EnsureScopedEditorial
|
||||
return $next($request);
|
||||
}
|
||||
abort_unless($access->editorial($actor), 403);
|
||||
$allowed = in_array($request->route()->getName(), ['editorial.edit', 'editorial.save', 'services.resource', 'services.resource.update'], true)
|
||||
$allowed = in_array($request->route()->getName(), ['editorial.edit', 'editorial.save', 'editorial.preview', 'editorial.preview-content', 'services.resource', 'services.resource.update'], true)
|
||||
|| ($request->route()->getName() === 'services.moderate' && $request->route('type') === 'resources');
|
||||
abort_unless($allowed, 403);
|
||||
if ($request->route('id') !== null) {
|
||||
|
||||
@@ -19,7 +19,8 @@ final class EnsureScopedPortalAdministration
|
||||
$path = $request->path();
|
||||
$interests = $request->is('management/interests', 'management/interests/*', 'api/v1/investment/admin/interests/*');
|
||||
// Fiscal maintenance reuses authorised administration; unrelated gates remain unchanged.
|
||||
$catalogue = $request->is('management/economic-references/lot-01', 'management/economic-references/lot-01/*',
|
||||
$catalogue = $request->is('management/territory-geometry-import', 'management/territory-geometry-import/*',
|
||||
'management/economic-references/lot-01', 'management/economic-references/lot-01/*',
|
||||
'management/education', 'management/education/*', 'management/investment/references', 'management/investment/references/*', 'api/v1/investment/admin/references', 'api/v1/investment/admin/references/*',
|
||||
'management/investment/zones', 'management/investment/zones/*', 'api/v2/investment/admin/zones', 'api/v2/investment/admin/zones/*',
|
||||
'management/balcao/atribuicoes', 'management/balcao/atribuicoes/*', 'management/balcao/servicos',
|
||||
|
||||
@@ -51,8 +51,10 @@ class HandleInertiaRequests extends Middleware
|
||||
return [
|
||||
...parent::share($request),
|
||||
'name' => config('app.name'),
|
||||
'procurementEnabled' => app(\App\Services\Procurement::class)->available(),
|
||||
'homologationReserved' => (bool) config('portal.homologation_only', false),
|
||||
'profileAccess' => app(PortalProfileAccess::class)->navigation($request->user()),
|
||||
'contentManagementHref' => app(\App\Services\ContentManagement::class)->access($request->user()) !== [] ? '/gestao-conteudos' : null,
|
||||
'portalOperator' => config('portal.operator_name'),
|
||||
'portalInstitutionalContactEmail' => filter_var(config('portal.institutional_contact_email'), FILTER_VALIDATE_EMAIL) ?: null,
|
||||
'legacyContentEnabled' => app(LegacyContent::class)->available(),
|
||||
@@ -68,7 +70,7 @@ class HandleInertiaRequests extends Middleware
|
||||
'create' => ['label' => 'Nova oportunidade', 'href' => '/management/opportunities/create'],
|
||||
'installationHref' => '/management/opportunity-management-install',
|
||||
] : null,
|
||||
'flash' => ['success' => fn () => $request->session()->get('success'), 'warning' => fn () => $request->session()->get('warning')],
|
||||
'flash' => ['success' => fn () => $request->session()->get('success'), 'warning' => fn () => $request->session()->get('warning'), 'uploaded_document_id' => fn () => $request->session()->get('uploaded_document_id')],
|
||||
'scopedAdministration' => $request->user() && app(\App\Services\ScopedPortalAdministration::class)->administrative($request->user()) ? [
|
||||
['label'=>'Zonas e parques','href'=>'/management/investment/zones'],
|
||||
['label'=>'Referências económicas','href'=>'/management/economic-references/lot-01'],
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/** Navigation over existing authorisations; never grants a role or publishes content. */
|
||||
final class ContentManagement
|
||||
{
|
||||
public function access(?User $actor): array
|
||||
{
|
||||
if (! $actor || ! $actor->hasVerifiedEmail()) return [];
|
||||
$profiles = app(PortalProfileAccess::class);
|
||||
$navigation = $profiles->navigation($actor);
|
||||
$legacy = $profiles->legacyWriteAllowed($actor);
|
||||
$documents = app(DocumentManagementAccess::class);
|
||||
$editor = $documents->full($actor) || $documents->editorial($actor);
|
||||
$upload = $editor || $documents->limited($actor);
|
||||
$result = [];
|
||||
if (app(Procurement::class)->available() && app(Procurement::class)->access($actor)) $result['procurement'] = ['index' => '/management/concursos', 'create' => '/management/concursos/novo'];
|
||||
if ($legacy || ($navigation['mine'] ?? null) || ($navigation['review'] ?? null)) {
|
||||
$result['opportunities'] = ['index' => $legacy ? '/management/opportunities' : '/minhas-oportunidades',
|
||||
'create' => $legacy ? '/management/opportunities/create' : ($navigation['create'] ?? null), 'profileWorkflow' => ! $legacy];
|
||||
}
|
||||
if ($upload) $result['documents'] = ['index' => '/management/portal-services', 'edit' => $editor];
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function view(User $actor): array
|
||||
{
|
||||
$access = $this->access($actor);
|
||||
abort_unless($access !== [], 403);
|
||||
$counts = [];
|
||||
if (isset($access['opportunities'])) {
|
||||
$counts['opportunities'] = app(PortalProfileAccess::class)->scope(Opportunity::query(), $actor)
|
||||
->selectRaw("COALESCE(workflow_context->>'working_state',status) as state, count(*) as total")
|
||||
->groupByRaw("COALESCE(workflow_context->>'working_state',status)")->pluck('total', 'state');
|
||||
}
|
||||
if (isset($access['documents'])) {
|
||||
$documents = app(DocumentManagementAccess::class);
|
||||
$query = $documents->full($actor) ? DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->whereNull('d.deleted_at')
|
||||
: ($access['documents']['edit'] ? $documents->editorialResources($actor) : null);
|
||||
$counts['documents'] = $query ? $query->select('r.published', DB::raw('count(*) as total'))->groupBy('r.published')->get()
|
||||
->mapWithKeys(fn ($row) => [$row->published ? 'published' : 'unpublished' => (int) $row->total]) : [];
|
||||
}
|
||||
return ['access' => $access, 'counts' => $counts];
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,9 @@ class DocumentManagementAccess
|
||||
/** Restore the approved editorial operation without a global management grant. */
|
||||
public function editorial(User $actor): bool
|
||||
{
|
||||
return config('portal.homologation_only', false)
|
||||
&& in_array(strtolower(trim($actor->email)), config('homologation_editorial.accounts', []), true)
|
||||
// Tempo's named editorial designation does not depend on whether the
|
||||
// site's technical reviewer reservation is enabled.
|
||||
return in_array(strtolower(trim($actor->email)), config('homologation_editorial.accounts', []), true)
|
||||
&& app(PortalProfileAccess::class)->superuser($actor)
|
||||
&& ! app(PortalProfileAccess::class)->has($actor, 'auditor')
|
||||
&& app(PortalProfileAccess::class)->mfa($actor);
|
||||
@@ -49,6 +50,11 @@ class DocumentManagementAccess
|
||||
->where('d.external_id', $item['external_id']);
|
||||
});
|
||||
}
|
||||
foreach (json_decode(file_get_contents(config_path('legacy_editorial_resources.json')), true, 512, JSON_THROW_ON_ERROR) as $item) {
|
||||
// These exact original bytes contain the legacy identity/source.
|
||||
// Destination upload IDs differ; the approved file scope does not.
|
||||
$query->orWhere('d.sha256', $item['sha256']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use GuzzleHttp\Psr7\Uri;
|
||||
use GuzzleHttp\Psr7\UriResolver;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
/** Reads verified originals without writing a derived edition or changing publication. */
|
||||
final class EditorialReading
|
||||
{
|
||||
public function publicResources()
|
||||
{
|
||||
return DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')
|
||||
->where('r.published', true)->where('r.rights_status', 'authorized')->where('d.rights_status', 'authorized')
|
||||
->where('d.status', 'active')->where('d.data_classification', 'public')->whereNull('d.deleted_at')
|
||||
->where('d.malware_scan_status', 'clean')->whereColumn('d.sha256', 'd.malware_scanned_sha256');
|
||||
}
|
||||
|
||||
public function original(object $resource): ?array
|
||||
{
|
||||
$document = DB::table('documents')->find($resource->document_id);
|
||||
if (! $document || $document->status !== 'active' || $document->malware_scan_status !== 'clean' || ! in_array($document->media_type, ['text/plain', 'application/json'], true)) {
|
||||
return null;
|
||||
}
|
||||
$binary = app(VerifiedDocumentBinary::class)->respond($document);
|
||||
$bytes = $binary instanceof BinaryFileResponse ? file_get_contents($binary->getFile()->getPathname()) : $binary->getContent();
|
||||
if (! is_string($bytes) || strlen($bytes) > 10485760) {
|
||||
return null;
|
||||
}
|
||||
$original = json_decode($bytes, true);
|
||||
|
||||
return is_array($original) && ($original['format'] ?? '') === 'investmoz-legacy-article-v1' && is_string($original['content_html'] ?? null) ? $original : null;
|
||||
}
|
||||
|
||||
public function plain(string $html): string
|
||||
{
|
||||
return trim(html_entity_decode(strip_tags(preg_replace('~</(?:p|div|li|h[1-6]|tr)>|<br\s*/?>~i', "\n\n", $html)), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
||||
}
|
||||
|
||||
public function content(object $resource, ?array $original): array
|
||||
{
|
||||
return $resource->editorial_content ? json_decode($resource->editorial_content, true, 512, JSON_THROW_ON_ERROR)
|
||||
: ['body' => $this->plain($original['content_html'] ?? ''), 'image_resource_id' => null, 'image_mode' => 'original', 'preserve_original_html' => true];
|
||||
}
|
||||
|
||||
private function sourceUrl(mixed $url, ?string $base = null): ?string
|
||||
{
|
||||
if (! is_string($url) || trim($url) === '') {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
$absolute = $base ? (string) UriResolver::resolve(new Uri($base), new Uri($url)) : $url;
|
||||
|
||||
return filter_var($absolute, FILTER_VALIDATE_URL) && in_array(strtolower((string) parse_url($absolute, PHP_URL_SCHEME)), ['http', 'https'], true) ? $absolute : null;
|
||||
} catch (\InvalidArgumentException) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function reading(object $resource, ?array $override = null, bool $private = false): array
|
||||
{
|
||||
$original = $this->original($resource);
|
||||
$content = $override ?? $this->content($resource, $original);
|
||||
abort_unless(trim($content['body'] ?? '') !== '', 404, 'Este documento não contém um artigo legível.');
|
||||
$document = DB::table('documents')->find($resource->document_id);
|
||||
// The original is verified above; ordinary text adaptations retain the binary gate too.
|
||||
app(VerifiedDocumentBinary::class)->respond($document);
|
||||
$mode = $content['image_mode'] ?? (! empty($content['image_resource_id']) ? 'selected' : 'none');
|
||||
$originalImageUrl = $this->sourceUrl($original['image_source_url'] ?? null, $original['source_url'] ?? null);
|
||||
$expectedImageSha = $original['image_sha256'] ?? null;
|
||||
if (! $expectedImageSha && $originalImageUrl === 'https://investemoz.tempo.co.mz/images/tempo-background.png') {
|
||||
foreach (json_decode(file_get_contents(config_path('legacy_editorial_resources.json')), true, 512, JSON_THROW_ON_ERROR) as $accepted) {
|
||||
if ($accepted['sha256'] === $document->sha256) {
|
||||
$expectedImageSha = $accepted['original_image_sha256'] ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$images = $this->publicResources()->whereIn('d.media_type', ['image/png', 'image/jpeg', 'image/webp']);
|
||||
$image = null;
|
||||
if ($mode === 'selected' && ! empty($content['image_resource_id'])) {
|
||||
$image = (clone $images)->where('r.id', $content['image_resource_id'])->first(['r.id', 'r.title']);
|
||||
}
|
||||
if ($mode === 'original' && $original) {
|
||||
$image = (clone $images)->where(function ($q) use ($expectedImageSha, $originalImageUrl) {
|
||||
$q->whereRaw('1=0');
|
||||
if ($expectedImageSha) {
|
||||
$q->orWhere('d.sha256', $expectedImageSha);
|
||||
} elseif ($originalImageUrl) {
|
||||
$q->orWhere('r.source_url', $originalImageUrl)->orWhere('d.original_source', $originalImageUrl);
|
||||
}
|
||||
})->first(['r.id', 'r.title']);
|
||||
}
|
||||
if ($image) {
|
||||
// A public flag alone never licenses missing or changed image bytes.
|
||||
$binary = (clone $images)->where('r.id', $image->id)->first(['d.*']);
|
||||
try {
|
||||
app(VerifiedDocumentBinary::class)->respond($binary);
|
||||
} catch (HttpException) {
|
||||
$image = null;
|
||||
}
|
||||
}
|
||||
$originalFormat = $original && ($content['preserve_original_html'] ?? false) && $content['body'] === $this->plain($original['content_html']);
|
||||
$html = $originalFormat ? app(LegacyContent::class)->sanitize($original['content_html'], true) : null;
|
||||
$imageReferences = [];
|
||||
if ($original) {
|
||||
preg_match_all('~<img\b[^>]*\bsrc\s*=\s*([\'"])(.*?)\1~is', $original['content_html'], $matches);
|
||||
foreach ($matches[2] as $source) {
|
||||
$url = $this->sourceUrl(html_entity_decode($source, ENT_QUOTES | ENT_HTML5, 'UTF-8'), $original['source_url'] ?? null);
|
||||
if ($url) {
|
||||
$imageReferences[] = $url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['id' => $resource->id, 'title' => $resource->title, 'description' => $resource->description,
|
||||
'author' => $document->issuer, 'body' => $content['body'], 'html' => $html,
|
||||
'image_url' => $image ? '/documentos/'.$image->id.'/view' : null,
|
||||
'image_notice' => $mode === 'selected' && ! $image ? 'A imagem escolhida está indisponível para divulgação; a selecção mantém-se na edição.' : ($mode === 'original' && ! $image && ! empty($original['image_source_url']) ? 'Imagem de origem referenciada, mas sem exemplar público verificado na biblioteca. A referência original mantém-se preservada.' : null),
|
||||
'source_url' => $this->sourceUrl($original['source_url'] ?? $resource->source_url),
|
||||
'published_at' => $original['published_at'] ?? null, 'modified_at' => $original['modified_at'] ?? null,
|
||||
'legacy_id' => $original['legacy_id'] ?? null, 'image_source_url' => $originalImageUrl, 'image_expected_sha256' => $expectedImageSha, 'image_references' => array_values(array_unique($imageReferences)),
|
||||
'metadata_version' => $resource->metadata_version, 'private_preview' => $private,
|
||||
'original_url' => $private ? '/management/documents/'.$resource->document_id.'/download' : '/documentos/'.$resource->id,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -231,16 +231,25 @@ class InvestmentCatalogue
|
||||
} elseif (in_array($relation, ['clusters', 'corridors'], true)) {
|
||||
$query->whereIn('id', $this->publicStructureIds($relation, $foreign));
|
||||
} else {
|
||||
$query->whereIn('id', InvestmentPortfolioController::visible()->whereNotNull($foreign)->pluck($foreign));
|
||||
$query->whereIn('id', app(OpportunityClassification::class)->targets('project')->select('id'));
|
||||
}
|
||||
}
|
||||
if (! $management && $path !== [] && $relation !== 'locations') {
|
||||
$dossiers = InvestmentPortfolioController::visible();
|
||||
app(InvestmentGeography::class)->constrainDossiersToTerritories($dossiers,
|
||||
app(InvestmentGeography::class)->descendants($path[array_key_last($path)]->id));
|
||||
$query->whereIn('id', $dossiers->whereNotNull($foreign)->select($foreign));
|
||||
$territories = app(InvestmentGeography::class)->descendants($path[array_key_last($path)]->id);
|
||||
$query->where(fn ($area) => $area->whereIn('primary_territory_id', $territories)
|
||||
->orWhereIn('id', $dossiers->whereNotNull($foreign)->select($foreign)));
|
||||
}
|
||||
$result[$relation] = $query->orderBy('name')->get(['id', 'name']);
|
||||
if (! $management && $relation !== 'locations') {
|
||||
$dossiers = InvestmentPortfolioController::visible()->whereIn($foreign, $result[$relation]->pluck('id'))
|
||||
->orderBy('id')->get(['id', $foreign])->unique($foreign)->keyBy($foreign);
|
||||
$result[$relation]->each(function ($record) use ($dossiers): void {
|
||||
$record->url = isset($dossiers[$record->id]) ? '/carteira/'.$dossiers[$record->id]->id : null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$zones = app(InvestmentZones::class)->visible();
|
||||
@@ -250,24 +259,15 @@ class InvestmentCatalogue
|
||||
['province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'geo_location_id']));
|
||||
}
|
||||
$result['zones'] = $zones->orderBy('name')->get(['id', 'name', 'category']);
|
||||
if (! $management && ($path !== [] || ! empty($territoryFilters['sector']))) {
|
||||
$scope = \Illuminate\Support\Arr::only($territoryFilters, ['sector','province_id','district_id','admin_post_id','locality_id','include_national']);
|
||||
$available = ['chain' => [], 'activity' => []];
|
||||
$this->query($scope)->reorder()->chunkById(200, function ($records) use (&$available) {
|
||||
foreach ($records as $record) {
|
||||
foreach (app(OpportunityClassification::class)->publicRows($record) as $row) {
|
||||
if (isset($available[$row['target_type']])) $available[$row['target_type']][] = $row['target_id'];
|
||||
}
|
||||
foreach (['subsector_id','economic_activity_id'] as $key) {
|
||||
$id = $record->workflow_context['publication']['taxonomy'][$key] ?? null;
|
||||
if ($id) $available['activity'][] = $id;
|
||||
}
|
||||
}
|
||||
}, 'opportunities.id', 'id');
|
||||
foreach ($available as $kind => $ids) {
|
||||
$result['classification']['targets'][$kind] = collect($result['classification']['targets'][$kind])->whereIn('id', $ids)->values();
|
||||
}
|
||||
if (! $management && $path !== []) {
|
||||
// A chain's documented scope comes from its public cluster, never
|
||||
// from the opportunities surviving the current search.
|
||||
$ids = app(OpportunityClassification::class)->targets('chain')
|
||||
->whereIn('cluster_id', $result['clusters']->pluck('id'))->pluck('id');
|
||||
$result['classification']['targets']['chain'] = collect($result['classification']['targets']['chain'])->whereIn('id', $ids)->values();
|
||||
}
|
||||
// Activities are a public classification catalogue, not evidence of
|
||||
// investment availability in a territory or sector.
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
/** Context belongs to the existing encrypted dossier. Sources remain read-only. */
|
||||
@@ -22,7 +23,7 @@ final class InvestorContext
|
||||
|
||||
public const SOURCE = 'investor-context';
|
||||
|
||||
public const FIELDS = ['project_title', 'project_summary', 'sector_id', 'sector', 'province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location', 'currency', 'expected_start_date', 'fiscal_period', 'fiscal_periodicity', 'fiscal_month', 'investor_origin', 'person_type', 'entity_stage', 'tax_residency_country', 'fiscal_residency_country', 'activity_id', 'project_type', 'maturity_stage', 'special_zone_id', 'investment_regime'];
|
||||
public const FIELDS = ['project_title', 'project_summary', 'sector_id', 'sector', 'province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location', 'currency', 'expected_start_date', 'fiscal_period', 'fiscal_periodicity', 'fiscal_month', 'investor_origin', 'person_type', 'entity_stage', 'tax_residency_country', 'fiscal_residency_country', 'activity_id', 'project_type', 'maturity_stage', 'special_zone_id', 'investment_regime', ...PrivateInvestmentPreparation::FIELDS];
|
||||
|
||||
public function __construct(private readonly VirtualDesk $desk) {}
|
||||
|
||||
@@ -267,10 +268,20 @@ final class InvestorContext
|
||||
|
||||
private function input(array $input): array
|
||||
{
|
||||
abort_if(array_diff(array_keys($input), ['version', 'selection', 'declarations', 'replace_fields', 'preview_hash', 'operation_key', 'confirm', '_token']), 422, 'Campo de controlo desconhecido.');
|
||||
abort_if(array_diff(array_keys($input), ['version', 'selection', 'declarations', 'replace_fields', 'preview_hash', 'operation_key', 'confirm', 'document_links', '_token']), 422, 'Campo de controlo desconhecido.');
|
||||
|
||||
return Validator::make($input, ['version' => 'required|integer|min:1', 'selection' => 'present|array:opportunity,sector,territory,park',
|
||||
'selection.*' => 'nullable|string|max:100', 'declarations' => 'nullable|array:project_title,project_summary,currency,expected_start_date,investor_origin,person_type,entity_stage,tax_residency_country,fiscal_residency_country,project_type,maturity_stage', 'declarations.project_title' => 'nullable|string|max:200', 'declarations.project_summary' => 'nullable|string|max:5000',
|
||||
abort_if(! app(PrivateInvestmentPreparation::class)->available() && (! empty($input['document_links']) || array_intersect(array_keys(is_array($input['declarations'] ?? null) ? $input['declarations'] : []), PrivateInvestmentPreparation::FIELDS)), 409, 'Esta preparação ainda não está disponível neste ambiente. As respostas foram preservadas.');
|
||||
|
||||
$inputRules = [
|
||||
'document_links' => 'sometimes|array:'.implode(',', PrivateInvestmentPreparation::DOCUMENTS),
|
||||
'document_links.*' => 'nullable|uuid',
|
||||
];
|
||||
foreach (app(PrivateInvestmentPreparation::class)->questions() as $question) {
|
||||
$inputRules['declarations.'.$question['key']] = isset($question['options']) ? ['nullable', Rule::in(array_keys($question['options']))] : ['nullable', 'regex:/^\d+(?:\.\d+)?$/D', 'numeric', 'min:0', 'max:999999999999999999'];
|
||||
}
|
||||
|
||||
return Validator::make($input, [...$inputRules, 'version' => 'required|integer|min:1', 'selection' => 'present|array:opportunity,sector,territory,park',
|
||||
'selection.*' => 'nullable|string|max:100', 'declarations' => 'nullable|array:project_title,project_summary,currency,expected_start_date,investor_origin,person_type,entity_stage,tax_residency_country,fiscal_residency_country,project_type,maturity_stage,'.implode(',', PrivateInvestmentPreparation::FIELDS), 'declarations.project_title' => 'nullable|string|max:200', 'declarations.project_summary' => 'nullable|string|max:5000',
|
||||
'declarations.currency' => 'nullable|regex:/^[A-Z]{3}$/', 'declarations.expected_start_date' => 'nullable|date_format:Y-m-d', 'declarations.investor_origin' => 'nullable|in:national,foreign,mixed', 'declarations.person_type' => 'nullable|in:singular,collective,sole_trader,consortium', 'declarations.entity_stage' => 'nullable|in:existing,being_formed', 'declarations.tax_residency_country' => ['nullable', 'string', app(CountryCatalogue::class)->rule()], 'declarations.fiscal_residency_country' => ['nullable', 'string', app(CountryCatalogue::class)->rule()], 'declarations.project_type' => 'nullable|string|max:100', 'declarations.maturity_stage' => 'nullable|string|max:100',
|
||||
'replace_fields' => 'nullable|array', 'replace_fields.*' => 'string|in:'.implode(',', self::FIELDS)])->validate();
|
||||
}
|
||||
@@ -283,6 +294,15 @@ final class InvestorContext
|
||||
abort_unless((int) $case->version === $data['version'], 409, 'O dossier mudou; recupere a versão atual.');
|
||||
app(InvestorPath::class)->view($actor, $id);
|
||||
$old = $this->state($case);
|
||||
$links = $old['preparation_documents'] ?? [];
|
||||
foreach ($data['document_links'] ?? [] as $key => $document) {
|
||||
if ($document) {
|
||||
abort_unless(DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $id)->where('d.id', $document)->whereNull('d.deleted_at')->exists(), 422, 'Seleccione um documento já associado a este dossier; os dados anteriores foram preservados.');
|
||||
$links[$key] = $document;
|
||||
} else {
|
||||
unset($links[$key]);
|
||||
}
|
||||
}
|
||||
$selection = $old['selection'];
|
||||
foreach ($data['selection'] as $slot => $key) {
|
||||
if ($key === null || $key === '') {
|
||||
@@ -353,16 +373,29 @@ final class InvestorContext
|
||||
$changedFields = array_column($replaced, 'field');
|
||||
$conditionsChanged = (bool) array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary']));
|
||||
$dependencyChanged = $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || $conditionsChanged;
|
||||
$privateConditionsChanged = (bool) array_intersect($changedFields, PrivateInvestmentPreparation::FIELDS);
|
||||
$calculationInputsChanged = $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || (bool) array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary', ...array_diff(PrivateInvestmentPreparation::FIELDS, ['total_investment', 'area_required', 'area_unit'])]));
|
||||
$payload = $this->desk->decode($case->payload);
|
||||
$results = $payload['investor_path']['results'] ?? [];
|
||||
$requests = DB::table('desk_requests')->where('case_id', $id)->orderBy('id')->get(['id', 'status', 'protocol', 'version'])->all();
|
||||
$steps = $dependencyChanged ? array_values(array_unique([...(array_intersect($changed, ['opportunity', 'sector']) ? ['S02', 'S03', 'S04', 'S08', 'S12', 'S13', 'S14', 'S15', 'S17'] : []), ...(array_intersect($changed, ['territory', 'park']) ? ['S08', 'S09', 'S10', 'S11', 'S17'] : [])])) : [];
|
||||
if ($conditionsChanged) {
|
||||
if (array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary', ...PrivateInvestmentPreparation::FIELDS]))) {
|
||||
$steps = array_values(array_unique([...$steps, 'S02', 'S03', 'S08', 'S12', 'S13', 'S17']));
|
||||
}
|
||||
if ($privateConditionsChanged) {
|
||||
$steps = array_values(array_unique([...$steps, 'S02', 'S05', 'S06', 'S07']));
|
||||
}
|
||||
$privatePreparation = null;
|
||||
if (app(PrivateInvestmentPreparation::class)->available()) {
|
||||
$docs = $this->preparationDocuments($id);
|
||||
$beforePreparation = app(PrivateInvestmentPreparation::class)->assess($old, $docs);
|
||||
$afterPreparation = app(PrivateInvestmentPreparation::class)->assess(['fields' => $fields, 'selection' => $selection, 'preparation_documents' => $links], $docs);
|
||||
$privatePreparation = ['before' => $beforePreparation, 'after' => $afterPreparation];
|
||||
}
|
||||
$plan = ['case_id' => $id, 'version' => (int) $case->version, 'revision' => (int) $old['revision'], 'selection' => $selection, 'fields' => $fields, 'provenance' => $provenance,
|
||||
'updated' => $replaced, 'maintained' => $kept, 'changed_dimensions' => $changed, 'dependent_results' => (int) $case->user_id === $actor->id && $dependencyChanged ? array_map(fn ($r) => ['id' => $r['id'], 'engine' => $r['engine']], $results) : [],
|
||||
'requests' => $requests, 'review_steps' => $steps, 'dependency_changed' => $dependencyChanged, 'documents_preserved' => DB::table('desk_documents')->where('case_id', $id)->count(),
|
||||
'preparation_documents' => $links, 'private_investment' => $privatePreparation,
|
||||
'updated' => $replaced, 'maintained' => $kept, 'changed_dimensions' => $changed, 'dependent_results' => (int) $case->user_id === $actor->id && $calculationInputsChanged ? array_map(fn ($r) => ['id' => $r['id'], 'engine' => $r['engine']], $results) : [],
|
||||
'requests' => $requests, 'review_steps' => $steps, 'dependency_changed' => $dependencyChanged, 'calculation_inputs_changed' => $calculationInputsChanged, 'documents_preserved' => DB::table('desk_documents')->where('case_id', $id)->count(),
|
||||
'notice' => 'Não confirma incentivos, licenças, disponibilidade de terra, reserva ou autorização. Submissões e decisões anteriores mantêm o conteúdo enviado.'];
|
||||
$hash = hash_hmac('sha256', json_encode(Arr::sortRecursive([$actor->id, $data, $plan]), JSON_THROW_ON_ERROR), config('app.key'));
|
||||
|
||||
@@ -390,7 +423,7 @@ final class InvestorContext
|
||||
$before = $this->state($case);
|
||||
$plan = $this->preview($actor, $id, $input);
|
||||
abort_unless(hash_equals($plan['preview_hash'], $control['preview_hash']), 409, 'O dossier ou uma fonte mudou desde a prévia; compare novamente.');
|
||||
if (! $plan['changed_dimensions'] && ! $plan['updated'] && empty($input['declarations']) && empty($input['replace_fields'])) {
|
||||
if (! $plan['changed_dimensions'] && ! $plan['updated'] && empty($input['declarations']) && empty($input['replace_fields']) && empty($input['document_links'])) {
|
||||
$result = ['case_id' => $id, 'version' => (int) $case->version, 'revision' => $before['revision'], 'changed' => false, 'replayed' => false];
|
||||
DB::table('audit_events')->insert(['source_system' => self::SOURCE, 'entity_type' => 'desk_cases', 'entity_id' => $id, 'actor_user_id' => $actor->id, 'correlation_id' => $control['operation_key'], 'action' => 'context_unchanged', 'data_classification' => 'restricted', 'after_data' => json_encode(['case_id' => $id, 'request_hash' => $requestHash, 'result' => $result], JSON_THROW_ON_ERROR)]);
|
||||
|
||||
@@ -402,9 +435,9 @@ final class InvestorContext
|
||||
foreach ($plan['review_steps'] as $step) {
|
||||
$review[$step] = ['revision' => $revision, 'reason' => 'O contexto mudou; reveja os pressupostos e a evidência aplicável.'];
|
||||
}
|
||||
$context = ['revision' => $revision, 'selection' => $plan['selection'], 'fields' => $plan['fields'], 'provenance' => $plan['provenance'], 'review_steps' => $review,
|
||||
$context = ['preparation_documents' => $plan['preparation_documents'], 'preparation_snapshot' => $plan['private_investment']['after'] ?? $before['preparation_snapshot'] ?? null, 'revision' => $revision, 'selection' => $plan['selection'], 'fields' => $plan['fields'], 'provenance' => $plan['provenance'], 'review_steps' => $review,
|
||||
'dependency_hash' => $this->dependencyHash(['selection' => $plan['selection'], 'fields' => Arr::except($plan['fields'], ['project_title', 'project_summary'])]), 'updated_at' => now()->toIso8601String(), 'updated_by' => $actor->id];
|
||||
if ($plan['dependency_changed']) {
|
||||
if ($plan['calculation_inputs_changed']) {
|
||||
foreach ($payload['investor_path']['results'] ?? [] as $i => $r) {
|
||||
$payload['investor_path']['results'][$i] = [...$r, 'context_state' => 'review_required', 'context_invalidated_revision' => $revision, 'context_invalidated_at' => now()->toIso8601String()];
|
||||
}
|
||||
@@ -439,21 +472,30 @@ final class InvestorContext
|
||||
}
|
||||
}
|
||||
|
||||
return ['simulator_context_revision' => $simulatorRevision, 'case' => $path['case'], 'context' => $ctx, 'steps' => array_map(fn ($s) => Arr::only($s, ['code', 'title', 'state', 'context_review_required']), $path['steps']),
|
||||
$preparation = app(PrivateInvestmentPreparation::class)->available() ? app(PrivateInvestmentPreparation::class)->assess($ctx, $this->preparationDocuments($id)) : null;
|
||||
$ctx['preparation_assessment'] = $preparation;
|
||||
|
||||
return ['private_investment' => $preparation, 'preparation_documents' => $this->preparationDocuments($id), 'simulator_context_revision' => $simulatorRevision, 'case' => $path['case'], 'context' => $ctx, 'steps' => array_map(fn ($s) => Arr::only($s, ['code', 'title', 'state', 'context_review_required']), $path['steps']),
|
||||
'can_write' => $path['canWrite'], 'next' => $path['next'], 'results' => $path['results'], 'requests' => $path['requests'],
|
||||
'prefill' => $this->prefill($ctx), 'notice' => 'A seleção apoia a preparação e não confirma benefícios, licenças, direitos ou disponibilidade.'];
|
||||
}
|
||||
|
||||
public function preparationDocuments(string $id): array
|
||||
{
|
||||
return DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $id)->whereNull('d.deleted_at')->get(['l.id', 'l.document_id', 'd.file_name', 'd.status', 'd.malware_scan_status', 'd.version_no', 'd.sha256'])->map(fn ($row) => (array) $row)->all();
|
||||
}
|
||||
|
||||
public function prefill(array $ctx): array
|
||||
{
|
||||
$f = $ctx['fields'];
|
||||
$sim = Arr::except(Arr::only($f, self::FIELDS), ['sector', 'province', 'district', 'location']);
|
||||
$sim = Arr::except(Arr::only($f, self::FIELDS), ['sector', 'province', 'district', 'location', ...array_diff(PrivateInvestmentPreparation::FIELDS, ['total_investment', 'area_required', 'area_unit'])]);
|
||||
$zone = $ctx['selection']['park'] ?? null;
|
||||
$sim['context_applicability_snapshot'] = json_encode([
|
||||
'version' => 1, 'context_revision' => $ctx['revision'],
|
||||
'zone' => $zone ? Arr::only($zone, ['id', 'title', 'href', 'version', 'sha256', 'fields', 'analysis']) : null,
|
||||
'provenance' => $ctx['provenance'],
|
||||
'path' => $ctx['path_applicability'] ?? null,
|
||||
'preparation' => isset($ctx['preparation_assessment']) ? Arr::except($ctx['preparation_assessment'], ['questions']) : null,
|
||||
'fields' => Arr::only($f, ['investor_origin', 'tax_residency_country', 'entity_stage', 'currency', 'expected_start_date', 'investment_regime']),
|
||||
'notice' => 'Referências e declarações para preparação; sem reconhecimento automático de elegibilidade ou benefício.',
|
||||
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
@@ -136,7 +136,7 @@ final class InvestorPath
|
||||
$documents = DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')
|
||||
->where('l.case_id', $id)->whereNull('d.deleted_at')
|
||||
->when(! $owner && ! $manager, fn ($q) => $q->where(fn ($p) => $p->whereNull('l.request_id')->orWhereIn('l.request_id', $requests->pluck('id'))))
|
||||
->get(['l.id', 'l.document_id', 'd.file_name', 'd.status', 'd.malware_scan_status']);
|
||||
->get(['l.id', 'l.document_id', 'd.file_name', 'd.status', 'd.malware_scan_status', 'd.version_no', 'd.sha256']);
|
||||
// Only the private draft owner can receive the financial snapshot/result. Dossier access alone is insufficient.
|
||||
$results = (int) $case->user_id === $actor->id ? ($path['results'] ?? []) : [];
|
||||
$next = collect($steps)->first(fn ($step) => $step['preparation_allowed'] && ! in_array($step['state'], ['completed', 'approved', 'waiting_external', 'submitted', 'reviewing'], true));
|
||||
@@ -145,12 +145,17 @@ final class InvestorPath
|
||||
? LandAssistanceCase::where('user_id', $actor->id)->latest('updated_at')->get()
|
||||
->first(fn ($land) => ($land->payload['investor_path_case_id'] ?? null) === $id) : null;
|
||||
|
||||
return ['case' => ['id' => $id, 'title' => $case->title, 'version' => $case->version, 'closed' => $case->closed_at !== null],
|
||||
$preparation = ($owner || $manager) && app(PrivateInvestmentPreparation::class)->available() ? app(PrivateInvestmentPreparation::class)->assess($context, $documents->map(fn ($d) => (array) $d)->all()) : null;
|
||||
if ($preparation && $preparation['enabled']) {
|
||||
$next = ['code' => 'S06', 'title' => $preparation['next'], 'action' => 'Preparar', 'href' => '/investimentos/'.$id.'/contexto/editar#preparation-questions'];
|
||||
}
|
||||
|
||||
return ['private_investment' => $preparation, 'case' => ['id' => $id, 'title' => $case->title, 'version' => $case->version, 'closed' => $case->closed_at !== null],
|
||||
'journey_id' => $journey->id, 'definition_id' => $version->id, 'definition_version' => $version->version_no, 'source_sha256' => $definition['source_sha256'],
|
||||
'phases' => $definition['phases'], 'steps' => $steps, 'facts' => $facts, 'fact_reasons' => $path['fact_reasons'] ?? [],
|
||||
'fintech' => ($owner || $manager) && (isset($path['fintech']) || collect($path['sector_contexts'] ?? [])->contains(fn ($s) => in_array($s['slug'], ['fintech', 'tecnologias-de-informacao', 'mercado-financeiro'], true)) || collect($path['content_sources'] ?? [])->contains('return_href', '/tech-fintech')) ? app(FintechInvestment::class)->view($payload, $steps) : null,
|
||||
'canWrite' => $owner && ! $case->closed_at, 'canReview' => $manager && (int) $case->user_id !== $actor->id && ! $case->closed_at,
|
||||
'next' => $next ? ['code' => $next['code'], 'title' => $next['title'], 'action' => 'Preparar', 'href' => '#'.$next['code']] : null,
|
||||
'next' => $next ? ['code' => $next['code'], 'title' => $next['title'], 'action' => 'Preparar', 'href' => $next['href'] ?? '#'.$next['code']] : null,
|
||||
'investment_context' => ($owner || $manager) ? $context : null,
|
||||
'results' => $results, 'requests' => $requests, 'documents' => $documents,
|
||||
'zone_contexts' => ($owner || $manager) ? ($path['zone_contexts'] ?? []) : [],
|
||||
|
||||
@@ -279,7 +279,7 @@ final class LegacyContent
|
||||
}
|
||||
|
||||
/** Allow presentation markup only. No scripts, styles, events, embeds or remote image execution. */
|
||||
public function sanitize(string $html): string
|
||||
public function sanitize(string $html, bool $preserveStructure = false): string
|
||||
{
|
||||
$doc = new DOMDocument;
|
||||
$old = libxml_use_internal_errors(true);
|
||||
@@ -296,7 +296,7 @@ final class LegacyContent
|
||||
}
|
||||
$headingOffset = 2 - (empty($sourceLevels) ? 2 : min($sourceLevels));
|
||||
$previousHeading = 1;
|
||||
$clean = function (DOMNode $node) use (&$clean, $doc, $headingOffset, &$previousHeading): void {
|
||||
$clean = function (DOMNode $node) use (&$clean, $doc, $headingOffset, &$previousHeading, $preserveStructure): void {
|
||||
foreach (iterator_to_array($node->childNodes) as $child) {
|
||||
if (! $child instanceof DOMElement) {
|
||||
continue;
|
||||
@@ -311,7 +311,7 @@ final class LegacyContent
|
||||
$node->replaceChild($section, $child);
|
||||
$child = $section;
|
||||
}
|
||||
if (! in_array(strtolower($child->tagName), ['p', 'strong', 'b', 'em', 'i', 'ul', 'ol', 'li', 'br', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'a'], true)) {
|
||||
if (! in_array(strtolower($child->tagName), array_merge(['p', 'strong', 'b', 'em', 'i', 'ul', 'ol', 'li', 'br', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote', 'a'], $preserveStructure ? ['div', 'span', 'small', 'section', 'table', 'caption', 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'figure', 'figcaption'] : []), true)) {
|
||||
$node->removeChild($child);
|
||||
|
||||
continue;
|
||||
|
||||
@@ -184,7 +184,9 @@ class PortalProfileAccess
|
||||
|
||||
public function canCreate(User $actor, ?string $organization, string $type): bool
|
||||
{
|
||||
$included = in_array($type, OpportunityCanonicalContract::BASE_CATEGORIES, true) || app(OpportunityModularProfiles::class)->enabled();
|
||||
// Draft preparation is allowed within the assigned modality. Executable
|
||||
// submission support is checked separately by the canonical contract.
|
||||
$included = in_array($type, OpportunityManagement::CATEGORIES, true);
|
||||
|
||||
// This is creation as an authorized Promotor, never an exceptional SU act.
|
||||
return $included && $this->available() && $this->scoped($actor, 'promoter', $organization, $type) && ! $this->has($actor, 'auditor');
|
||||
@@ -370,7 +372,8 @@ class PortalProfileAccess
|
||||
foreach (json_decode($grant->opportunity_types, true, 512, JSON_THROW_ON_ERROR) as $type) {
|
||||
$key = ($grant->organization_id ?? 'individual').'|'.$type;
|
||||
if (! isset($seen[$key]) && $this->canCreate($actor, $grant->organization_id, $type)) {
|
||||
$contexts[] = ['organization_id' => $grant->organization_id, 'type' => $type];
|
||||
$contexts[] = ['organization_id' => $grant->organization_id,
|
||||
'organization_name' => $grant->organization_id ? DB::table('organizations')->where('id', $grant->organization_id)->value('name') : null, 'type' => $type];
|
||||
$seen[$key] = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Brick\Math\BigDecimal;
|
||||
|
||||
/** Adapter of the dossier's declared context to preparation of a private-investment request. No tax or entitlement decision. */
|
||||
final class PrivateInvestmentPreparation
|
||||
{
|
||||
public const VERSION = 'private-investment-preparation-2026-v1';
|
||||
|
||||
public const SOURCE = 'https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf';
|
||||
|
||||
public const HASH = 'ba1e9eaabfec5d0c4cb2d9f2444a6860f7a90f222034764197829be402d408ed';
|
||||
|
||||
public const FIELDS = ['investment_form', 'investment_preparation', 'investment_activity_scope', 'implementation_mode', 'capital_origin', 'site_tenure', 'project_concession', 'significant_implications', 'land_extension', 'forest_concession', 'forest_concession_hectares', 'total_investment', 'area_required', 'area_unit'];
|
||||
|
||||
public const DOCUMENTS = ['identity', 'registration', 'name_reservation', 'site_plan', 'lease', 'duat', 'representation_license', 'feasibility', 'financial_capacity', 'foreign_capital'];
|
||||
|
||||
public function available(): bool
|
||||
{
|
||||
return (bool) config('investor-path.private_preparation', false);
|
||||
}
|
||||
|
||||
public function questions(): array
|
||||
{
|
||||
$tri = ['unknown' => 'Por determinar', 'yes' => 'Sim', 'no' => 'Não'];
|
||||
|
||||
return [
|
||||
['key' => 'investment_preparation', 'label' => 'Pretende preparar o pedido de garantias/incentivos ao investimento privado?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 2'],
|
||||
['key' => 'investment_activity_scope', 'label' => 'Natureza efectiva da actividade do projecto', 'options' => ['unknown' => 'Por determinar', 'general' => 'Outra actividade económica', 'extractive' => 'Prospecção, pesquisa ou produção extractiva', 'processing' => 'Processamento industrial mineiro/petrolífero', 'public_nonprofit' => 'Investimento público ou exclusivamente social/sem fins lucrativos'], 'source' => 'Lei 8/2023, artigos 2 e 22'],
|
||||
['key' => 'investment_form', 'label' => 'Tipo deste investimento', 'options' => ['unknown' => 'Por determinar', 'direct' => 'Directo', 'indirect' => 'Indirecto'], 'source' => 'Lei 8/2023, artigo 14(b); Regulamento, artigo 13', 'help' => 'O limiar monetário do artigo 13 aplica-se ao investimento directo. A forma indirecta não é convertida silenciosamente em directa.'],
|
||||
['key' => 'implementation_mode', 'label' => 'Modalidade da empresa implementadora', 'options' => ['unknown' => 'Por determinar', 'domestic_entity' => 'Empresa constituída/em constituição em Moçambique', 'foreign_representation' => 'Representação comercial estrangeira'], 'source' => 'Regulamento 8/2024, artigo 9(2)'],
|
||||
['key' => 'capital_origin', 'label' => 'Origem dos capitais/recursos deste investimento', 'options' => ['unknown' => 'Por determinar', 'national' => 'Nacionais', 'external' => 'Provenientes do exterior', 'mixed' => 'Mistos'], 'source' => 'Lei 8/2023, glossário; Regulamento, artigos 4–6'],
|
||||
['key' => 'site_tenure', 'label' => 'Fundamento declarado da utilização das instalações', 'options' => ['unknown' => 'Por determinar', 'lease' => 'Arrendamento', 'duat' => 'DUAT'], 'source' => 'Regulamento, artigos 9(1)(d) e 14(1)(d)'],
|
||||
['key' => 'project_concession', 'label' => 'O empreendimento é PPP ou concessão empresarial?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(ii)'],
|
||||
['key' => 'significant_implications', 'label' => 'Existem previsíveis implicações económicas, ambientais, de segurança ou saúde pública que exijam autorização?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(i)', 'help' => 'Declare o enquadramento conhecido; o nome do sector não responde a esta pergunta. Sem fundamento suficiente, mantenha por determinar.'],
|
||||
['key' => 'land_extension', 'label' => 'O projecto requer extensão de terra?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(iii)'],
|
||||
['key' => 'forest_concession', 'label' => 'O projecto requer concessão florestal?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(iv)'],
|
||||
['key' => 'forest_concession_hectares', 'label' => 'Área da concessão florestal (hectares)', 'type' => 'number', 'when' => ['field' => 'forest_concession', 'equals' => 'yes'], 'source' => 'Lei 8/2023, artigo 22(1)(b)(iv)'],
|
||||
['key' => 'total_investment', 'label' => 'Valor total do investimento na moeda do projecto', 'type' => 'number', 'source' => 'Regulamento, artigo 13(2)'],
|
||||
['key' => 'area_required', 'label' => 'Área de terra requerida', 'type' => 'number', 'when' => ['field' => 'land_extension', 'equals' => 'yes'], 'source' => 'Lei 8/2023, artigo 22(1)(b)(iii)'],
|
||||
['key' => 'area_unit', 'label' => 'Unidade da área requerida', 'options' => ['ha' => 'Hectares', 'm2' => 'Metros quadrados'], 'when' => ['field' => 'land_extension', 'equals' => 'yes'], 'source' => 'Conversão de unidade exacta; não é benefício ou condição adicional'],
|
||||
];
|
||||
}
|
||||
|
||||
private function number(array $f, string $key): ?BigDecimal
|
||||
{
|
||||
$value = $f[$key] ?? '';
|
||||
|
||||
return is_scalar($value) && preg_match('/^\d+(?:\.\d+)?$/D', (string) $value) ? BigDecimal::of((string) $value) : null;
|
||||
}
|
||||
|
||||
public function assess(array $context, array $documents = []): array
|
||||
{
|
||||
$f = $context['fields'] ?? [];
|
||||
$missing = [];
|
||||
$answer = function (string $key) use ($f, &$missing): ?bool {
|
||||
if (! in_array($f[$key] ?? '', ['yes', 'no'], true)) {
|
||||
$missing[] = $key;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $f[$key] === 'yes';
|
||||
};
|
||||
$enabled = ($f['investment_preparation'] ?? '') === 'yes';
|
||||
$date = $f['expected_start_date'] ?? '';
|
||||
$period = preg_match('/^2026-\d{2}-\d{2}$/D', $date) === 1;
|
||||
$scope = $f['investment_activity_scope'] ?? 'unknown';
|
||||
$covered = in_array($scope, ['general', 'processing'], true);
|
||||
$mode = 'indeterminate';
|
||||
$reasons = [];
|
||||
if ($enabled) {
|
||||
if (! $date) {
|
||||
$missing[] = 'expected_start_date';
|
||||
}
|
||||
if ($scope === 'unknown' || $scope === '') {
|
||||
$missing[] = 'investment_activity_scope';
|
||||
}
|
||||
if (empty($f['currency'])) {
|
||||
$missing[] = 'currency';
|
||||
}
|
||||
if ($covered && $period) {
|
||||
$triggers = [];
|
||||
if ($scope === 'processing') {
|
||||
$triggers['processing'] = true;
|
||||
$reasons[] = 'Processamento industrial mineiro/petrolífero: Lei 8/2023, artigo 22(1)(b)(v).';
|
||||
}
|
||||
$amount = $this->number($f, 'total_investment');
|
||||
$triggers['amount'] = $amount && ($f['currency'] ?? '') === 'MZN' && ($f['investment_form'] ?? '') === 'direct' ? $amount->isGreaterThan('32000000000') : null;
|
||||
if (! in_array($f['investment_form'] ?? '', ['direct', 'indirect'], true)) {
|
||||
$missing[] = 'investment_form';
|
||||
}
|
||||
if (($f['investment_form'] ?? '') === 'indirect') {
|
||||
$reasons[] = 'Investimento indirecto: a classificação pela dimensão e o encaminhamento monetário não são determinados por este lote. As restantes condições documentadas continuam avaliadas.';
|
||||
}
|
||||
if (! $amount) {
|
||||
$missing[] = 'total_investment';
|
||||
}
|
||||
if (($f['currency'] ?? '') !== 'MZN') {
|
||||
$reasons[] = 'A condição monetária não é determinada noutra moeda: não se fez conversão.';
|
||||
}
|
||||
foreach (['project_concession', 'significant_implications'] as $key) {
|
||||
$triggers[$key] = $answer($key);
|
||||
}
|
||||
$land = $answer('land_extension');
|
||||
if ($land === true) {
|
||||
$area = $this->number($f, 'area_required');
|
||||
$unit = $f['area_unit'] ?? '';
|
||||
if (! $area) {
|
||||
$missing[] = 'area_required';
|
||||
}if (! in_array($unit, ['ha', 'm2'], true)) {
|
||||
$missing[] = 'area_unit';
|
||||
}
|
||||
$triggers['land_extension'] = $area && in_array($unit, ['ha', 'm2'], true) ? $area->isGreaterThanOrEqualTo($unit === 'ha' ? '10000' : '100000000') : null;
|
||||
} else {
|
||||
$triggers['land_extension'] = $land;
|
||||
}
|
||||
$forest = $answer('forest_concession');
|
||||
if ($forest === true) {
|
||||
$area = $this->number($f, 'forest_concession_hectares');
|
||||
if (! $area) {
|
||||
$missing[] = 'forest_concession_hectares';
|
||||
}$triggers['forest_concession'] = $area ? $area->isGreaterThan('100000') : null;
|
||||
} else {
|
||||
$triggers['forest_concession'] = $forest;
|
||||
}
|
||||
foreach (['amount' => 'Valor superior a 32 mil milhões MZN — R13(2), R21(2)(a)', 'project_concession' => 'PPP/concessão empresarial — Lei 8/2023, art. 22(1)(b)(ii), R21(2)(b)', 'significant_implications' => 'Implicações significativas declaradas — Lei 8/2023, art. 22(1)(b)(i), R21(1)', 'land_extension' => 'Extensão de terra igual/superior a 10 mil hectares — Lei 8/2023, art. 22(1)(b)(iii), R21(2)(c)', 'forest_concession' => 'Concessão florestal superior a 100 mil hectares — Lei 8/2023, art. 22(1)(b)(iv), R21(2)(d)'] as $condition => $description) {
|
||||
if (($triggers[$condition] ?? null) === true) {
|
||||
$reasons[] = $description.'.';
|
||||
}
|
||||
}
|
||||
$mode = in_array(true, $triggers, true) ? 'authorization' : (in_array(null, $triggers, true) ? 'indeterminate' : 'registration');
|
||||
$reasons[] = $mode === 'authorization' ? 'Foi identificada uma condição de autorização; as restantes condições desconhecidas não foram presumidas negativas.' : ($mode === 'registration' ? 'As condições de autorização deste lote foram declaradas negativas e o montante não ultrapassa o limite.' : 'Falta resolver pelo menos uma condição de autorização.');
|
||||
}
|
||||
foreach (['implementation_mode', 'capital_origin', 'site_tenure', 'entity_stage'] as $key) {
|
||||
if (empty($f[$key]) || $f[$key] === 'unknown') {
|
||||
$missing[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($mode === 'authorization' && in_array(true, array_intersect_key($triggers, array_flip(['amount', 'project_concession', 'land_extension', 'forest_concession'])), true)) {
|
||||
$missing = array_values(array_diff($missing, ['investment_form', 'project_concession', 'significant_implications', 'land_extension', 'forest_concession', 'forest_concession_hectares', 'area_required', 'area_unit', 'total_investment']));
|
||||
}
|
||||
$base = $enabled && $covered && $period ? true : null;
|
||||
if (! $enabled || in_array($scope, ['extractive', 'public_nonprofit'], true) || ($date && ! $period)) {
|
||||
$base = false;
|
||||
}
|
||||
$rules = [
|
||||
['identity', 'Identificação do investidor', $base, 'S06', 'R9(1)(a) / R14(1)(a)', 41, 'Identificação do proponente do pedido.'],
|
||||
['registration', 'Certidão de registo comercial', $base === true ? (($f['entity_stage'] ?? '') === 'existing' ? true : (($f['entity_stage'] ?? '') === 'being_formed' ? false : null)) : $base, 'S05', 'R9(1)(b) / R14(1)(b)', 41, 'Alternativa para empresa já registada.'],
|
||||
['name_reservation', 'Reserva da denominação social', $base === true ? (($f['entity_stage'] ?? '') === 'being_formed' ? true : (($f['entity_stage'] ?? '') === 'existing' ? false : null)) : $base, 'S05', 'R9(1)(b) / R14(1)(b)', 41, 'Alternativa para empresa em constituição.'],
|
||||
['site_plan', 'Planta ou esboço do local', $base, 'S06', 'R9(1)(c) / R14(1)(c)', 42, 'Local de implantação identificado; não certifica disponibilidade.'],
|
||||
];
|
||||
foreach (['lease' => 'Contrato de arrendamento', 'duat' => 'Título DUAT'] as $key => $label) {
|
||||
$rules[] = [$key, $label, $base === true ? (($f['site_tenure'] ?? 'unknown') === 'unknown' ? null : ($f['site_tenure'] ?? '') === $key) : $base, 'S06', 'R9(1)(d) / R14(1)(d)', 42, 'Alternativa correspondente à utilização declarada das instalações.'];
|
||||
}
|
||||
$rules[] = ['representation_license', 'Licença de representação comercial', $base === true ? (($f['implementation_mode'] ?? 'unknown') === 'unknown' ? null : ($f['implementation_mode'] ?? '') === 'foreign_representation') : $base, 'S06', 'R9(2)', 42, 'Só para a modalidade representação comercial estrangeira; não decorre da nacionalidade.'];
|
||||
foreach (['feasibility' => 'Estudo de viabilidade técnica e económico-financeira', 'financial_capacity' => 'Comprovação da capacidade financeira'] as $key => $label) {
|
||||
$rules[] = [$key, $label, $base === true ? ($mode === 'indeterminate' ? null : $mode === 'authorization') : $base, 'S02', 'R14(1)–(2)', 45, 'Integra a preparação do pedido de autorização.'];
|
||||
}
|
||||
$rules[] = ['foreign_capital', 'Prova de realização/registo do aporte externo', $base === true ? (($f['capital_origin'] ?? 'unknown') === 'unknown' ? null : in_array($f['capital_origin'] ?? '', ['external', 'mixed'], true)) : $base, 'S06', 'R5(1)–(5)', 39, 'Preparação específica do aporte externo; formalidades cambiais concretas não calculadas.'];
|
||||
$links = $context['preparation_documents'] ?? [];
|
||||
$requirements = [];
|
||||
foreach ($rules as [$key,$label,$applies,$stage,$article,$page,$reason]) {
|
||||
$doc = isset($links[$key]) ? collect($documents)->firstWhere('document_id', $links[$key]) : null;
|
||||
$requirements[] = ['key' => $key, 'label' => $label, 'state' => $applies === false ? 'not_applicable' : ($applies === null ? 'indeterminate' : ($doc ? 'associated' : 'missing')), 'required' => $applies === true, 'document_id' => $links[$key] ?? null, 'document' => $doc, 'stage' => $stage, 'reason' => $reason, 'source' => ['reference' => $article, 'url' => self::SOURCE.'#page='.$page, 'sha256' => self::HASH, 'period' => '2026; Regulamento desde 07/03/2024'], 'legal_verification' => 'not_inferred'];
|
||||
}
|
||||
$authoritySource = ['reference' => 'R13(1)', 'url' => self::SOURCE.'#page=45'];
|
||||
$zone = $context['selection']['park'] ?? null;
|
||||
$amount = $this->number($f, 'total_investment');
|
||||
$authority = 'Por determinar neste lote';
|
||||
if ($base === true && $mode === 'registration' && $amount && ($f['currency'] ?? '') === 'MZN') {
|
||||
$special = in_array($zone['fields']['investment_regime'] ?? '', ['zee', 'zfi'], true);
|
||||
$authority = $special ? ($amount->isLessThanOrEqualTo('6500000000') ? 'APIEX — condicionado ao enquadramento ZEE/ZFI efectivamente reconhecido' : 'Ministro que superintende Finanças') : ($amount->isLessThanOrEqualTo('3500000000') ? 'Governador da Província' : ($amount->isLessThanOrEqualTo('6500000000') ? 'APIEX' : 'Ministro que superintende Finanças'));
|
||||
}
|
||||
if ($base === true && $mode === 'authorization') {
|
||||
$council = array_intersect_key($triggers, array_flip(['amount', 'project_concession', 'land_extension', 'forest_concession']));
|
||||
$authority = in_array(true, $council, true) ? 'Conselho de Ministros' : (in_array(null, $council, true) ? 'Autorização identificada; completar as condições em falta para distinguir a entidade decisória' : 'Ministro que superintende Finanças; eventual submissão ao Conselho de Ministros nos termos do R21(3)');
|
||||
$authoritySource = ['reference' => 'R21(1)–(3)', 'url' => self::SOURCE.'#page=50'];
|
||||
}
|
||||
|
||||
$labels = array_column($this->questions(), 'label', 'key') + ['currency' => 'Moeda do projecto', 'expected_start_date' => 'Data prevista de início', 'entity_stage' => 'Situação da entidade'];
|
||||
$missingLabels = array_map(fn ($key) => $labels[$key] ?? $key, array_values(array_unique($missing)));
|
||||
|
||||
return ['version' => self::VERSION, 'enabled' => $enabled, 'period_covered' => $period, 'scope_covered' => $covered, 'mode' => $mode, 'reason' => $reasons, 'missing_fields' => array_values(array_unique($missing)), 'missing_labels' => $missingLabels, 'questions' => $this->questions(), 'requirements' => $requirements, 'authority' => $authority, 'authority_source' => $authoritySource, 'next' => $base === false ? 'Rever o âmbito/período; este lote não cobre o pedido' : ($mode === 'indeterminate' ? 'Completar condições do enquadramento' : ($mode === 'authorization' ? 'Preparar estudo e documentos do pedido de autorização' : 'Preparar documentos do pedido de mero registo')), 'calculation_notice' => 'O regime procedimental não altera a taxa de IVA. Reutilize a simulação geral apenas no seu âmbito e período; benefícios, apuramento final e conversão cambial não são presumidos.', 'source' => ['url' => self::SOURCE, 'sha256' => self::HASH], 'notice' => 'Preparação informativa baseada em declarações; documentos associados não constituem validação jurídica, certificado ou submissão à Administração.'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\User;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/** Publication of notices; never receives bids or changes the linked investment. */
|
||||
final class Procurement
|
||||
{
|
||||
public const TEXT = ['title', 'reference', 'responsible_unit', 'modality', 'object', 'object_kind', 'quantities', 'financier', 'instrument', 'edition', 'rule_source', 'financing_agreement', 'eligibility', 'requirements', 'proposal_validity', 'guarantee_status', 'guarantees', 'delivery_channel', 'delivery_address', 'delivery_url', 'public_contact', 'dissemination', 'origin_url', 'origin_published_at', 'clarifications_at', 'deadline', 'opening_at', 'timezone', 'minimum_source', 'minimum_edition', 'minimum_modality', 'minimum_day_basis', 'result', 'internal_notes', 'public_reason', 'minimum_working_days', 'minimum_holidays', 'minimum_calendar_source'];
|
||||
|
||||
public function available(): bool
|
||||
{
|
||||
return config('procurement.enabled') && Schema::hasTable('portal_tenders');
|
||||
}
|
||||
|
||||
public function access(?User $actor): bool
|
||||
{
|
||||
if (! $actor || ! $actor->hasVerifiedEmail()) {
|
||||
return false;
|
||||
}
|
||||
$docs = app(DocumentManagementAccess::class);
|
||||
|
||||
return ! app(PortalProfileAccess::class)->has($actor, 'auditor') && ($docs->full($actor) || $docs->editorial($actor));
|
||||
}
|
||||
|
||||
public function guard(?User $actor): void
|
||||
{
|
||||
abort_unless($this->available(), 404);
|
||||
abort_unless($this->access($actor), 403, 'Use a autorização editorial já atribuída.');
|
||||
}
|
||||
|
||||
public function references(User $actor): array
|
||||
{
|
||||
$this->guard($actor);
|
||||
$editable = app(DocumentManagementAccess::class)->documents($actor)->select('id');
|
||||
|
||||
return ['regimes' => config('procurement.regimes'), 'acts' => config('procurement.acts'),
|
||||
'organizations' => DB::table('organizations')->whereNull('deleted_at')->orderBy('name')->get(['id', 'name']),
|
||||
'sectors' => DB::table('sectors')->orderBy('name')->get(['id', 'name']),
|
||||
'territories' => app(InvestmentGeography::class)->references(true)->orderBy('name')->get(['id', 'name', 'location_type']),
|
||||
'opportunities' => app(PortalProfileAccess::class)->scope(Opportunity::query(), $actor)->orderBy('title')->get(['id', 'title']),
|
||||
'projects' => DB::table('projects')->whereNull('deleted_at')->whereIn('data_classification', ['internal', 'public'])->orderBy('name')->get(['id', 'name']),
|
||||
'documents' => DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->whereNull('d.deleted_at')
|
||||
->where(fn ($q) => $q->whereIn('d.id', $editable)->orWhere(fn ($p) => $p->where('r.published', true)->where('d.data_classification', 'public')))
|
||||
->orderBy('r.title')->get(['r.id', 'r.title', 'r.published', 'd.malware_scan_status'])];
|
||||
}
|
||||
|
||||
public function record(string $id): object
|
||||
{
|
||||
abort_unless($this->available(), 404);
|
||||
|
||||
return DB::table('portal_tenders')->where('id', $id)->firstOrFail();
|
||||
}
|
||||
|
||||
public function save(User $actor, ?string $id, array $input): string
|
||||
{
|
||||
$this->guard($actor);
|
||||
$v = Validator::make($input, ['version' => 'required|integer|min:0', 'reason' => 'required|string|min:20|max:1000', 'draft' => 'required|array']);
|
||||
$v->validate();
|
||||
$draft = $this->normalize($actor, $input['draft']);
|
||||
|
||||
return DB::transaction(function () use ($actor, $id, $input, $draft) {
|
||||
$old = $id ? DB::table('portal_tenders')->where('id', $id)->lockForUpdate()->firstOrFail() : null;
|
||||
abort_unless((int) ($old->version ?? 0) === (int) $input['version'], 409, 'A versão mudou; recupere o concurso antes de guardar.');
|
||||
$id ??= (string) Str::uuid();
|
||||
$version = ($old->version ?? 0) + 1;
|
||||
if ($old) {
|
||||
DB::table('portal_tenders')->where('id', $id)->update(['draft' => json_encode($draft, JSON_THROW_ON_ERROR), 'version' => $version, 'updated_at' => now()]);
|
||||
} else {
|
||||
DB::table('portal_tenders')->insert(['id' => $id, 'draft' => json_encode($draft, JSON_THROW_ON_ERROR), 'version' => $version, 'created_by' => $actor->id, 'created_at' => now(), 'updated_at' => now()]);
|
||||
}
|
||||
$this->version($actor, $id, $version, 'draft', $draft, $input['reason'], false);
|
||||
|
||||
return $id;
|
||||
});
|
||||
}
|
||||
|
||||
private function normalize(User $actor, array $input): array
|
||||
{
|
||||
$rules = array_fill_keys(self::TEXT, 'nullable|string|max:12000');
|
||||
$rules = [...$rules, 'regime' => 'nullable|in:state,external,private,other', 'organization_id' => 'nullable|uuid|exists:organizations,id',
|
||||
'opportunity_id' => 'nullable|integer|exists:opportunities,id', 'project_id' => 'nullable|uuid|exists:projects,id', 'sector_id' => 'nullable|integer|exists:sectors,id',
|
||||
'territory_id' => 'nullable|uuid|exists:geo_locations,id', 'contact_authorized' => 'boolean', 'minimum_days' => 'nullable|integer|min:0|max:3650',
|
||||
'lots' => 'array|max:100', 'lots.*' => 'array:code,description,quantities', 'lots.*.code' => 'required|string|max:80|distinct', 'lots.*.description' => 'required|string|max:3000', 'lots.*.quantities' => 'nullable|string|max:1500',
|
||||
'attachments' => 'array|max:100', 'attachments.*' => 'array:resource_id,public,purpose', 'attachments.*.resource_id' => 'required|integer|exists:portal_resources,id', 'attachments.*.public' => 'required|boolean', 'attachments.*.purpose' => 'required|string|max:150'];
|
||||
foreach (['origin_url', 'delivery_url', 'rule_source'] as $url) {
|
||||
$rules[$url] = 'nullable|url:http,https|max:2000';
|
||||
}
|
||||
$data = Validator::make($input, $rules)->validate();
|
||||
if (! empty($data['opportunity_id'])) {
|
||||
abort_unless(app(PortalProfileAccess::class)->scope(Opportunity::query(), $actor)->whereKey($data['opportunity_id'])->exists(), 403);
|
||||
}
|
||||
if (! empty($data['project_id'])) {
|
||||
abort_unless(DB::table('projects')->where('id', $data['project_id'])->whereNull('deleted_at')->whereIn('data_classification', ['internal', 'public'])->exists(), 422);
|
||||
}
|
||||
foreach ($data['attachments'] ?? [] as $attachment) {
|
||||
$resource = DB::table('portal_resources')->where('id', $attachment['resource_id'])->firstOrFail();
|
||||
abort_unless($this->publicDocuments()->where('r.id', $resource->id)->exists() || app(DocumentManagementAccess::class)->documents($actor)->where('id', $resource->document_id)->exists(), 403);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function preview(User $actor, string $id, string $act): array
|
||||
{
|
||||
$this->guard($actor);
|
||||
$record = $this->record($id);
|
||||
abort_unless(array_key_exists($act, config('procurement.acts')), 422);
|
||||
$p = json_decode($record->draft, true, 512, JSON_THROW_ON_ERROR);
|
||||
Validator::make($p, array_fill_keys(['title', 'reference', 'organization_id', 'modality', 'object', 'regime', 'instrument', 'edition', 'rule_source', 'origin_published_at', 'deadline', 'delivery_channel', 'timezone'], 'required'))->validate();
|
||||
Validator::make($p, ['timezone' => 'timezone', 'object_kind' => 'nullable|in:works,goods,services,mixed,other', 'guarantee_status' => 'nullable|in:required,not_required,unknown'])->validate();
|
||||
Validator::make($p, ['public_reason' => 'required|string|min:20|max:1000'])->validate();
|
||||
$dates = [];
|
||||
foreach (['origin_published_at', 'clarifications_at', 'deadline', 'opening_at'] as $field) {
|
||||
if (empty($p[$field])) {
|
||||
continue;
|
||||
}
|
||||
$validator = Validator::make([$field => $p[$field]], [$field => 'date_format:Y-m-d\TH:i']);
|
||||
$validator->validate();
|
||||
$dates[$field] = CarbonImmutable::createFromFormat('!Y-m-d\TH:i', $p[$field], $p['timezone']);
|
||||
}
|
||||
$this->require($dates['deadline']->greaterThan($dates['origin_published_at']), 'deadline', 'Entrega anterior à publicação de origem.');
|
||||
if (isset($dates['clarifications_at'])) {
|
||||
$this->require($dates['clarifications_at']->betweenIncluded($dates['origin_published_at'], $dates['deadline']), 'clarifications_at', 'Esclarecimentos fora do período de publicação/entrega.');
|
||||
}
|
||||
if (isset($dates['opening_at'])) {
|
||||
$this->require($dates['opening_at']->greaterThanOrEqualTo($dates['deadline']), 'opening_at', 'A abertura indicada é anterior à entrega.');
|
||||
}
|
||||
if (($p['guarantee_status'] ?? '') === 'required') {
|
||||
$this->require(! empty($p['guarantees']), 'guarantees', 'Descreva a garantia documentada exigida.');
|
||||
}
|
||||
if (isset($p['minimum_days'])) {
|
||||
foreach (['minimum_source', 'minimum_edition', 'minimum_modality', 'minimum_day_basis'] as $field) {
|
||||
$this->require(! empty($p[$field]), $field, 'Identifique fonte, edição, modalidade e contagem do prazo mínimo.');
|
||||
}
|
||||
$this->require($p['minimum_modality'] === $p['modality'], 'minimum_modality', 'A regra indicada pertence a outra modalidade.');
|
||||
$earliest = $dates['origin_published_at'];
|
||||
if ($p['minimum_day_basis'] === 'business') {
|
||||
$this->require(! empty($p['minimum_calendar_source']), 'minimum_calendar_source', 'Identifique a fonte do calendário de dias úteis e feriados.');
|
||||
$weekdays = array_filter(array_map('trim', explode(',', (string) ($p['minimum_working_days'] ?? ''))));
|
||||
$this->require(count($weekdays) > 0 && ! array_diff($weekdays, ['1', '2', '3', '4', '5', '6', '7']), 'minimum_working_days', 'Indique os dias de trabalho documentados (1=segunda, 7=domingo).');
|
||||
$holidays = array_values(array_filter(preg_split('/[\s,;]+/', (string) ($p['minimum_holidays'] ?? ''))));
|
||||
Validator::make(['dates' => $holidays], ['dates' => 'array|max:1000', 'dates.*' => 'date_format:Y-m-d'])->validate();
|
||||
for ($left = (int) $p['minimum_days'],$iterations = 0; $left > 0; $iterations++) {
|
||||
$this->require($iterations < 36500, 'minimum_days', 'Calendário insuficiente para calcular o prazo indicado.');
|
||||
$earliest = $earliest->addDay();
|
||||
if (in_array((string) $earliest->isoWeekday(), $weekdays, true) && ! in_array($earliest->format('Y-m-d'), $holidays, true)) {
|
||||
$left--;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$this->require($p['minimum_day_basis'] === 'calendar', 'minimum_day_basis', 'Identifique dias corridos ou calendário de dias úteis documentado.');
|
||||
$earliest = $earliest->addDays((int) $p['minimum_days']);
|
||||
}
|
||||
$this->require($dates['deadline']->greaterThanOrEqualTo($earliest), 'deadline', 'Prazo inferior ao mínimo documentado para esta modalidade/edição/calendário.');
|
||||
}
|
||||
$previous = $record->published_version_id ? $this->publishedVersion($record->published_version_id) : null;
|
||||
$this->require($previous ? $act !== 'initial' : $act === 'initial', 'act', $previous ? 'O anúncio inicial já existe; publique um acto associado.' : 'Publique primeiro o anúncio inicial.');
|
||||
if ($previous) {
|
||||
$before = $previous['payload'];
|
||||
if (($before['deadline'] ?? '') !== $p['deadline'] || ($before['timezone'] ?? '') !== $p['timezone']) {
|
||||
$this->require($act === 'extension', 'act', 'Uma mudança do prazo exige acto de prorrogação, com data anterior e nova.');
|
||||
}
|
||||
if ($act === 'extension') {
|
||||
$this->require($dates['deadline']->greaterThan(CarbonImmutable::parse($before['deadline'], $before['timezone'])), 'deadline', 'A prorrogação deve avançar o prazo anterior.');
|
||||
$p['deadline_previous'] = $before['deadline'];
|
||||
$p['timezone_previous'] = $before['timezone'];
|
||||
}
|
||||
$p['extended'] = ($before['extended'] ?? false) || $act === 'extension';
|
||||
$p['procedural_state'] = $before['procedural_state'] ?? null;
|
||||
}
|
||||
if ($act === 'award') {
|
||||
$p['procedural_state'] = 'results';
|
||||
}
|
||||
if ($act === 'cancellation') {
|
||||
$p['procedural_state'] = 'cancelled';
|
||||
}
|
||||
if (in_array($act, ['award', 'cancellation', 'other'], true)) {
|
||||
$this->require(! empty($p['result']), 'result', 'Descreva o acto ou resultado documentado.');
|
||||
}
|
||||
$p['organization_name'] = DB::table('organizations')->where('id', $p['organization_id'])->whereNull('deleted_at')->value('name');
|
||||
$this->require((bool) $p['organization_name'], 'organization_id', 'Entidade eliminada; recupere a referência.');
|
||||
$p['sector_name'] = empty($p['sector_id']) ? null : DB::table('sectors')->where('id', $p['sector_id'])->value('name');
|
||||
$p['territory_name'] = empty($p['territory_id']) ? null : app(InvestmentGeography::class)->references(true)->where('id', $p['territory_id'])->value('name');
|
||||
if (! empty($p['territory_id'])) {
|
||||
$this->require((bool) $p['territory_name'], 'territory_id', 'Referência territorial indisponível.');
|
||||
}
|
||||
unset($p['internal_notes']);
|
||||
if (! ($p['contact_authorized'] ?? false)) {
|
||||
unset($p['public_contact']);
|
||||
}
|
||||
$attachments = [];
|
||||
foreach ($p['attachments'] ?? [] as $item) {
|
||||
if (! $item['public']) {
|
||||
continue;
|
||||
}
|
||||
$doc = $this->publicDocuments()->where('r.id', $item['resource_id'])->first(['r.id', 'r.title', 'd.sha256', 'd.id as document_id']);
|
||||
$this->require((bool) $doc, 'attachments', 'Anexo público exige publicação autorizada, scan limpo e direitos sem restrição na biblioteca.');
|
||||
$attachments[] = [...$item, 'title' => $doc->title, 'sha256' => trim($doc->sha256), 'document_id' => $doc->document_id];
|
||||
}
|
||||
$p['attachments'] = $attachments;
|
||||
if (! empty($p['opportunity_id'])) {
|
||||
$o = Opportunity::published()->whereKey($p['opportunity_id'])->first(['id', 'title', 'slug']);
|
||||
$p['opportunity'] = $o ? ['title' => $o->title, 'url' => '/oportunidades/'.$o->slug] : null;
|
||||
}
|
||||
if (! empty($p['project_id'])) {
|
||||
$p['project_name'] = DB::table('projects')->where('id', $p['project_id'])->whereNull('deleted_at')->where('data_classification', 'public')->value('name');
|
||||
}
|
||||
unset($p['opportunity_id'],$p['project_id']);
|
||||
$plan = ['id' => $id, 'version' => $record->version, 'act' => $act, 'payload' => $p];
|
||||
$plan['hash'] = hash_hmac('sha256', json_encode($plan, JSON_THROW_ON_ERROR), config('app.key'));
|
||||
|
||||
return $plan;
|
||||
}
|
||||
|
||||
public function publish(User $actor, string $id, array $input): void
|
||||
{
|
||||
$this->guard($actor);
|
||||
Validator::make($input, ['hash' => 'required|string|size:64', 'act' => 'required|string', 'reason' => 'required|string|min:20|max:1000'])->validate();
|
||||
DB::transaction(function () use ($actor, $id, $input) {
|
||||
$record = DB::table('portal_tenders')->where('id', $id)->lockForUpdate()->firstOrFail();
|
||||
$plan = $this->preview($actor, $id, $input['act']);
|
||||
abort_unless(hash_equals($plan['hash'], $input['hash']), 409, 'Prévia alterada; reveja o conteúdo antes de publicar.');
|
||||
abort_unless($input['reason'] === $plan['payload']['public_reason'], 409, 'O motivo público deve coincidir com a prévia.');
|
||||
$version = $record->version + 1;
|
||||
$vid = $this->version($actor, $id, $version, $input['act'], $plan['payload'], $input['reason'], true);
|
||||
DB::table('portal_tenders')->where('id', $id)->update(['published_version_id' => $vid, 'version' => $version, 'updated_at' => now()]);
|
||||
});
|
||||
}
|
||||
|
||||
private function version(User $actor, string $id, int $version, string $act, array $payload, string $reason, bool $published): string
|
||||
{
|
||||
$vid = (string) Str::uuid();
|
||||
DB::table('portal_tender_versions')->insert(['id' => $vid, 'tender_id' => $id, 'version' => $version, 'act' => $act, 'payload' => json_encode($payload, JSON_THROW_ON_ERROR), 'reason' => $reason, 'actor_id' => $actor->id, 'published_at' => $published ? now() : null, 'created_at' => now()]);
|
||||
DB::table('audit_events')->insert(['source_system' => 'procurement', 'entity_type' => 'portal_tenders', 'entity_id' => $id, 'actor_user_id' => $actor->id, 'action' => $published ? 'notice_published' : 'tender_draft_saved', 'data_classification' => 'internal', 'after_data' => json_encode(['version_id' => $vid, 'act' => $act, 'reason' => $reason], JSON_THROW_ON_ERROR)]);
|
||||
|
||||
return $vid;
|
||||
}
|
||||
|
||||
public function publicDocuments(): Builder
|
||||
{
|
||||
return DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->where('r.published', true)->where('r.rights_status', '!=', 'restricted')->where('d.rights_status', '!=', 'restricted')->where('d.data_classification', 'public')->where('d.status', 'active')->whereNull('d.deleted_at')->where('d.malware_scan_status', 'clean')->whereColumn('d.sha256', 'd.malware_scanned_sha256')
|
||||
->where(fn ($q) => $q->whereNull('d.source_system')->orWhereNotIn('d.source_system', ['opportunity-resources', 'opportunity-image-derivatives']))
|
||||
->whereNotExists(fn ($q) => $q->selectRaw('1')->from('opportunity_documents as od')->whereColumn('od.document_id', 'd.id')->whereRaw("NOT jsonb_exists(COALESCE(od.payload,'{}'::jsonb),'library_source')"))
|
||||
->whereNotExists(fn ($q) => $q->selectRaw('1')->from('opportunity_images as oi')->whereColumn('oi.document_id', 'd.id')->whereRaw("NOT jsonb_exists(COALESCE(oi.payload,'{}'::jsonb),'library_source')"));
|
||||
}
|
||||
|
||||
public function publishedVersion(string $id): array
|
||||
{
|
||||
$v = DB::table('portal_tender_versions')->where('id', $id)->whereNotNull('published_at')->firstOrFail();
|
||||
$p = json_decode($v->payload, true, 512, JSON_THROW_ON_ERROR);
|
||||
foreach ($p['attachments'] ?? [] as $i => $item) {
|
||||
$p['attachments'][$i]['available'] = $this->publicDocuments()->where('r.id', $item['resource_id'])->where('d.id', $item['document_id'])->where('d.sha256', $item['sha256'])->exists();
|
||||
}
|
||||
|
||||
return ['id' => $v->id, 'version' => $v->version, 'act' => $v->act, 'payload' => $p, 'published_at' => $v->published_at, 'reason' => $v->reason];
|
||||
}
|
||||
|
||||
public function state(array $version): string
|
||||
{
|
||||
$p = $version['payload'];
|
||||
if (in_array($p['procedural_state'] ?? '', ['cancelled', 'results'], true)) {
|
||||
return $p['procedural_state'];
|
||||
}
|
||||
if (CarbonImmutable::parse($p['deadline'], $p['timezone'])->isPast()) {
|
||||
return 'deadline_ended';
|
||||
}
|
||||
|
||||
return ($p['extended'] ?? false) ? 'extended' : 'open';
|
||||
}
|
||||
|
||||
private function require(bool $condition, string $field, string $message): void
|
||||
{
|
||||
if (! $condition) {
|
||||
throw ValidationException::withMessages([$field => $message]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ class TerritoryGeometryImport
|
||||
|
||||
private function guard(User $actor, bool $write = false): void
|
||||
{
|
||||
abort_unless($actor->can('manage-portal'), 403);
|
||||
abort_unless($actor->can('manage-portal') || app(ScopedPortalAdministration::class)->administrative($actor), 403);
|
||||
abort_unless(config('geometry-import.enabled'), 503, 'Importação cartográfica não disponibilizada.');
|
||||
if (! $write) {
|
||||
return;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
return [
|
||||
// Explicit opt-in after authenticated installation; never infer activation from schema/ready.
|
||||
'private_preparation' => env('PRIVATE_INVESTMENT_PREPARATION_ENABLED', false),
|
||||
'enabled' => env('INVESTOR_PATH_ENABLED', false),
|
||||
'definition_id' => '61000919-0000-4000-8000-000000000001',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
[
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62413-20170626",
|
||||
"sha256": "b471c1afd7235f359acdcc16a93088f77e1f988706c8d3c4d4f11c5d560aea47",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-73945-20170731",
|
||||
"sha256": "2aa0ce0eb305bfdf93e390a978d3694d0fbc0129034077b68cebce418f878415",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-76474-20170808",
|
||||
"sha256": "8f769aec4715449d056c8c25294060b2ff290ec0893c28e9bdcc4e8092fc75f5",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62414-20170626",
|
||||
"sha256": "a040c0b7cb95dd7f053f4d13662dd58b2b06453b6fa61a2d531d0257a8db8c17",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62418-20170626",
|
||||
"sha256": "e8ccafa68f0d1aa80f2e41d9e09f7b13e7c653a8472a731798cfd5e04ce2963e",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-75002-20170804",
|
||||
"sha256": "7b6fbd998be2b47b96e9a4724560c192c502709fa308d6590c0797ddcdb10544",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-73246-20170728",
|
||||
"sha256": "430ee8674c71104fc126bed2911d676f150ac623857a1ada75662987fd9ac390",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-77232-20170811",
|
||||
"sha256": "27544d3127803b7495bef9fd633147d0225d8c31816c4d59ed05a2edd59de070",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62416-20170626",
|
||||
"sha256": "ac575ce340571192552c89b63f0bd62a84dbc47828e6930133af5ff70e249319",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62929-20170627",
|
||||
"sha256": "488017b062c6760659d9147b7f12aea4afd8d824121adb9462e2c7a2ae58437b",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62649-20170626",
|
||||
"sha256": "38a858713472759a2d20a308bdd9d8afa9cfb76dcaf8d11ca709e89261573e42",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62739-20170626",
|
||||
"sha256": "5870fc0fad9bb7af537b7b1468151cde138b1b886ce846c7be25647b630245dd",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62854-20170627",
|
||||
"sha256": "44b1ba9d593bb625cf53de8909e78eb0b3129742c3ec9d58ec68c984e8930bce",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-63128-20170627",
|
||||
"sha256": "74be8f29e5053f53ab1a39a2f63208145c30f23357ca6d78b637dd02bf9c1fb4",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-63181-20170627",
|
||||
"sha256": "3d0f9ac14d36156d8178e33f8616d6406f31ea4cd69c8308cbf4227586d7ccb3",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-63146-20170627",
|
||||
"sha256": "542353466cc6d82b651619c495f138b0576a4be41d21aad194ae2005050cb1bc",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62688-20170626",
|
||||
"sha256": "a210a789b06d425822f4124f74226a2b27246c086c5ec5f041f1b2237d7c2b65",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-63248-20170628",
|
||||
"sha256": "6c30e61197b8568332af6d9b19bbba7ad8665b3873bb031a8c8aedd41536b060",
|
||||
"kind": "historico_noticioso",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173220-20260616",
|
||||
"sha256": "5b76c3d84dcf7f9a489686d90e3857ae6536705c918e841310bb42b354e80976",
|
||||
"kind": "setorial",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173222-20260616",
|
||||
"sha256": "e64d7063d1ac4dda028c97e1ba64938d2a9e426a0b630c57ee5df52a41312cc7",
|
||||
"kind": "setorial",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173482-20260915",
|
||||
"sha256": "6f888e133a779c719073246f89bcae150198e5897c0e7d9b7ba75f54c54a7830",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173484-20260915",
|
||||
"sha256": "297df455a9c6fd1f0ab7ca7bbe124ce483c86fe55be26baa695f03089b3ac049",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173478-20260915",
|
||||
"sha256": "d4e320ed99dd9d53d205e47932f73651976d43081d5d39f2bfc2996901408644",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173480-20260915",
|
||||
"sha256": "c3ee6401eda5b582c61969b5e3b74fdf26ef34ca481b1aca8c4c309d845147ae",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173476-20260915",
|
||||
"sha256": "72fbf28391db4804e0637b64e7f59e9fa597812a2681b4562dc3d6dfb4093a30",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173470-20260915",
|
||||
"sha256": "26feeae6b8d04972c7a64ee360583cccdcaeb4b0d8795fd7bc2fd3c38866cc19",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173474-20260915",
|
||||
"sha256": "9fa741a17b746bfa2f3ce7b6ce1d2bb34d3a35d3b58330b0554f9e0a6844f049",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173463-20260915",
|
||||
"sha256": "4f34b7bac4b3030c0614844d379f6e3cfad1b7f6ea3db575a58c33ae033b0e73",
|
||||
"kind": "trabalho_domestico",
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
}
|
||||
]
|
||||
+1
-1
@@ -23,7 +23,7 @@ return [
|
||||
'opportunity_other_qa_subtypes' => [],
|
||||
'operator_name' => env('PORTAL_OPERATOR_NAME'),
|
||||
// Tempo, 19/10/2026: institutional contact; commercial department remains separate.
|
||||
'institutional_contact_email' => env('PORTAL_INSTITUTIONAL_CONTACT_EMAIL', 'info@tempo.co.mz'),
|
||||
'institutional_contact_email' => env('PORTAL_INSTITUTIONAL_CONTACT_EMAIL', 'comercial@tempo.co.mz'),
|
||||
'support' => [
|
||||
'email' => env('PORTAL_SUPPORT_EMAIL'),
|
||||
'phone' => env('PORTAL_SUPPORT_PHONE'),
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'enabled' => (bool) env('PROCUREMENT_ENABLED', false),
|
||||
'regimes' => ['state' => 'Contratação pública do Estado', 'external' => 'Financiamento externo', 'private' => 'Concurso privado', 'other' => 'Outro regime documentado'],
|
||||
'acts' => ['initial' => 'Anúncio inicial', 'clarification' => 'Esclarecimento', 'correction' => 'Rectificação', 'extension' => 'Prorrogação', 'award' => 'Resultado / adjudicação', 'cancellation' => 'Cancelamento', 'other' => 'Outro acto'],
|
||||
];
|
||||
@@ -0,0 +1,50 @@
|
||||
<?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
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('portal_tenders', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary();
|
||||
$t->jsonb('draft');
|
||||
$t->unsignedInteger('version')->default(1);
|
||||
$t->uuid('published_version_id')->nullable();
|
||||
$t->foreignId('created_by')->constrained('users')->restrictOnDelete();
|
||||
$t->timestampsTz();
|
||||
});
|
||||
Schema::create('portal_tender_versions', function (Blueprint $t) {
|
||||
$t->uuid('id')->primary();
|
||||
$t->foreignUuid('tender_id')->constrained('portal_tenders')->restrictOnDelete();
|
||||
$t->unsignedInteger('version');
|
||||
$t->string('act', 24);
|
||||
$t->jsonb('payload');
|
||||
$t->text('reason');
|
||||
$t->foreignId('actor_id')->constrained('users')->restrictOnDelete();
|
||||
$t->timestampTz('published_at')->nullable();
|
||||
$t->timestampTz('created_at')->useCurrent();
|
||||
$t->unique(['tender_id', 'version']);
|
||||
});
|
||||
Schema::create('portal_tender_watches', function (Blueprint $t) {
|
||||
$t->foreignUuid('tender_id')->constrained('portal_tenders')->restrictOnDelete();
|
||||
$t->foreignId('user_id')->constrained('users')->restrictOnDelete();
|
||||
$t->uuid('seen_version_id')->nullable();
|
||||
$t->timestampsTz();
|
||||
$t->primary(['tender_id', 'user_id']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (DB::table('portal_tenders')->exists()) {
|
||||
throw new RuntimeException('Preserve procedures, acts and history; use a reviewed data reversal.');
|
||||
}
|
||||
Schema::dropIfExists('portal_tender_watches');
|
||||
Schema::dropIfExists('portal_tender_versions');
|
||||
Schema::dropIfExists('portal_tenders');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
FROM node:22-bookworm AS node
|
||||
|
||||
FROM php:8.4-cli-bookworm
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libpq-dev libzip-dev unzip git curl \
|
||||
&& docker-php-ext-install -j"$(nproc)" pdo_pgsql pgsql zip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=node /usr/local/bin/node /usr/local/bin/node
|
||||
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules
|
||||
RUN ln -s ../lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
|
||||
&& ln -s ../lib/node_modules/npm/bin/npx-cli.js /usr/local/bin/npx \
|
||||
&& ln -s ../lib/node_modules/corepack/dist/corepack.js /usr/local/bin/corepack
|
||||
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json ./
|
||||
RUN npm ci --no-audit --no-fund
|
||||
COPY . .
|
||||
@@ -0,0 +1,13 @@
|
||||
FROM php:8.4-cli-bookworm
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libpq-dev libzip-dev unzip git curl \
|
||||
&& docker-php-ext-install -j"$(nproc)" pdo_pgsql pgsql zip \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||
WORKDIR /var/www/html
|
||||
COPY composer.json composer.lock ./
|
||||
RUN composer install --no-interaction --prefer-dist --no-progress --no-scripts
|
||||
COPY . .
|
||||
RUN composer dump-autoload --optimize
|
||||
@@ -0,0 +1,176 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>InvestMoz — Levantamento funcional e modelos</title>
|
||||
<style>
|
||||
@page { size: A4; margin: 18mm 17mm 18mm 17mm; }
|
||||
body { font-family: Aptos, Calibri, Arial, sans-serif; color:#1d2939; font-size:10.5pt; line-height:1.42; }
|
||||
h1 { color:#123d68; font-size:26pt; margin:0 0 8pt; }
|
||||
h2 { color:#123d68; font-size:17pt; border-bottom:2px solid #13a789; padding-bottom:5pt; margin-top:24pt; }
|
||||
h3 { color:#145b82; font-size:13pt; margin:17pt 0 5pt; }
|
||||
p { margin:5pt 0 8pt; }
|
||||
.cover { padding-top:100pt; page-break-after:always; }
|
||||
.kicker { text-transform:uppercase; letter-spacing:2px; color:#138a77; font-weight:bold; }
|
||||
.subtitle { color:#52657a; font-size:15pt; }
|
||||
.meta { margin-top:35pt; padding:12pt; background:#eff6fb; border-left:4px solid #1d75bd; }
|
||||
.callout { background:#fff7e8; border-left:4px solid #ee9b20; padding:9pt 12pt; margin:12pt 0; }
|
||||
.note { color:#52657a; font-size:9pt; }
|
||||
table { border-collapse:collapse; width:100%; margin:8pt 0 14pt; font-size:9.2pt; }
|
||||
th { background:#123d68; color:white; text-align:left; padding:6pt; }
|
||||
td { border:1px solid #cbd5e1; padding:5pt 6pt; vertical-align:top; }
|
||||
tr:nth-child(even) td { background:#f5f8fb; }
|
||||
.diagram { text-align:center; margin:12pt 0; page-break-inside:avoid; }
|
||||
.diagram svg { width:100%; height:auto; }
|
||||
.legend { color:#52657a; font-size:9pt; }
|
||||
.small { font-size:9pt; }
|
||||
.pagebreak { page-break-before:always; }
|
||||
ul { margin-top:3pt; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<section class="cover">
|
||||
<p class="kicker">Documento de análise funcional</p>
|
||||
<h1>InvestMoz</h1>
|
||||
<p class="subtitle">Levantamento profissional do projecto, casos de uso reais, actores e diagramas</p>
|
||||
<div class="meta"><b>Data:</b> 9 de Outubro de 2026<br><b>Âmbito:</b> aplicação Laravel/React e documentação do repositório Investidor-V2<br><b>Versão:</b> 1.0 · análise do código e dos documentos disponíveis</div>
|
||||
<p style="margin-top:42pt;font-size:13pt">Portal de descoberta de oportunidades e serviços de investimento, ligado a fluxos autenticados de candidatura, tratamento institucional, evidência documental e acompanhamento.</p>
|
||||
<p class="note" style="margin-top:55pt">Documento técnico-funcional. A presença de uma rota, tabela, job ou descrição não demonstra, por si só, activação em produção.</p>
|
||||
</section>
|
||||
|
||||
<h2>1. Sumário executivo</h2>
|
||||
<p>O InvestMoz é uma plataforma digital que aproxima investidores, promotores e instituições. A experiência começa no portal público, onde se consultam sectores, oportunidades, negócios existentes, território, zonas e serviços. A partir daí, a pessoa pode manifestar interesse, iniciar uma simulação ou pedir apoio. Os percursos que exigem continuidade passam para a área autenticada, onde se mantêm perfil, rascunhos, candidaturas, anexos e comunicação.</p>
|
||||
<p>Do lado institucional, os pedidos podem ser revistos e acompanhados por CRM local, tarefas, revisões e eventos. A plataforma também contém domínios de catálogo territorial, zonas de investimento, documentação, governação fiscal, educação e competências, assistência de terra, parcerias, balcão virtual e IA baseada em fontes.</p>
|
||||
<div class="callout"><b>Leitura funcional:</b> trata-se de uma plataforma de encaminhamento e gestão de jornadas de investimento, e não apenas de um catálogo de anúncios. A camada pública informa e capta pedidos; a camada autenticada conserva o trabalho; a camada de gestão analisa, valida e decide segundo permissões e evidência.</div>
|
||||
<p>O README descreve a base inicial de sectores, oportunidades e manifestações de interesse. Rotas, controladores, migrations e documentação de domínio mostram uma solução mais abrangente. O levantamento de casos detalhados tem 24 casos de uso; este relatório acrescenta cenários concretos, diagramas UML/ER e a leitura da frequência dos eventos.</p>
|
||||
|
||||
<h2>2. O que o projecto faz</h2>
|
||||
<table><tr><th>Capacidade</th><th>Valor para o utilizador</th><th>Exemplos de dados</th></tr>
|
||||
<tr><td>Descoberta pública</td><td>Encontrar oportunidades, negócios, sectores, serviços e referências territoriais.</td><td>sectors, opportunities, existing_businesses, investment_zones</td></tr>
|
||||
<tr><td>Captação de interesse</td><td>Enviar manifestação associada a uma oportunidade ou negócio, com consentimento.</td><td>interest_requests, consents, receipts</td></tr>
|
||||
<tr><td>Jornada do investidor</td><td>Guardar itens, criar dossier/submissão, anexar documentos e acompanhar mensagens.</td><td>investment_submissions, revisions, messages, attachments</td></tr>
|
||||
<tr><td>Tratamento institucional</td><td>Qualificar pedidos, atribuir responsável, criar tarefas e acompanhar a negociação.</td><td>leads, contacts, crm_opportunities, tasks, activities</td></tr>
|
||||
<tr><td>Planeamento territorial</td><td>Relacionar projectos, clusters, corredores, localizações e zonas.</td><td>projects, economic_clusters, development_corridors, geo_locations</td></tr>
|
||||
<tr><td>Documentos e evidência</td><td>Preservar origem, versões, hash, scan, classificação e autorização de divulgação.</td><td>documents, document_versions, malware_scans, associations</td></tr>
|
||||
<tr><td>Simulação e regras fiscais</td><td>Guardar rascunhos, executar simulações com parâmetros/fontes rastreáveis e governar versões.</td><td>simulations, scenarios, results, tax_rule_versions, legal_sources</td></tr>
|
||||
<tr><td>Serviços complementares</td><td>Registar educação, assistência de terra, parcerias, balcão virtual e apoio com IA.</td><td>training needs, land cases, partnerships, desk cases, ai runs</td></tr></table>
|
||||
|
||||
<h2>3. Actores e fronteiras de acesso</h2>
|
||||
<table><tr><th>Actor</th><th>O que pretende</th><th>Zona típica</th></tr>
|
||||
<tr><td>Visitante</td><td>Consultar conteúdos públicos e iniciar interesse, apoio ou simulação.</td><td>Portal público</td></tr>
|
||||
<tr><td>Investidor / utilizador registado</td><td>Manter perfil, favoritos, rascunhos, submissões, anexos e mensagens.</td><td>Área autenticada</td></tr>
|
||||
<tr><td>Promotor / proprietário de negócio</td><td>Registar ou actualizar negócio e responder a pedidos de informação.</td><td>Área autenticada e workflow de revisão</td></tr>
|
||||
<tr><td>Gestor de portal / CRM</td><td>Rever interesses, qualificar leads, atribuir tarefas e gerir conteúdos.</td><td>Gestão interna</td></tr>
|
||||
<tr><td>Gestor territorial</td><td>Manter projectos, relações, zonas, geometria e referências territoriais.</td><td>Gestão interna</td></tr>
|
||||
<tr><td>Revisor documental / gestor fiscal</td><td>Validar metadados, evidência, versões e regras sob as respectivas funções.</td><td>Gestão restrita</td></tr>
|
||||
<tr><td>Agente de serviço / parceiro institucional</td><td>Tratar casos ou colaborar em parcerias e salas.</td><td>Serviços e colaboração</td></tr>
|
||||
<tr><td>Administrador / worker</td><td>Gerir segurança/configuração ou executar trabalho assíncrono autorizado.</td><td>Administração / sistema</td></tr></table>
|
||||
<p class="note">Os nomes acima são papéis funcionais consolidados. A autorização efectiva depende das policies, middleware, MFA, configuração e permissões em vigor.</p>
|
||||
|
||||
<h2>4. Diagrama de casos de uso com actores</h2>
|
||||
<p>O diagrama destaca actores (“bonecos”) e os principais serviços que usam. As associações mostram interacções típicas; não indicam que todos os serviços estejam activos na mesma instalação.</p>
|
||||
<div class="diagram">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1050 610" role="img" aria-label="Diagrama UML de casos de uso InvestMoz com actores">
|
||||
<defs><style>.actor{stroke:#173b60;stroke-width:3;fill:none}.line{stroke:#728397;stroke-width:1.7}.uc{fill:#eef6fc;stroke:#2778b8;stroke-width:2}.txt{font:16px Arial;fill:#17324d}.label{font:bold 15px Arial;fill:#17324d}.boundary{fill:#fbfdff;stroke:#9bb0c3;stroke-width:2;stroke-dasharray:8 5}</style></defs>
|
||||
<rect x="220" y="22" width="795" height="558" rx="14" class="boundary"/><text x="245" y="52" class="label">Sistema InvestMoz</text>
|
||||
<!-- stick actors -->
|
||||
<g class="actor"><circle cx="95" cy="115" r="17"/><path d="M95 132v65m-30-38h60m-30 38-27 42m27-42 27 42"/></g><text x="50" y="245" class="label">Visitante</text>
|
||||
<g class="actor"><circle cx="95" cy="335" r="17"/><path d="M95 352v65m-30-38h60m-30 38-27 42m27-42 27 42"/></g><text x="37" y="465" class="label">Investidor</text>
|
||||
<g class="actor"><circle cx="95" cy="510" r="17"/><path d="M95 527v45m-30-25h60m-30 25-22 33m22-33 22 33"/></g><text x="15" y="606" class="label">Gestor</text>
|
||||
<g class="actor"><circle cx="1025" cy="165" r="15"/><path d="M1025 180v55m-25-33h50m-25 33-22 36m22-36 22 36"/></g><text x="979" y="267" class="label">Revisor</text>
|
||||
<g class="actor"><circle cx="1025" cy="400" r="15"/><path d="M1025 415v55m-25-33h50m-25 33-22 36m22-36 22 36"/></g><text x="967" y="502" class="label">Parceiro</text>
|
||||
<!-- use cases -->
|
||||
<g><ellipse cx="410" cy="112" rx="145" ry="31" class="uc"/><text x="410" y="118" text-anchor="middle" class="txt">Consultar catálogo e mapa</text>
|
||||
<ellipse cx="730" cy="112" rx="145" ry="31" class="uc"/><text x="730" y="118" text-anchor="middle" class="txt">Pesquisar oportunidade/negócio</text>
|
||||
<ellipse cx="410" cy="205" rx="145" ry="31" class="uc"/><text x="410" y="211" text-anchor="middle" class="txt">Manifestar interesse</text>
|
||||
<ellipse cx="730" cy="205" rx="145" ry="31" class="uc"/><text x="730" y="211" text-anchor="middle" class="txt">Executar simulação</text>
|
||||
<ellipse cx="410" cy="300" rx="145" ry="31" class="uc"/><text x="410" y="306" text-anchor="middle" class="txt">Criar candidatura / dossier</text>
|
||||
<ellipse cx="730" cy="300" rx="145" ry="31" class="uc"/><text x="730" y="306" text-anchor="middle" class="txt">Anexar e acompanhar documentos</text>
|
||||
<ellipse cx="410" cy="397" rx="145" ry="31" class="uc"/><text x="410" y="403" text-anchor="middle" class="txt">Tratar interesse no CRM</text>
|
||||
<ellipse cx="730" cy="397" rx="145" ry="31" class="uc"/><text x="730" y="403" text-anchor="middle" class="txt">Validar e publicar conteúdo</text>
|
||||
<ellipse cx="410" cy="495" rx="145" ry="31" class="uc"/><text x="410" y="501" text-anchor="middle" class="txt">Gerir território e zonas</text>
|
||||
<ellipse cx="730" cy="495" rx="145" ry="31" class="uc"/><text x="730" y="501" text-anchor="middle" class="txt">Colaborar em parceria/serviço</text></g>
|
||||
<!-- actor associations -->
|
||||
<g class="line"><path d="M112 115H264M112 115 585 112M112 145 265 195M112 355 265 300M112 370 265 300M112 388 585 300M112 515 265 397M112 525 265 495M112 540 585 495M1008 175 875 397M1008 190 875 300M1008 410 875 495"/></g>
|
||||
</svg></div>
|
||||
<p class="legend">Exemplo do percurso mais comum: visitante pesquisa → manifesta interesse → investidor cria/acompanha candidatura → gestão trata o pedido → revisão e decisão.</p>
|
||||
|
||||
<h2>5. Casos de uso reais — jornadas ilustrativas</h2>
|
||||
<h3>UC-A — Investidor encontra oportunidade e manifesta interesse</h3>
|
||||
<ol><li>O visitante consulta sectores, filtra oportunidades por localização e abre o detalhe.</li><li>Escolhe “manifestar interesse”, informa contacto, modalidade e mensagem, e aceita o consentimento aplicável.</li><li>O sistema valida o alvo, regista o pedido e, quando configurado, um recibo/idempotência.</li><li>O investidor pode autenticar-se para guardar a oportunidade ou avançar para uma submissão.</li><li>Um gestor consulta o pedido, verifica consentimentos, atribui responsável, qualifica e cria tarefa de seguimento.</li><li>As alterações relevantes ficam no histórico; notificações e integrações só são consideradas entregues quando o disparo e o serviço estiverem confirmados.</li></ol>
|
||||
<p><b>Resultado:</b> pedido rastreável ligado à oportunidade e encaminhado para tratamento. <b>Entidades:</b> Opportunity, Investor/User, InterestRequest, Consent, Lead, Task, AuditEvent.</p>
|
||||
<h3>UC-B — Proprietário submete um negócio para divulgação</h3>
|
||||
<ol><li>O proprietário cria rascunho e informa modalidade, objectos, localização e documentos de suporte.</li><li>Submete o registo; o estado passa para revisão conforme o workflow aplicável.</li><li>O gestor analisa e pode aprovar, rejeitar ou pedir informação complementar.</li><li>O revisor documental confere original, hash, versão, scan e autorização de divulgação.</li><li>Só após as validações necessárias o conteúdo pode ser publicado ao público; publicação não altera a situação jurídica do documento.</li></ol>
|
||||
<p><b>Resultado:</b> negócio com histórico e decisão rastreável. <b>Estados documentados:</b> draft → submitted → in_review → approved → published, com ramos needs_information, rejected, withdrawn, suspended e archived.</p>
|
||||
<h3>UC-C — Gestor territorial valida zona e relações</h3>
|
||||
<ol><li>O gestor regista a zona e a referência/fonte pertinente.</li><li>Associa localização, operador, categoria, geometria e relações com projectos, clusters ou corredores.</li><li>As relações sem evidência verificada permanecem pendentes e identificadas.</li><li>Um revisor valida a evidência e as regras de publicação; alterações ficam versionadas.</li><li>A zona só aparece no portal conforme estado e autorização de divulgação.</li></ol>
|
||||
<h3>UC-D — Utilizador guarda um rascunho e executa simulação</h3>
|
||||
<ol><li>O utilizador preenche o formulário; os dados incompletos podem ser guardados como rascunho recuperável.</li><li>Quando solicita o cálculo, o sistema selecciona questionário e parâmetros elegíveis.</li><li>O job de simulação executa após a transacção e grava cenários, resultados, obrigações e fontes usadas.</li><li>O relatório pode ser consultado; o rascunho continua distinto do cálculo executado.</li></ol>
|
||||
<div class="callout"><b>Regra de integridade:</b> importar ou publicar fonte documental não activa automaticamente uma regra fiscal. Parâmetros ausentes/ambíguos permanecem indeterminados.</div>
|
||||
<h3>UC-E — Parceiros colaboram numa parceria ou serviço</h3>
|
||||
<p>Um actor cria uma proposta; outro manifesta interesse; após aceitação, os participantes podem colaborar numa sala, trocar mensagens/documentos e acompanhar marcos. O balcão virtual segue padrão semelhante para pedidos de serviço: registo, análise, pedido de documentos, eventos, revisão e resolução.</p>
|
||||
|
||||
<h2>6. Diagrama de classes (visão lógica)</h2>
|
||||
<p>O diagrama apresenta as classes de domínio principais, atributos representativos e operações. É uma vista funcional; não reproduz todos os campos nem métodos Laravel.</p>
|
||||
<div class="diagram"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1100 690" role="img" aria-label="Diagrama UML de classes InvestMoz">
|
||||
<defs><style>.box{fill:#fff;stroke:#276c9b;stroke-width:2}.head{fill:#e8f2f9}.ct{font:14px Arial;fill:#17324d}.ch{font:bold 15px Arial;fill:#123d68}.rel{stroke:#60768b;stroke-width:1.8;fill:none}.mult{font:12px Arial;fill:#455b70}</style></defs>
|
||||
<!-- classes -->
|
||||
<g><rect x="30" y="30" width="220" height="120" rx="5" class="box"/><rect x="30" y="30" width="220" height="32" class="head"/><text x="140" y="52" text-anchor="middle" class="ch">User</text><text x="44" y="82" class="ct">id, email, verified_at</text><text x="44" y="105" class="ct">2FA / passkeys</text><text x="44" y="132" class="ct">+ authenticate()</text>
|
||||
<rect x="315" y="30" width="220" height="120" rx="5" class="box"/><rect x="315" y="30" width="220" height="32" class="head"/><text x="425" y="52" text-anchor="middle" class="ch">Investor</text><text x="329" y="82" class="ct">id, user_id, profile</text><text x="329" y="105" class="ct">company interests</text><text x="329" y="132" class="ct">+ manageProfile()</text>
|
||||
<rect x="600" y="30" width="220" height="120" rx="5" class="box"/><rect x="600" y="30" width="220" height="32" class="head"/><text x="710" y="52" text-anchor="middle" class="ch">Opportunity</text><text x="614" y="82" class="ct">sector, slug, status</text><text x="614" y="105" class="ct">location, investment</text><text x="614" y="132" class="ct">+ publish()</text>
|
||||
<rect x="865" y="30" width="205" height="120" rx="5" class="box"/><rect x="865" y="30" width="205" height="32" class="head"/><text x="967" y="52" text-anchor="middle" class="ch">Sector</text><text x="879" y="82" class="ct">id, name, slug</text><text x="879" y="105" class="ct">active / published</text><text x="879" y="132" class="ct">+ listOpportunities()</text>
|
||||
<rect x="30" y="230" width="235" height="130" rx="5" class="box"/><rect x="30" y="230" width="235" height="32" class="head"/><text x="147" y="252" text-anchor="middle" class="ch">InterestRequest</text><text x="44" y="282" class="ct">target, contact, consent</text><text x="44" y="305" class="ct">status, submitted_at</text><text x="44" y="338" class="ct">+ submit() / qualify()</text>
|
||||
<rect x="325" y="230" width="235" height="130" rx="5" class="box"/><rect x="325" y="230" width="235" height="32" class="head"/><text x="442" y="252" text-anchor="middle" class="ch">Lead / Task</text><text x="339" y="282" class="ct">owner, next_action</text><text x="339" y="305" class="ct">due_at, status</text><text x="339" y="338" class="ct">+ assign() / complete()</text>
|
||||
<rect x="620" y="230" width="235" height="130" rx="5" class="box"/><rect x="620" y="230" width="235" height="32" class="head"/><text x="737" y="252" text-anchor="middle" class="ch">Submission</text><text x="634" y="282" class="ct">owner, target, state</text><text x="634" y="305" class="ct">revisions, needs</text><text x="634" y="338" class="ct">+ save() / submit()</text>
|
||||
<rect x="910" y="230" width="160" height="130" rx="5" class="box"/><rect x="910" y="230" width="160" height="32" class="head"/><text x="990" y="252" text-anchor="middle" class="ch">Document</text><text x="924" y="282" class="ct">hash, source</text><text x="924" y="305" class="ct">version, status</text><text x="924" y="338" class="ct">+ scan()</text>
|
||||
<rect x="30" y="450" width="235" height="130" rx="5" class="box"/><rect x="30" y="450" width="235" height="32" class="head"/><text x="147" y="472" text-anchor="middle" class="ch">Project / Zone</text><text x="44" y="502" class="ct">territory, geometry</text><text x="44" y="525" class="ct">relations, evidence</text><text x="44" y="558" class="ct">+ validate() / publish()</text>
|
||||
<rect x="325" y="450" width="235" height="130" rx="5" class="box"/><rect x="325" y="450" width="235" height="32" class="head"/><text x="442" y="472" text-anchor="middle" class="ch">Simulation</text><text x="339" y="502" class="ct">questionnaire, input</text><text x="339" y="525" class="ct">scenario, result, sources</text><text x="339" y="558" class="ct">+ execute() / report()</text>
|
||||
<rect x="620" y="450" width="235" height="130" rx="5" class="box"/><rect x="620" y="450" width="235" height="32" class="head"/><text x="737" y="472" text-anchor="middle" class="ch">TaxRuleVersion</text><text x="634" y="502" class="ct">source, dates, parameters</text><text x="634" y="525" class="ct">review / approval state</text><text x="634" y="558" class="ct">+ resolveForPeriod()</text>
|
||||
<rect x="910" y="450" width="160" height="130" rx="5" class="box"/><rect x="910" y="450" width="160" height="32" class="head"/><text x="990" y="472" text-anchor="middle" class="ch">AuditEvent</text><text x="924" y="502" class="ct">actor, entity</text><text x="924" y="525" class="ct">action, time</text><text x="924" y="558" class="ct">+ record()</text></g>
|
||||
<!-- relations -->
|
||||
<g class="rel"><path d="M250 90H315M535 90H600M820 90H865M140 150V230M425 150V230M710 150V230M265 295H325M560 295H620M855 295H910M140 360V450M442 360V450M737 360V450M855 515H910M250 130 900 470"/></g>
|
||||
<g class="mult"><text x="270" y="82">1 : 0..1</text><text x="547" y="82">0..*</text><text x="834" y="82">1</text><text x="148" y="205">1 : 0..*</text><text x="273" y="286">0..1</text><text x="567" y="286">1..*</text><text x="861" y="286">0..*</text><text x="145" y="435">1 : 0..*</text><text x="450" y="435">1 : 0..*</text></g>
|
||||
</svg></div>
|
||||
<p class="note">A relação exacta Investor/User, a associação Submission/target e os vínculos Document/entidade podem ser opcionais ou implementados por tabelas polimórficas/de junção; confirme no schema antes de gerar código ou migrações.</p>
|
||||
|
||||
<h2>7. Diagrama de relacionamento (ER simplificado)</h2>
|
||||
<div class="diagram"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1080 560" role="img" aria-label="Diagrama de relacionamento das entidades InvestMoz">
|
||||
<defs><style>.e{fill:#edf5fb;stroke:#2477b6;stroke-width:2}.et{font:bold 15px Arial;fill:#123d68}.r{stroke:#61788d;stroke-width:2;fill:none}.rt{font:12px Arial;fill:#40566b}</style></defs>
|
||||
<g><rect x="35" y="45" width="155" height="60" rx="9" class="e"/><text x="112" y="81" text-anchor="middle" class="et">SECTOR</text><rect x="260" y="45" width="180" height="60" rx="9" class="e"/><text x="350" y="81" text-anchor="middle" class="et">OPPORTUNITY</text><rect x="515" y="45" width="190" height="60" rx="9" class="e"/><text x="610" y="81" text-anchor="middle" class="et">INTEREST_REQUEST</text><rect x="805" y="45" width="160" height="60" rx="9" class="e"/><text x="885" y="81" text-anchor="middle" class="et">INVESTOR</text>
|
||||
<rect x="35" y="205" width="155" height="60" rx="9" class="e"/><text x="112" y="241" text-anchor="middle" class="et">USER</text><rect x="260" y="205" width="180" height="60" rx="9" class="e"/><text x="350" y="241" text-anchor="middle" class="et">SUBMISSION</text><rect x="515" y="205" width="190" height="60" rx="9" class="e"/><text x="610" y="241" text-anchor="middle" class="et">DOCUMENT</text><rect x="805" y="205" width="160" height="60" rx="9" class="e"/><text x="885" y="241" text-anchor="middle" class="et">AUDIT_EVENT</text>
|
||||
<rect x="35" y="365" width="155" height="60" rx="9" class="e"/><text x="112" y="401" text-anchor="middle" class="et">PROJECT</text><rect x="260" y="365" width="180" height="60" rx="9" class="e"/><text x="350" y="401" text-anchor="middle" class="et">CLUSTER</text><rect x="515" y="365" width="190" height="60" rx="9" class="e"/><text x="610" y="401" text-anchor="middle" class="et">INVESTMENT_ZONE</text><rect x="805" y="365" width="160" height="60" rx="9" class="e"/><text x="885" y="401" text-anchor="middle" class="et">LEGAL_SOURCE</text>
|
||||
<rect x="145" y="490" width="180" height="55" rx="9" class="e"/><text x="235" y="523" text-anchor="middle" class="et">SIMULATION</text><rect x="450" y="490" width="180" height="55" rx="9" class="e"/><text x="540" y="523" text-anchor="middle" class="et">TAX_RULE_VERSION</text><rect x="755" y="490" width="180" height="55" rx="9" class="e"/><text x="845" y="523" text-anchor="middle" class="et">SIMULATION_RESULT</text></g>
|
||||
<g class="r"><path d="M190 75H260M440 75H515M705 75H805M190 235H260M440 235H515M705 235H805M190 395H260M440 395H515M705 395H805M235 425V490M325 518H450M630 518H755M350 105V205M610 105V205M610 265V365M885 105V205M885 425V455H540V490"/></g>
|
||||
<g class="rt"><text x="210" y="67">1 : N</text><text x="465" y="67">1 : N</text><text x="730" y="67">N : 0..1</text><text x="210" y="227">1 : N</text><text x="462" y="227">N : M* via links</text><text x="730" y="227">1 : N</text><text x="210" y="387">N : M*</text><text x="462" y="387">N : M*</text><text x="730" y="387">1 : N</text><text x="235" y="466">1 : N</text><text x="346" y="510">1 : N</text><text x="650" y="510">1 : N</text><text x="354" y="160">1 : N</text><text x="617" y="160">association</text><text x="894" y="160">actor</text><text x="670" y="448">source</text></g>
|
||||
</svg></div>
|
||||
<p class="note">N:M = muitos-para-muitos, normalmente resolvido por tabela de associação. Ligações genéricas de documentos/auditoria podem não ser FKs directas. Este mapa é uma vista funcional, não um substituto do ERD integral em docs/investmoz/erd.mmd e do dicionário de dados.</p>
|
||||
|
||||
<h2>8. Eventos e frequência</h2>
|
||||
<table><tr><th>Evento/processo</th><th>Gatilho</th><th>Frequência confirmada</th><th>Leitura operacional</th></tr>
|
||||
<tr><td>Escalar tarefas vencidas</td><td>Scheduler Laravel</td><td>A cada 15 minutos; sem sobreposição configurada</td><td>Cadência declarada em routes/console.php. O scheduler do servidor tem de estar activo.</td></tr>
|
||||
<tr><td>Executar simulação (RunSimulation)</td><td>Pedido de cálculo aceite</td><td>Por pedido, via fila, afterCommit</td><td>Assíncrono; volume depende da utilização e dos workers.</td></tr>
|
||||
<tr><td>Scan de malware (ScanDocumentForMalware)</td><td>Upload ou pedido de análise em fluxos suportados</td><td>Por documento, via fila; job único</td><td>Depende de worker e scanner. Upload não prova análise concluída.</td></tr>
|
||||
<tr><td>Comparação de compatibilidade MNE</td><td>Alteração aplicável do negócio</td><td>Por alteração aplicável, em fila de sombra</td><td>Job identificado; frequência de alterações não medida.</td></tr>
|
||||
<tr><td>Notificação de interesse</td><td>Recepção de interesse</td><td>Disparo não confirmado</td><td>Job NotifyInterestReceived existe, mas não foi localizada chamada dispatch().</td></tr>
|
||||
<tr><td>Outbox/inbox e sincronização externa</td><td>Evento/mensagem externa</td><td>Sem calendário operacional confirmado</td><td>Estrutura preparada; documentação indica sincronização real pendente.</td></tr>
|
||||
<tr><td>Pesquisas, submissões, revisões e auditoria</td><td>Acção do visitante/utilizador/gestor</td><td>Sob procura ou acção; contagem indisponível</td><td>Sem logs/analytics não se calculam ocorrências por período.</td></tr></table>
|
||||
<p><b>Frequência configurada não é frequência observada.</b> Não foram disponibilizados logs, contagens de eventos da base activa, filas, analytics nem tráfego HTTP. Assim, eventos por hora/dia/mês, sazonalidade, falhas e utilizadores activos permanecem indeterminados. Não se extrapolam estes valores a partir de testes, seeders, screenshots ou schema.</p>
|
||||
|
||||
<h2>9. Inventário dos 24 casos de uso</h2>
|
||||
<table><tr><th>Grupo</th><th>Casos</th></tr>
|
||||
<tr><td>Portal e catálogo</td><td>UC-01 Consultar página inicial e serviços; UC-02 Pesquisar sectores e oportunidades; UC-03 Consultar negócio existente.</td></tr>
|
||||
<tr><td>Conta e jornada</td><td>UC-04 Criar conta/autenticar/recuperar acesso; UC-05 Manifestar interesse numa oportunidade; UC-06 Manifestar interesse num negócio; UC-07 Guardar oportunidade; UC-08 Manter submissão; UC-09 Submeter candidatura.</td></tr>
|
||||
<tr><td>Negócios e CRM</td><td>UC-10 Gerir negócio existente; UC-11 Tratar interesses no CRM.</td></tr>
|
||||
<tr><td>Território e documentos</td><td>UC-12 Gerir catálogo territorial; UC-13 Gerir zonas; UC-14 Carregar, verificar e publicar documento.</td></tr>
|
||||
<tr><td>Simulação/fiscalidade</td><td>UC-15 Criar rascunho; UC-16 Executar simulação; UC-17 Gerir regra fiscal.</td></tr>
|
||||
<tr><td>Serviços especializados</td><td>UC-18 Gerir educação e competências; UC-19 Assistência de terra; UC-20 Gerir parceria; UC-21 Usar balcão virtual.</td></tr>
|
||||
<tr><td>Transversal</td><td>UC-22 Usar IA com evidência; UC-23 Sincronizar sistemas externos; UC-24 Auditar alterações.</td></tr></table>
|
||||
<p>Os pré-requisitos, fluxos alternativos e tabelas associadas por UC encontram-se no ficheiro <i>casos-de-uso-aplicacao-actual.md</i>.</p>
|
||||
|
||||
<h2>10. Integridade, segurança e limitações</h2>
|
||||
<ul><li>Disponibilidade para consulta, situação jurídica/documental e uso em regra/cálculo são estados independentes.</li><li>Respeitar autorização de divulgação e âmbito; relações sem evidência ficam pendentes.</li><li>Preservar original, proveniência, ID exacto, edição/data, hash e histórico. Não confirmar autenticidade, vigência, direitos ou scan sem evidência.</li><li>Importação não é publicação; publicação documental não activa cálculo fiscal; rascunho não é candidatura aprovada.</li><li>Integração desenhada não significa sincronização activa. Cobertura e estado de cada domínio devem ser verificados nos respectivos relatórios de validação.</li><li>Antes de alterações a dados activos, confirmar backup/restauro e acção autenticada. Testes e migrações usam exclusivamente base QA isolada.</li></ul>
|
||||
|
||||
<h2>11. Fontes de análise</h2>
|
||||
<ul><li>README.md do repositório;</li><li>docs/investmoz/casos-de-uso-aplicacao-actual.md;</li><li>docs/investmoz/erd.mmd, data-dictionary.csv e table-catalog.csv;</li><li>rotas, controladores, modelos, jobs, comando de escalada e migrations;</li><li>InvestMoz — Levantamento funcional, casos de uso e diagramas.docx, em Downloads.</li></ul>
|
||||
<p class="note">O inventário cobriu 981 caminhos listados por rg --files, excluindo vendor e node_modules. Isso descreve o repositório acessível, não anexos externos, conteúdo de armazenamento privado ou base de dados de produção.</p>
|
||||
</body></html>
|
||||
@@ -0,0 +1,526 @@
|
||||
# Casos de uso da aplicação actual
|
||||
|
||||
Este documento descreve os processos actualmente implementados ou previstos
|
||||
pelas rotas, controladores, serviços e migrations existentes no projecto.
|
||||
Não substitui regras legais nem autoriza publicação de dados.
|
||||
|
||||
## 1. Actores
|
||||
|
||||
| Actor | Responsabilidade |
|
||||
|---|---|
|
||||
| Visitante | Consulta o portal, oportunidades, negócios, zonas e documentos públicos. |
|
||||
| Utilizador registado | Mantém perfil, rascunhos, submissões, favoritos e mensagens. |
|
||||
| Investidor | Pesquisa oportunidades, manifesta interesse e acompanha candidaturas. |
|
||||
| Gestor | Trata interesses, leads, tarefas, oportunidades e catálogo. |
|
||||
| Revisor documental | Verifica documentos, malware, versões e autorização de divulgação. |
|
||||
| Gestor territorial | Gere projectos, clusters, corredores, geografia e zonas. |
|
||||
| Gestor fiscal | Gere fontes legais, versões de regras e parâmetros do simulador. |
|
||||
| Gestor de educação | Gere competências, necessidades, cursos e propostas. |
|
||||
| Parceiro institucional | Participa em parcerias, salas, mensagens, documentos e marcos. |
|
||||
| Administrador | Gere utilizadores, permissões, configurações e instalação de módulos. |
|
||||
| Sistema externo | Recebe/envia eventos através de outbox, inbox e sincronizações. |
|
||||
|
||||
## 2. Visão geral do ciclo da aplicação
|
||||
|
||||
```text
|
||||
Portal público
|
||||
-> consulta de catálogo
|
||||
-> interesse, apoio ou simulação
|
||||
-> autenticação/conta quando necessário
|
||||
-> candidatura ou submissão
|
||||
-> documentos e validação
|
||||
-> CRM, tarefas e comunicação
|
||||
-> decisão, parceria ou acompanhamento
|
||||
```
|
||||
|
||||
Os principais agregados são `opportunities`, `existing_businesses`,
|
||||
`interest_requests`, `investment_submissions`, `documents`, `investment_zones`
|
||||
e `simulations`.
|
||||
|
||||
## 3. UC-01 — Consultar a página inicial e serviços
|
||||
|
||||
**Actor:** Visitante.
|
||||
|
||||
**Pré-condições:** O portal está disponível.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O visitante abre `/`.
|
||||
2. O sistema apresenta a página inicial e os serviços publicados.
|
||||
3. O visitante escolhe oportunidades, negócios, zonas, simulador, documentos,
|
||||
apoio ou parcerias.
|
||||
4. O sistema aplica apenas dados que estejam disponíveis para o público.
|
||||
|
||||
**Dados:** `portal_resources`, `documents`, `opportunities`, `sectors`,
|
||||
`existing_businesses`, `investment_zones`.
|
||||
|
||||
**Resultado:** O visitante é encaminhado para o processo escolhido.
|
||||
|
||||
## 4. UC-02 — Pesquisar sectores e oportunidades
|
||||
|
||||
**Actor:** Visitante ou investidor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O actor consulta `/oportunidades` ou a API de oportunidades.
|
||||
2. O sistema filtra por sector, localização, slug, estado e outros critérios.
|
||||
3. O detalhe da oportunidade mostra perfil, território, relações, documentos
|
||||
públicos e informação de investimento.
|
||||
4. O actor pode iniciar uma manifestação de interesse ou guardar a oportunidade.
|
||||
|
||||
**Dados:** `sectors`, `opportunities`, `opportunity_profiles`,
|
||||
`opportunity_facets`, `opportunity_clusters`, `opportunity_corridors`,
|
||||
`opportunity_projects`, `opportunity_locations`, `opportunity_favourites`.
|
||||
|
||||
**Resultado:** Consulta pública ou início de UC-05/UC-06.
|
||||
|
||||
## 5. UC-03 — Consultar um negócio existente
|
||||
|
||||
**Actor:** Visitante ou investidor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O actor consulta `/negocios`.
|
||||
2. O sistema lista negócios cujo estado e autorização permitem divulgação.
|
||||
3. O actor abre o detalhe pelo `slug`.
|
||||
4. O sistema apresenta apenas título, descrição, modalidade, localização,
|
||||
objectos e documentos autorizados.
|
||||
5. O actor pode iniciar uma manifestação de interesse.
|
||||
|
||||
**Dados:** `existing_businesses`, `existing_business_objects`,
|
||||
`existing_business_locations`, `existing_business_document_links`,
|
||||
`documents`, `interest_requests`.
|
||||
|
||||
**Regra:** Dados privados, mandatos e documentos não autorizados não são
|
||||
expostos pelo portal público.
|
||||
|
||||
## 6. UC-04 — Criar conta, autenticar e recuperar acesso
|
||||
|
||||
**Actor:** Visitante/utilizador.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O visitante cria uma conta com nome, email e palavra-passe.
|
||||
2. O sistema cria `users`.
|
||||
3. O utilizador confirma o email quando aplicável.
|
||||
4. O utilizador inicia sessão.
|
||||
5. O sistema aplica as políticas de autenticação, confirmação de palavra-passe
|
||||
e, para operações protegidas, segundo factor.
|
||||
|
||||
**Dados:** `users`, `passkeys`, `sessions`, `password_reset_tokens`.
|
||||
|
||||
**Alternativas:** Registo rejeitado por email existente; palavra-passe
|
||||
incorrecta; recuperação por token; desafio de dois factores.
|
||||
|
||||
## 7. UC-05 — Manifestar interesse numa oportunidade
|
||||
|
||||
**Actor:** Visitante ou investidor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O actor abre o formulário de interesse.
|
||||
2. Informa contacto, oportunidade, modalidade, valor e mensagem.
|
||||
3. O sistema valida os dados, consentimentos e alvo.
|
||||
4. É criado `interest_requests`.
|
||||
5. O sistema regista recibo/idempotência quando aplicável.
|
||||
6. O sistema envia notificação ou evento de tratamento.
|
||||
|
||||
**Dados:** `interest_requests`, `consents`, `interest_submission_receipts`,
|
||||
`notifications`, `outbox_events`.
|
||||
|
||||
**Estado inicial:** `received`, `submitted` ou equivalente configurado.
|
||||
|
||||
**Resultado:** O interesse fica disponível para tratamento no CRM.
|
||||
|
||||
## 8. UC-06 — Manifestar interesse num negócio existente
|
||||
|
||||
**Actor:** Investidor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O investidor escolhe um `existing_business` publicado.
|
||||
2. Selecciona a modalidade e, se aplicável, percentagem e intervalo de valor.
|
||||
3. O sistema verifica que o negócio pode receber interesse.
|
||||
4. Cria `interest_requests.existing_business_id`.
|
||||
5. Regista consentimento para contacto e partilha da informação.
|
||||
6. O caso passa para tratamento interno.
|
||||
|
||||
**Dados:** `existing_businesses`, `interest_requests`, `investors`, `consents`.
|
||||
|
||||
**Regra:** Um pedido não pode apontar simultaneamente para uma oportunidade e
|
||||
para um negócio existente.
|
||||
|
||||
## 9. UC-07 — Guardar uma oportunidade e manter espaço do investidor
|
||||
|
||||
**Actor:** Utilizador autenticado.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O utilizador abre uma oportunidade.
|
||||
2. Escolhe guardar/iniciar no espaço do investidor.
|
||||
3. O sistema regista a relação em `opportunity_favourites` ou inicia uma
|
||||
submissão/dossier.
|
||||
4. O utilizador consulta os seus itens em `/investidor` ou `/carteira`.
|
||||
|
||||
**Dados:** `users`, `opportunity_favourites`, `investment_dossiers`,
|
||||
`investor_accounts`, `investment_accounts`.
|
||||
|
||||
## 10. UC-08 — Criar e manter uma submissão de investimento
|
||||
|
||||
**Actor:** Investidor.
|
||||
|
||||
**Pré-condições:** Utilizador autenticado e verificado.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O investidor cria uma submissão.
|
||||
2. O sistema cria `investment_submissions` com estado `draft`.
|
||||
3. O investidor grava o payload e as necessidades específicas.
|
||||
4. Pode adicionar anexos e mensagens.
|
||||
5. O sistema cria eventos e revisões para manter histórico.
|
||||
6. O investidor consulta ou actualiza a submissão.
|
||||
|
||||
**Dados:** `investment_submissions`, `investment_specific_needs`,
|
||||
`investment_submission_attachments`, `investment_submission_events`,
|
||||
`investment_submission_revisions`, `investment_messages`, `documents`.
|
||||
|
||||
## 11. UC-09 — Submeter candidatura
|
||||
|
||||
**Actor:** Investidor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O investidor confirma os dados da submissão.
|
||||
2. O sistema verifica campos obrigatórios, alvo e anexos.
|
||||
3. A submissão muda de `draft` para `submitted`.
|
||||
4. É associado ou criado o pedido de interesse, quando exigido.
|
||||
5. O sistema cria evento de submissão e notifica a gestão.
|
||||
|
||||
**Resultado:** A candidatura deixa de ser apenas rascunho e entra na fila de
|
||||
tratamento interno.
|
||||
|
||||
## 12. UC-10 — Gerir um negócio existente
|
||||
|
||||
**Actor:** Investidor, proprietário ou gestor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O actor cria ou actualiza o negócio.
|
||||
2. Regista modalidade, descrição, objectos, localização e documentos.
|
||||
3. O sistema cria revisões e mantém o snapshot anterior.
|
||||
4. Um gestor pode solicitar informação, rever ou rejeitar.
|
||||
5. O negócio percorre o workflow configurado.
|
||||
6. Apenas após validação e autorização poderá ser publicado.
|
||||
|
||||
**Dados:** `existing_businesses`, `existing_business_objects`,
|
||||
`existing_business_revisions`, `existing_business_reviews`,
|
||||
`business_workflow_transitions`, `existing_business_mandates`, `documents`.
|
||||
|
||||
**Estados possíveis:** `draft`, `submitted`, `in_review`,
|
||||
`needs_information`, `approved`, `published`, `suspended`, `rejected`,
|
||||
`withdrawn`, `archived`.
|
||||
|
||||
## 13. UC-11 — Tratar interesses no CRM
|
||||
|
||||
**Actor:** Gestor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O gestor abre a fila de interesses.
|
||||
2. Consulta o pedido, consentimentos e alvo.
|
||||
3. Atribui o caso a um responsável.
|
||||
4. Converte o interesse em `leads`, quando aplicável.
|
||||
5. Cria tarefas e actividades.
|
||||
6. Actualiza o estado e regista o resultado.
|
||||
7. Pode associar um `crm_opportunity` e continuar a negociação.
|
||||
|
||||
**Dados:** `interest_requests`, `leads`, `investors`, `contacts`,
|
||||
`crm_opportunities`, `tasks`, `activities`, `consents`.
|
||||
|
||||
**Estados de interesse:** recebimento, qualificação, contacto, diligência,
|
||||
negociação, conversão, recusa, cancelamento ou encerramento.
|
||||
|
||||
## 14. UC-12 — Gerir catálogo territorial
|
||||
|
||||
**Actor:** Gestor territorial.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O gestor cria ou actualiza projectos, clusters e corredores.
|
||||
2. Liga projectos a locais.
|
||||
3. Liga projectos a clusters e corredores.
|
||||
4. Cria cadeias de valor e lacunas de investimento.
|
||||
5. Regista a fonte e o estado da relação.
|
||||
6. Outro gestor pode validar ou revogar a relação.
|
||||
|
||||
**Dados:** `projects`, `economic_clusters`, `value_chains`,
|
||||
`value_chain_links`, `development_corridors`, `cluster_projects`,
|
||||
`corridor_projects`, `corridor_clusters`, `investment_gaps`, `geo_locations`.
|
||||
|
||||
## 15. UC-13 — Gerir zonas de investimento
|
||||
|
||||
**Actor:** Gestor territorial com permissão e MFA quando exigido.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. Regista a fonte legal em `investment_legal_sources`.
|
||||
2. Cria a zona em `investment_zones`.
|
||||
3. Associa operador, categoria e localização.
|
||||
4. Adiciona geometria, anexos e relações com projectos, clusters ou corredores.
|
||||
5. Um revisor valida a informação.
|
||||
6. A zona é publicada, suspensa ou revogada.
|
||||
7. O sistema mantém revisões imutáveis.
|
||||
|
||||
**Estados:** `draft`, `published`, `suspended`; validação `pending`,
|
||||
`validated`, `historical`.
|
||||
|
||||
**Dados:** `investment_zones`, `investment_legal_sources`,
|
||||
`investment_zone_geometries`, `investment_zone_relations`,
|
||||
`investment_zone_links`, `investment_zone_attachments`, `investment_zone_revisions`.
|
||||
|
||||
## 16. UC-14 — Carregar, verificar e publicar documento
|
||||
|
||||
**Actor:** Gestor documental.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O gestor carrega o ficheiro.
|
||||
2. O sistema preserva o original e calcula o hash.
|
||||
3. Detecta duplicados e conflitos de versão.
|
||||
4. Regista `documents` e, quando necessário, `document_versions`.
|
||||
5. Executa análise antimalware.
|
||||
6. O gestor revê classificação, datas, fonte e autorização.
|
||||
7. O documento é associado ao negócio, candidatura, zona, projecto ou portal.
|
||||
8. Só então pode ser disponibilizado publicamente.
|
||||
|
||||
**Dados:** `documents`, `document_versions`, `document_malware_scans`,
|
||||
`portal_resources`, `portal_resource_revisions` e tabelas `*_documents`.
|
||||
|
||||
**Estados documentais:** pendente, limpo, infectado, rejeitado, privado,
|
||||
publicável, publicado ou revogado.
|
||||
|
||||
## 17. UC-15 — Criar rascunho de simulação
|
||||
|
||||
**Actor:** Visitante ou utilizador.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O actor abre o simulador.
|
||||
2. O sistema cria ou recupera um cookie de recuperação.
|
||||
3. Guarda os dados em `simulation_form_drafts`.
|
||||
4. O actor pode continuar mais tarde.
|
||||
5. Um utilizador autenticado pode reclamar o rascunho.
|
||||
6. O rascunho pode ser ligado a um negócio existente, mediante autorização.
|
||||
|
||||
**Resultado:** Dados incompletos permanecem como rascunho e não são tratados
|
||||
como cálculo fiscal executado.
|
||||
|
||||
## 18. UC-16 — Executar simulação
|
||||
|
||||
**Actor:** Visitante ou utilizador.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O actor submete os dados do simulador.
|
||||
2. O sistema escolhe a versão do questionário.
|
||||
3. Resolve os parâmetros fiscais elegíveis.
|
||||
4. Cria `simulations` e `simulation_scenarios`.
|
||||
5. Executa o cálculo através de job.
|
||||
6. Guarda resultados, obrigações e fontes.
|
||||
7. Gera relatório quando solicitado.
|
||||
8. Regista auditoria e idempotência.
|
||||
|
||||
**Dados:** `questionnaire_versions`, `simulations`, `simulation_scenarios`,
|
||||
`simulation_results`, `simulation_obligations`, `simulation_reports`,
|
||||
`simulation_jobs`, `simulation_result_sources`, `simulation_audit_events`.
|
||||
|
||||
## 19. UC-17 — Gerir regra fiscal
|
||||
|
||||
**Actor:** Autor, revisor, aprovador ou publicador fiscal.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O autor cria uma versão da regra.
|
||||
2. Associa a regra a uma fonte legal.
|
||||
3. O revisor verifica expressão, datas e parâmetros.
|
||||
4. O aprovador decide.
|
||||
5. O publicador disponibiliza a versão.
|
||||
6. O sistema regista cada transição.
|
||||
|
||||
**Dados:** `simulation_tax_rule_versions`, `simulation_tax_rule_events`,
|
||||
`investment_legal_sources`, `users`.
|
||||
|
||||
**Regra:** Importar ou publicar um documento não activa automaticamente um
|
||||
cálculo fiscal.
|
||||
|
||||
## 20. UC-18 — Gerir educação e competências
|
||||
|
||||
**Actor:** Gestor de educação.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. Mantém catálogo de competências, profissões, instituições e cursos.
|
||||
2. Regista necessidades de formação de projectos.
|
||||
3. Associa ocupações e competências.
|
||||
4. Regista capacidades e oportunidades educativas.
|
||||
5. Analisa lacunas.
|
||||
6. Cria proposta de investimento educativo.
|
||||
|
||||
**Dados:** `skills`, `occupations`, `training_needs`, `training_providers`,
|
||||
`courses`, `education_opportunities`, `investment_gaps`,
|
||||
`education_investment_proposals` e tabelas `skills_edu`.
|
||||
|
||||
## 21. UC-19 — Abrir e tratar assistência de terra
|
||||
|
||||
**Actor:** Investidor e gestor de assistência.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O investidor cria um caso de assistência.
|
||||
2. Regista localização, necessidade e documentos.
|
||||
3. O gestor analisa o caso.
|
||||
4. São registadas revisões, eventos e pedidos adicionais.
|
||||
5. O caso é encaminhado ou encerrado.
|
||||
|
||||
**Dados:** `land_assistance_cases`, `land_assistance_revisions`,
|
||||
`land_assistance_events`, `land_assistance_documents`, `documents`, `users`.
|
||||
|
||||
## 22. UC-20 — Criar e gerir parceria
|
||||
|
||||
**Actor:** Investidor, organização ou parceiro institucional.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O actor cria um rascunho de parceria.
|
||||
2. Regista origem, sector, local e objectivo.
|
||||
3. Outro actor manifesta interesse.
|
||||
4. A candidatura é aceite ou recusada.
|
||||
5. É aberta uma sala de parceria.
|
||||
6. Os participantes trocam mensagens e documentos.
|
||||
7. São definidos marcos, pagamentos ou acordo.
|
||||
8. O sistema regista eventos e notificações.
|
||||
|
||||
**Dados:** `portal_partnerships`, `partnership_origins`,
|
||||
`partnership_locations`, `partnership_interests`, `partnership_rooms`,
|
||||
`partnership_messages`, `partnership_documents`, `partnership_milestones`,
|
||||
`partnership_events`, `partnership_notifications`.
|
||||
|
||||
## 23. UC-21 — Usar o balcão virtual
|
||||
|
||||
**Actor:** Utilizador autenticado e agente de atendimento.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O utilizador abre um pedido no balcão.
|
||||
2. O sistema cria um caso e associa o serviço.
|
||||
3. O agente recebe o pedido.
|
||||
4. São trocadas mensagens e solicitados documentos.
|
||||
5. O caso passa por estados e revisões.
|
||||
6. Eventos, notificações e pagamentos são registados.
|
||||
7. O caso é resolvido ou encerrado.
|
||||
|
||||
**Dados:** `desk_service_versions`, `desk_cases`, `desk_requests`,
|
||||
`desk_documents`, `desk_events`, `desk_revisions`, `desk_messages`,
|
||||
`desk_notifications`, `desk_institutional_links`, `desk_payments`.
|
||||
|
||||
## 24. UC-22 — Usar inteligência artificial com evidência
|
||||
|
||||
**Actor:** Gestor autorizado.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. O gestor activa a capacidade em `ai_controls`.
|
||||
2. Regista uma fonte ou documento.
|
||||
3. O sistema cria chunks da fonte.
|
||||
4. O gestor inicia uma execução de IA.
|
||||
5. O sistema grava `ai_runs`.
|
||||
6. O resultado pode ser revisto ou receber feedback.
|
||||
7. A aplicação mantém a fonte que suporta a resposta.
|
||||
|
||||
**Dados:** `ai_controls`, `ai_sources`, `ai_chunks`, `ai_runs`, `ai_feedback`,
|
||||
`documents`, `users`.
|
||||
|
||||
## 25. UC-23 — Sincronizar com sistemas externos
|
||||
|
||||
**Actor:** Sistema externo ou processo de integração.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. A aplicação gera um evento em `outbox_events`.
|
||||
2. Um worker publica o evento.
|
||||
3. O sistema externo responde ou envia evento para `inbox_events`.
|
||||
4. A aplicação cria `sync_runs`.
|
||||
5. Falhas ou divergências são registadas em `reconciliation_issues`.
|
||||
6. O processo pode ser repetido de forma idempotente.
|
||||
|
||||
## 26. UC-24 — Auditoria e controlo de alterações
|
||||
|
||||
**Actor:** Sistema, gestor ou auditor.
|
||||
|
||||
**Fluxo principal:**
|
||||
|
||||
1. Uma operação protegida é executada.
|
||||
2. O sistema identifica actor, entidade, acção e correlação.
|
||||
3. Regista evento de auditoria.
|
||||
4. Alterações de entidades versionadas geram revisão/snapshot.
|
||||
5. O auditor consulta o histórico sem alterar o registo original.
|
||||
|
||||
**Dados:** `audit_events`, `simulation_audit_events`, `investment_zone_revisions`,
|
||||
`existing_business_revisions`, `investment_submission_revisions`,
|
||||
`desk_revisions`, `outbox_events`.
|
||||
|
||||
## 27. Regras transversais da aplicação
|
||||
|
||||
- Dados privados exigem autenticação e autorização.
|
||||
- Divulgação pública depende do estado de publicação e autorização registada.
|
||||
- Documentos originais devem ser preservados.
|
||||
- Hash, versão e proveniência devem permanecer associados ao documento.
|
||||
- Relações jurídicas ou territoriais não validadas ficam pendentes.
|
||||
- Rascunho não é submissão aprovada.
|
||||
- Importação não é publicação.
|
||||
- Publicação de documento não é activação de regra fiscal.
|
||||
- Cálculos fiscais devem usar fonte e parâmetro identificados.
|
||||
- Histórico e auditoria não devem ser apagados por rollback destrutivo.
|
||||
- Operações de migração e testes devem usar base QA isolada.
|
||||
|
||||
## 28. Mapa resumido dos processos
|
||||
|
||||
```text
|
||||
Visitante
|
||||
-> consulta catálogo
|
||||
-> interesse / apoio / simulação
|
||||
|
||||
Investidor
|
||||
-> conta
|
||||
-> favoritos / dossier
|
||||
-> submissão
|
||||
-> documentos
|
||||
-> mensagens
|
||||
|
||||
Gestor
|
||||
-> interesse
|
||||
-> lead
|
||||
-> tarefas
|
||||
-> oportunidade CRM
|
||||
-> decisão
|
||||
|
||||
Gestor territorial
|
||||
-> projecto / cluster / corredor
|
||||
-> zona
|
||||
-> geometria
|
||||
-> fonte legal
|
||||
-> validação / publicação
|
||||
|
||||
Gestor fiscal
|
||||
-> regra
|
||||
-> revisão
|
||||
-> aprovação
|
||||
-> publicação
|
||||
-> parâmetro usado pelo simulador
|
||||
|
||||
Parceiros
|
||||
-> parceria
|
||||
-> candidatura
|
||||
-> sala
|
||||
-> mensagens / documentos / marcos
|
||||
```
|
||||
@@ -315,3 +315,27 @@ Situação comum observada: `publication_status=draft`, `validation_status=valid
|
||||
- Verificação: `php artisan test tests/Feature/PortalServicesTest.php` — 17 testes/497 asserções aprovados em QA; cobrem publicação autorizada com pesquisa textual e filtros combinados, ficha, download, associação ao módulo e links de requisitos com estados confirmados/pendentes. `npm.cmd run build` concluído. O build alerta sobre pacote opcional `fontaine` e chunk `services` acima de 500 KB; sem erro de compilação. Não houve browser autenticado disponível, portanto não se declara publicação, pesquisa ou download públicos concluídos.
|
||||
- A autorização documental não foi aplicada a zonas, parques, negócios privados ou oportunidades de demonstração. A regra de publicação de parque não exige área confirmada; divergência de área, isoladamente, não é um gate técnico. Se a ficha avançar por decisão própria, deve omitir o total disputado ou mostrar os valores reportados com fonte/escopo, sem alegar disponibilidade. Beluluane, Munhava e Topuito requerem revisão por registo para fonte/localização pública e para retirar ou qualificar a área; Topuito só tem província associada no mestre geográfico, pelo que não se pode anunciar uma associação administrativa a Larde enquanto essa referência não existir. ZEE/ZFI continuam dependentes de acto oficial estruturado e validado, regime e relações territoriais apropriadas; uma área sem confirmação não substitui essas evidências.
|
||||
- Proposta para as seis oportunidades de demonstração já públicas: preservar os registos e IDs, adicionar origem/tipo `demo` e identificá-los visivelmente como **Exemplo de demonstração**, separar a pesquisa de exemplos da pesquisa de oportunidades reais e excluir os exemplos dos resultados reais por defeito. Não os converter nem retirar em massa; cada registo real exigirá origem e proponente documentados. Nenhuma alteração foi feita nesta execução.
|
||||
|
||||
### Referência da entrada única de gestão — QA, 22/10/2026
|
||||
|
||||
Nos requisitos existentes `MASTER-9f6dd1d9-0001`, `MASTER-9f6dd1d9-0003` e `MASTER-02f44baf-0002`, acrescentar como evidência delimitada a secção «Entrada única de gestão — preparação em QA, 22/10/2026» do manual de rastreabilidade e `tests/Feature/ContentManagementEntryTest.php`: quatro testes/99 asserções, ClamAV real e reversão transaccional em QA. Origem do objectivo e das regras operacionais: decisão expressa da Tempo; navegação, filtros e recuperação do upload são escolhas de implementação. Reutilizados biblioteca, armazenamento, perfis e histórico. Não altera os critérios de aceitação nem fecha os requisitos integrais: integração/contas reais e categorias não demonstradas conservam o seu estado. Geometrias permanecem separadas.
|
||||
|
||||
|
||||
AUD-GEO-02/03: Recuperação da demarcação territorial — QA, 22/10/2026. 408/581 limites recuperados em QA; cadeia territorial guardada/reaberta no portal. Homologacao ainda sem reposicao; consultar a nova secao no manual existente. Mantidas 173 correspondencias pendentes e ausencia de geometrias proprias das localidades. Origem: pedido expresso da Tempo de recuperar funcionalidade; enquadramento e mensagens sao escolhas tecnicas. Nao encerra o criterio de reposicao no destino.
|
||||
|
||||
|
||||
AUD-GEO-02/03 - 22/10/2026: Demarcação territorial reposta na homologação — 22/10/2026. 408 limites (11/133/264) importados pela conta real/MFA, com tres recibos auditados; backup/restauro de 171 tabelas conferido, 333 ficheiros verificados. Consulta do mapa demonstrada na oportunidade real #30, sem a alterar. Acessos previstos e originais privados preservados. Reposicao no destino concluida; 26+147 correspondencias e ausencia de poligonos proprios de localidades mantidas como lacunas reais. Ver fecho e guia no manual existente; sem repetir provas QA aceites.
|
||||
|
||||
### Incremento Concursos e anúncios — QA, 22/10/2026
|
||||
|
||||
Instalação autorizada e concluída na homologação em 22/10/2026: integração funcional `f4d09709`, schema selectivo/contrato/activação conferidos após backup e restauro isolado de 171 tabelas. Gestão real com MFA: cadastro, recuperação e prévia de rascunho privado auditado; catálogo público 200, zero anúncios publicados. Preservados dados/permissões/originais e geometrias. Evidência e links finais em «Instalação concluída na homologação» no manual e `unification/procurement-installation-receipt-20261022.json`. Provas QA aceites reutilizadas. Este fecho substitui o estado de proposta pendente de instalação abaixo, sem declarar aceite pela Tempo o guia não conferido nem expandir o âmbito para recepção de propostas.
|
||||
|
||||
Proposta de entrega da candidata `1bcf34c0`: ver «Proposta de instalação compatível com deploy automático» no manual existente. Identificados três novos cadastros e o gate real de contrato de migrations do pipeline. Sequência proposta: revisão → coordenação do deploy → backup/restauro isolado → migration selectiva autenticada com flag desligada → contrato → integração/build/promoção → activação → conferência do delta no destino. Reversão conserva tabelas, actos, auditoria e originais posteriores; não restaura backup antigo sobre dados novos. Provas QA reutilizadas; nenhuma integração, migration remota, activação ou publicação real executada nesta passagem. A Tempo ainda não conferiu links locais/guia; não registar aceitação nem entrega no destino por essa razão.
|
||||
|
||||
Nos requisitos existentes de gestão/publicação `MASTER-9f6dd1d9-0001`, `MASTER-9f6dd1d9-0003` e `MASTER-02f44baf-0002`, registar o recorte solicitado pela Tempo: procedimentos com regime/edição/fontes, lotes, rascunhos recuperáveis, prévia, actos versionados, catálogo/ficha/anexos anónimos, filtros, impressão e acompanhamento por conta simples. Evidência: secção «Concursos e anúncios — implementação em QA» do manual existente e `tests/Feature/ProcurementTest.php`, com passagem adicional no navegador dos três regimes, prorrogação, cancelamento, download e acompanhamento. Reutilizadas autorizações e biblioteca; não foram criados papéis ou aprovadores. Origem funcional: pedido expresso da Tempo; flag, prévia assinada e controlo de versão são escolhas técnicas. Entrega delimitada a QA: integração em main, migração/activação no destino e anúncios reais seguem o processo existente. Não fecha requisitos integrais de contratação/recepção de propostas, nem declara parametrizado todo o articulado legal. Geometrias permanecem no recorte separado já entregue.
|
||||
|
||||
### Acervo legado — correspondência e preparação QA, 22/10/2026
|
||||
|
||||
Actualização do mesmo requisito: os 28 rascunhos têm edição, leitura privada, prévia sem guardar, recuperação e publicação/retirada pela interface em QA, preservando originais, origem e revisões. Seis testes/1.000 asserções, TypeScript e build passaram; publicação positiva apenas em transacções revertidas. Pacote privado v2 preparado com 28 originais e proveniência; não transportado nem publicado na homologação. Guia, permissões existentes, lacunas de imagens, dependências da entrega e reversão na secção «Legado — edição e leitura dos 28 rascunhos em QA» do manual. Pedido funcional expresso da Tempo; hash de âmbito, prévia e controlo de versão são escolhas técnicas, sem novo papel/aprovador. Conserva o saldo de 20 públicos aceites e dois originais incompletos, sem repetir provas ou fechar a integração integral.
|
||||
|
||||
Referência do acervo legado, 22/10/2026: a lista única `v2/auditoria-portal/legado-correspondencia-integral.md` e o CSV existente reconciliam 50 identidades (20 publicados, 28 por integrar no destino, um vazio, um sem título). 20/20 páginas conferidas por HTTP anónimo/identidade/origem; cartões de Turismo observados no destino. Os 28 restantes avançaram para rascunhos internos QA, com ClamAV real e repetição idempotente, sem publicação. Recibo e limites no manual, secção «Legado — correspondência integral e avanço privado QA». As duas lacunas editoriais foram confirmadas no original. Não fecha a integração integral nem substitui revisão editorial por prova HTTP; nenhuma alteração activa nesta passagem.
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
# Levantamento funcional, entidades e eventos — InvestMoz
|
||||
|
||||
**Data do levantamento:** 9 de Outubro de 2026
|
||||
**Âmbito:** aplicação Laravel/React presente neste repositório.
|
||||
**Base de evidência:** README, rotas, controladores, modelos, migrations e documentação em `docs/investmoz/`. A existência de uma tabela, rota ou documento não prova que a função esteja activa em produção.
|
||||
|
||||
## 1. Resumo executivo
|
||||
|
||||
O projecto é uma plataforma de investimento com área pública e área autenticada. Abrange descoberta de oportunidades e negócios, manifestações de interesse, gestão de candidaturas e carteira, CRM local, geografia e zonas de investimento, simulador e governação fiscal, catálogo documental, educação, assistência de terra, parcerias, balcão virtual, IA com fontes e integrações preparadas.
|
||||
|
||||
O README apresenta a base inicial do portal (sectores, oportunidades e manifestações). O código actual e os documentos de domínio registam a extensão funcional posterior. Para o inventário detalhado dos fluxos, consultar [casos-de-uso-aplicacao-actual.md](casos-de-uso-aplicacao-actual.md), que identifica 24 casos de uso. Este documento complementa-o com uma visão de domínio, um diagrama resumido de relações e a frequência observável dos eventos.
|
||||
|
||||
## 2. Âmbito e método
|
||||
|
||||
Foi feito inventário dos caminhos acessíveis no repositório: **981 ficheiros** por `rg --files`, excluindo dependências geradas `vendor` e `node_modules`. Dentro da aplicação foram consultados, em particular, `routes/`, `app/Models/`, `app/Http/Controllers/`, `database/migrations/`, `database/seeders/` e `docs/investmoz/`. O número é uma contagem do inventário local nessa data, não um inventário de anexos externos ou de dados de produção.
|
||||
|
||||
Classificação usada:
|
||||
|
||||
- **Implementado no código:** rota, controlador, job, comando ou schema identificável.
|
||||
- **Documentado/planeado:** descrito em documentação, sem prova suficiente de execução integrada.
|
||||
- **Frequência de negócio desconhecida:** não há amostra de eventos de produção nem telemetria para calcular volume por hora/dia/mês.
|
||||
- **Cadência técnica explícita:** periodicidade configurada no scheduler ou no mecanismo de fila.
|
||||
|
||||
## 3. Actores e sistemas
|
||||
|
||||
| Actor | Objectivo principal | Áreas usadas |
|
||||
|---|---|---|
|
||||
| Visitante | Consultar conteúdos públicos e iniciar pedidos | Portal, oportunidades, negócios, zonas, documentos, simulador |
|
||||
| Investidor/utilizador | Manter perfil, favoritos, rascunhos, submissões, documentos e mensagens | Espaço do investidor, carteira, assistência, parcerias |
|
||||
| Gestor de portal/CRM | Rever interesses, gerir leads, tarefas, catálogo e publicação | CRM local, gestão de portal |
|
||||
| Gestor territorial | Manter projectos, clusters, corredores, relações, geografia e zonas | Catálogo territorial, GIS, zonas |
|
||||
| Revisor documental | Conferir proveniência, versão, hash, autorização e análise antimalware | Biblioteca documental |
|
||||
| Gestor fiscal | Rever fontes, parâmetros e versões das regras fiscais | Governação fiscal, simulador |
|
||||
| Gestor de educação | Manter competências, necessidades, cursos, capacidades e propostas | Educação |
|
||||
| Gestor/agente de assistência | Tratar pedidos de terra e pedidos do balcão virtual | Assistência de terra, balcão |
|
||||
| Parceiro institucional | Participar em oportunidades/parcerias e colaborar em salas | Parcerias |
|
||||
| Administrador | Gerir autenticação, segurança, configurações e permissões | Administração |
|
||||
| Sistema externo/worker | Receber/enviar mensagens ou executar trabalho assíncrono | Outbox/inbox, filas, integrações |
|
||||
|
||||
Papéis são uma leitura funcional dos fluxos. A autorização efectiva depende das policies, middleware e permissões implementadas, não apenas desta tabela.
|
||||
|
||||
## 4. Catálogo de casos de uso
|
||||
|
||||
O documento [Casos de uso da aplicação actual](casos-de-uso-aplicacao-actual.md) detalha pré-condições, fluxos e dados para cada caso. Índice consolidado:
|
||||
|
||||
| ID | Caso de uso | Actor principal | Resultado |
|
||||
|---|---|---|---|
|
||||
| UC-01 | Consultar página inicial e serviços | Visitante | Entrada no serviço público autorizado |
|
||||
| UC-02 | Pesquisar sectores e oportunidades | Visitante/investidor | Lista/detalhe e possível favorito/interesse |
|
||||
| UC-03 | Consultar negócio existente | Visitante/investidor | Consulta da oferta divulgável |
|
||||
| UC-04 | Criar conta, autenticar e recuperar acesso | Utilizador | Sessão autenticada/verificada |
|
||||
| UC-05 | Manifestar interesse numa oportunidade | Visitante/investidor | Pedido recebido para tratamento |
|
||||
| UC-06 | Manifestar interesse num negócio existente | Investidor | Pedido ligado ao negócio e consentimento |
|
||||
| UC-07 | Guardar oportunidade e manter espaço do investidor | Utilizador | Favorito, dossier ou item na carteira |
|
||||
| UC-08 | Criar e manter submissão de investimento | Investidor | Submissão em rascunho com histórico |
|
||||
| UC-09 | Submeter candidatura | Investidor | Entrada em fila de tratamento |
|
||||
| UC-10 | Gerir negócio existente | Proprietário/gestor | Registo revisto e eventualmente publicável |
|
||||
| UC-11 | Tratar interesses no CRM | Gestor | Qualificação, lead, tarefa e seguimento |
|
||||
| UC-12 | Gerir catálogo territorial | Gestor territorial | Projectos, clusters, corredores e relações |
|
||||
| UC-13 | Gerir zonas de investimento | Gestor territorial/revisor | Zona validada, publicada ou suspensa |
|
||||
| UC-14 | Carregar, verificar e publicar documento | Revisor documental | Documento preservado com estados independentes |
|
||||
| UC-15 | Criar/retomar rascunho de simulação | Visitante/utilizador | Rascunho recuperável, sem cálculo implícito |
|
||||
| UC-16 | Executar simulação | Visitante/utilizador | Resultado, obrigações, fontes e relatório |
|
||||
| UC-17 | Gerir regra fiscal | Gestor fiscal | Versão de regra passa por revisão/aprovação |
|
||||
| UC-18 | Gerir educação e competências | Gestor de educação | Catálogo, análise de lacunas e proposta |
|
||||
| UC-19 | Abrir/tratar assistência de terra | Investidor/gestor | Caso acompanhado até encaminhamento/fecho |
|
||||
| UC-20 | Criar e gerir parceria | Investidor/parceiro | Interesse, sala, mensagens, documentos e marcos |
|
||||
| UC-21 | Usar balcão virtual | Utilizador/agente | Pedido de serviço acompanhado até resolução |
|
||||
| UC-22 | Usar IA com evidência | Gestor autorizado | Execução rastreável ligada a fontes e feedback |
|
||||
| UC-23 | Sincronizar sistemas externos | Worker/sistema externo | Eventos recebidos/enviados e divergências registadas |
|
||||
| UC-24 | Auditar alterações | Sistema/gestor/auditor | Histórico e correlação para operações relevantes |
|
||||
|
||||
## 5. Diagrama de entidades e relações
|
||||
|
||||
Diagrama lógico resumido por domínios. As cardinalidades representam a estrutura documentada; algumas relações polimórficas, opcionais ou em tabelas de junção estão simplificadas. Para o ERD de schema detalhado, consultar [erd.mmd](erd.mmd), [data-dictionary.csv](data-dictionary.csv) e [table-catalog.csv](table-catalog.csv).
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USER ||--o{ INVESTOR : possui
|
||||
USER ||--o{ SUBMISSION : cria
|
||||
USER ||--o{ SIMULATION : executa
|
||||
USER ||--o{ AUDIT_EVENT : origina
|
||||
SECTOR ||--o{ OPPORTUNITY : classifica
|
||||
OPPORTUNITY ||--o{ INTEREST_REQUEST : recebe
|
||||
EXISTING_BUSINESS ||--o{ INTEREST_REQUEST : recebe
|
||||
INVESTOR ||--o{ INTEREST_REQUEST : manifesta
|
||||
INTEREST_REQUEST ||--o| LEAD : converte
|
||||
LEAD ||--o{ CRM_TASK : acompanha
|
||||
OPPORTUNITY ||--o{ SUBMISSION : referencia
|
||||
SUBMISSION ||--o{ SUBMISSION_REVISION : versiona
|
||||
SUBMISSION ||--o{ SUBMISSION_EVENT : regista
|
||||
SUBMISSION ||--o{ DOCUMENT_LINK : anexa
|
||||
PROJECT }o--o{ ECONOMIC_CLUSTER : associa
|
||||
PROJECT }o--o{ DEVELOPMENT_CORRIDOR : associa
|
||||
PROJECT ||--o{ INVESTMENT_GAP : identifica
|
||||
INVESTMENT_ZONE ||--o{ ZONE_REVISION : versiona
|
||||
INVESTMENT_ZONE ||--o{ ZONE_RELATION : liga
|
||||
INVESTMENT_ZONE ||--o{ ZONE_ATTACHMENT : documenta
|
||||
DOCUMENT ||--o{ DOCUMENT_VERSION : versiona
|
||||
DOCUMENT ||--o{ MALWARE_SCAN : analisa
|
||||
DOCUMENT }o--o{ PORTAL_RESOURCE : disponibiliza
|
||||
SIMULATION ||--o{ SIMULATION_SCENARIO : contém
|
||||
SIMULATION_SCENARIO ||--o{ SIMULATION_RESULT : produz
|
||||
SIMULATION_RESULT ||--o{ SIMULATION_OBLIGATION : discrimina
|
||||
SIMULATION_RESULT ||--o{ RESULT_SOURCE : fundamenta
|
||||
TAX_RULE_VERSION }o--o{ LEGAL_SOURCE : fundamenta
|
||||
PROJECT ||--o{ TRAINING_NEED : requer
|
||||
SKILL }o--o{ OCCUPATION : relacionada
|
||||
TRAINING_PROVIDER ||--o{ COURSE : oferece
|
||||
USER ||--o{ LAND_ASSISTANCE_CASE : requer
|
||||
PARTNERSHIP ||--o{ PARTNERSHIP_INTEREST : recebe
|
||||
PARTNERSHIP ||--o| PARTNERSHIP_ROOM : abre
|
||||
PARTNERSHIP_ROOM ||--o{ PARTNERSHIP_MESSAGE : contém
|
||||
DESK_CASE ||--o{ DESK_EVENT : regista
|
||||
AI_SOURCE ||--o{ AI_CHUNK : divide
|
||||
AI_SOURCE ||--o{ AI_RUN : suporta
|
||||
OUTBOX_EVENT }o..|| SYNC_RUN : processa
|
||||
SYNC_RUN ||--o{ RECONCILIATION_ISSUE : detecta
|
||||
```
|
||||
|
||||
### 5.1 Domínios e entidades nucleares
|
||||
|
||||
| Domínio | Entidades nucleares | Relações principais |
|
||||
|---|---|---|
|
||||
| Identidade e acesso | `users`, `passkeys`, sessões, organizações, memberships | Utilizador possui perfil e inicia operações protegidas |
|
||||
| Catálogo público | `sectors`, `opportunities`, catálogos de referência, recursos do portal | Sector classifica oportunidades; publicação controla exposição |
|
||||
| Negócios existentes | `existing_businesses`, objectos, locais, revisões, mandatos | Negócio tem objectos/locais/documentos e workflow próprio |
|
||||
| Investidor e CRM | `investors`, `interest_requests`, `leads`, `contacts`, oportunidades CRM, tarefas | Interesse pode converter-se em lead e gerar tarefas/actividades |
|
||||
| Carteira e submissões | dossiers, `investment_submissions`, necessidades, revisões, eventos, mensagens, anexos | Submissão pertence a utilizador e liga-se a oportunidade/negócio |
|
||||
| Território e GIS | `projects`, `economic_clusters`, `development_corridors`, `geo_locations`, `value_chains`, `investment_gaps` | Relações muitos-para-muitos documentam associação territorial/económica |
|
||||
| Zonas de investimento | `investment_zones`, fontes legais, geometrias, relações, anexos, revisões | Zona tem localização, evidência, relações e estado de validação/publicação |
|
||||
| Documentos | `documents`, versões, scans, associações por domínio, recursos do portal | Documento pode ser associado a várias entidades; disponibilidade, estado documental e uso em cálculo são independentes |
|
||||
| Simulação e fiscalidade | rascunhos, `simulations`, cenários, resultados, obrigações, fontes, regras versionadas | Execução usa versões/parâmetros elegíveis e preserva as fontes do resultado |
|
||||
| Educação | skills, occupations, providers, courses, training needs/capacity, propostas | Projectos expressam necessidades relacionadas com competências e formação |
|
||||
| Serviços e colaboração | casos de terra, balcão virtual, parcerias, salas, mensagens, documentos, marcos | Casos e salas mantêm eventos e participantes próprios |
|
||||
| IA e governação | controlos, fontes/chunks, execuções, feedback, auditoria | Execução de IA deve poder ser rastreada às fontes e ao actor |
|
||||
| Integrações | `outbox_events`, `inbox_events`, `sync_runs`, reconciliações | Eventos são idempotentes/correlacionáveis; desenho de schema não prova integração activa |
|
||||
|
||||
## 6. Eventos e frequência
|
||||
|
||||
### 6.1 Frequência explicitamente configurada
|
||||
|
||||
| Evento/processo | Gatilho | Frequência/cadência no código | Natureza e observação |
|
||||
|---|---|---|---|
|
||||
| Escalar tarefas vencidas (`investmoz:escalate-overdue-tasks`) | Scheduler Laravel | **A cada 15 minutos**, com `withoutOverlapping()` | Única periodicidade de scheduler encontrada em `routes/console.php`; execução real requer scheduler/cron activo |
|
||||
| Executar simulação (`RunSimulation`) | Pedido de cálculo aceite | Por submissão, via fila, `afterCommit` | Assíncrono; frequência depende da procura e de workers activos |
|
||||
| Analisar documento (`ScanDocumentForMalware`) | Upload/solicitação de scan em fluxos suportados | Por documento, via fila, `afterCommit`; job único por identidade configurada | Cadência sob procura; requer worker e scanner configurados. Upload/publicação não confirma scan limpo |
|
||||
| Comparar compatibilidade MNE (`CompareMneBusiness`) | Alteração de negócio que agenda comparação | Por alteração aplicável, em fila específica e `afterCommit` | Trabalho assíncrono/sombra; volume real desconhecido |
|
||||
| Notificar recepção de interesse (`NotifyInterestReceived`) | Disparo esperado após recepção de interesse | Job existe, mas não foi localizada chamada a `dispatch()` no código consultado; frequência de execução não confirmada | Tratar como capacidade prevista até localizar/activar o disparo; entrega efectiva também depende de configuração e worker |
|
||||
| Auditoria, revisões e eventos de domínio | Acção de utilizador/sistema | No momento da operação que os gera | Orientado a evento; não há taxa histórica neste levantamento |
|
||||
| Outbox/inbox e sincronização externa | Evento de domínio ou mensagem externa | Sem periodicidade operacional confirmada neste código/documentação consultada | Estrutura preparada; documentação aponta sincronização externa real como pendente/não activada |
|
||||
|
||||
### 6.2 Frequência por categoria de actividade
|
||||
|
||||
| Categoria | Frequência funcional esperada | O que é possível afirmar |
|
||||
|---|---|---|
|
||||
| Leituras públicas (home, catálogo, oportunidade, negócio, mapas/referências) | Sob procura do visitante | Há endpoints/páginas; sem logs ou analytics não se quantificam consultas |
|
||||
| Escritas de investidor (interesses, favoritos, submissões, mensagens, perfil) | Sob acção do utilizador | São iniciadas por pedido explícito; não há volume observado disponível |
|
||||
| Gestão (revisão, publicação, CRM, regras, zonas, catálogo) | Sob trabalho de gestores autorizados | Fluxos e protecções documentados; frequência por papel não medida |
|
||||
| Simulação e rascunhos | Sob utilização do simulador; rascunhos podem ser actualizados/retomados | Rota e jobs encontrados; não assumir execução periódica nem cálculo fiscal por guardar rascunho |
|
||||
| Uploads e análise documental | Sob envio/reanálise de documento | Job de scan é disparado por fluxos suportados; scan e publicação são estados distintos |
|
||||
| Auditoria | Sob operações auditadas | A frequência acompanha as operações instrumentadas; cobertura total precisa de confirmação por fluxo |
|
||||
| Regras fiscais e fontes | Sob processo de revisão/aprovação e alteração normativa | Não há calendário fixo; fonte publicada não activa automaticamente cálculo |
|
||||
| Integrações externas | Não determinável/activação pendente | Tabelas de integração e documentação não são prova de entrega ou sincronização real |
|
||||
|
||||
### 6.3 Frequência observada versus frequência de execução
|
||||
|
||||
Não foram disponibilizados logs, contagens de `audit_events`, filas, analytics, pedidos HTTP ou exportação da base activa. Por isso, **não é possível apresentar eventos por hora/dia/mês, sazonalidade, taxa de falhas ou utilizadores activos**. A cadência de 15 minutos é apenas o intervalo configurado para o comando agendado. Para apurar frequência real, recolher métricas autorizadas por tipo de evento, intervalo temporal e resultado, sem expor dados pessoais; documentar período da amostra, filtros, retenção e origem. Não inferir métricas a partir de seeders, testes, screenshots ou schema.
|
||||
|
||||
## 7. Sequência de alto nível
|
||||
|
||||
```text
|
||||
Visita pública
|
||||
-> pesquisa catálogo/oportunidades/negócios/território
|
||||
-> interesse, candidatura, assistência ou simulação
|
||||
-> autenticação e consentimento quando exigidos
|
||||
-> gravação do pedido/rascunho e histórico
|
||||
-> trabalho assíncrono quando aplicável (scan, cálculo, notificação)
|
||||
-> revisão por responsável autorizado
|
||||
-> decisão, comunicação ou publicação com autorização
|
||||
-> auditoria e eventual integração externa homologada
|
||||
```
|
||||
|
||||
## 8. Regras transversais que afectam os fluxos
|
||||
|
||||
- Disponibilidade para consulta, situação jurídica/documental e uso em regra/cálculo são estados independentes.
|
||||
- Importar ou publicar documento não activa regra nem cálculo fiscal.
|
||||
- Relações sem citação/evidência verificada devem ficar pendentes e identificadas.
|
||||
- Guardar um rascunho não equivale a submeter ou aprovar.
|
||||
- A divulgação obedece à autorização registada e ao seu âmbito.
|
||||
- Documento original, proveniência, identificador, edição/data, hash e histórico devem ser preservados; não marcar autenticidade, vigência, direitos ou scan como confirmados sem evidência.
|
||||
- Integração desenhada (outbox/inbox) não equivale a sincronização activa.
|
||||
- Alterações a dados activos exigem confirmação de backup/restauro e acção autenticada necessária; testes/migrações devem usar exclusivamente base QA isolada, conforme `AGENTS.md`.
|
||||
|
||||
## 9. Limitações e pontos para validação
|
||||
|
||||
1. A cobertura implementada de cada UC varia; alguns documentos descrevem trabalho parcial, proposto ou bloqueado. Consultar `docs/investmoz/validation.md`, `completion-audit-2026-10-04.md`, `delivery-gaps.md` e os README de cada domínio antes de declarar operação concluída.
|
||||
2. O ERD resumido não substitui constraints/FKs, cardinalidades exactas, escopos de acesso ou estados enumerados no schema e OpenAPI.
|
||||
3. A frequência real de utilização e dos eventos de negócio permanece **indeterminada** sem telemetria operacional.
|
||||
4. Execução do scheduler, workers, ClamAV, notificações, SuiteCRM e restantes conectores depende de configuração e operação externa verificável.
|
||||
5. O inventário desta tarefa é do repositório, não dos ficheiros/anexos em armazenamento externo nem da base de dados de produção.
|
||||
|
||||
## 10. Fontes locais principais
|
||||
|
||||
- [README do projecto](../../README.md)
|
||||
- [Casos de uso detalhados](casos-de-uso-aplicacao-actual.md)
|
||||
- [ERD do schema reconciliado](erd.mmd)
|
||||
- [Arquitectura e migrations](architecture-and-migrations.md)
|
||||
- [Validação](validation.md) e [auditoria de conclusão](completion-audit-2026-10-04.md)
|
||||
- Rotas: `routes/web.php`, `routes/api.php`, `routes/investment.php`, `routes/partnerships.php`, `routes/land-assistance.php`, `routes/investment-zones.php`, `routes/territorial-registry.php`
|
||||
- Cadência: `routes/console.php`; trabalhos assíncronos em `app/Jobs/`
|
||||
@@ -1646,5 +1646,360 @@
|
||||
],
|
||||
"return_evidence_id": "43317c22-50c8-40cf-98e4-e5d4d108c372",
|
||||
"B_recovered_context_revision": 2
|
||||
},
|
||||
"context_review_and_adaptive_path_preparation_20261022": {
|
||||
"reviewed_commit": "a76369eb",
|
||||
"review_status": "complete within accepted increment scope; no integration executed",
|
||||
"last_return_equivalent_evidence": "43317c22-50c8-40cf-98e4-e5d4d108c372",
|
||||
"accepted_AB_repeated": false,
|
||||
"next_lot": "Preparação de pedido de investimento privado: condição, fonte/período, formulário, percurso, cálculo e lacuna",
|
||||
"next_lot_status": "documented, not implemented or demonstrated end-to-end",
|
||||
"source_url": "https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf",
|
||||
"source_sha256": "ba1e9eaabfec5d0c4cb2d9f2444a6860f7a90f222034764197829be402d408ed",
|
||||
"new_fiscal_rule_activation": false,
|
||||
"next_acceptance": "Only synthetic C condition transitions and dossier/report links; reuse accepted context and IVA evidence"
|
||||
},
|
||||
"private_investment_executable_20261022": {
|
||||
"base": "52fbb146",
|
||||
"branch": "codex/private-investment-path-executable-QA",
|
||||
"status": "implemented and verified in isolated QA; not integrated",
|
||||
"source_version": "private-investment-preparation-2026-v1",
|
||||
"orientation": "Tempo 22/10: official sources execute without generic additional legal approval; permanent AGENTS updated",
|
||||
"qa_url": "http://127.0.0.1:18580/investimentos/4133311b-5426-4cec-900d-1f1484484083",
|
||||
"persistence": {
|
||||
"case_id": "4133311b-5426-4cec-900d-1f1484484083",
|
||||
"case_version": 8,
|
||||
"context_revision": 4,
|
||||
"mode": "registration",
|
||||
"association": {
|
||||
"lease": "e5ce142a-1178-46a2-b039-f187139c62cd"
|
||||
},
|
||||
"documents": 1,
|
||||
"context_events": 4,
|
||||
"draft_id": "beba1dec-fc05-4820-b099-24952c5ab36c",
|
||||
"draft_version": 4,
|
||||
"draft_sha256": "4136e82a3b9420ab8f980ec0d7bda6dc2a67b568c18535b5ba0ecc80efd37e37",
|
||||
"saved_context_revision": 3,
|
||||
"saved_preparation_mode": "authorization",
|
||||
"currency": "MZN",
|
||||
"date": "2026-01-01",
|
||||
"preparation_version": "private-investment-preparation-2026-v1"
|
||||
},
|
||||
"new_checks": {
|
||||
"feature_tests": 3,
|
||||
"assertions": 96,
|
||||
"frontend_delta_tests": 1,
|
||||
"typescript": "passed",
|
||||
"build": "passed"
|
||||
},
|
||||
"evidence_sha256": {
|
||||
"C-authorization-preview.png": "311c9f14ac601429666ded55c5906aa8ab018b5d31a1f101b4fb32697adf0817",
|
||||
"C-final-directness-preview.png": "2037958e2879fb184cac1c4aa07ddc367417dbd1541d979c4e653a63918cdaca",
|
||||
"C-final-path.png": "0c3c0900d861724b6def3f5850bd6c6c0f65dfee7cb01d6bb7c72c8bf645645a",
|
||||
"C-persistence-v3.json": "b8faad9a5d4d9174a489a7b1096104f3a91645201b221b1ad7ece4a217180e00",
|
||||
"C-persistence.json": "6b6120c88f41cff43b185905fdfec992ca76b359d0eab6bc1df02dee064c780d",
|
||||
"C-recovered-path.png": "c734d119b0f766d8600892713be473d1c1875cb6ff4474b6fceabef702a3490b",
|
||||
"C-report-check.json": "217b56b89041e6c8c05907af48c48dadf4f8316a2cce0f9195daf828258cb503",
|
||||
"C-report.docx": "28cfe4b75116c892d3e29fde389de7d067b456761756bab732af625178ee69ce"
|
||||
},
|
||||
"report": {
|
||||
"path": "C:\\Mac\\Home\\Downloads\\relatorio-investmoz-sintetico-qa-c-pedido-de-investimento-privado.docx",
|
||||
"size": 16674,
|
||||
"sha256": "28cfe4b75116c892d3e29fde389de7d067b456761756bab732af625178ee69ce"
|
||||
},
|
||||
"tool_limitation": "Download observation timed out and browser debugger detached; completed DOCX found in Downloads, verified before any retry; no repeated export",
|
||||
"reused": "Accepted A/B, decimal calculation, CSV, exporters and native PDF pagination",
|
||||
"main_integrated": false,
|
||||
"homologation_changed": false,
|
||||
"fiscal_rule_published_or_activated": false,
|
||||
"limitations": [
|
||||
"Only 2026 private investment preparation, documented registration/authorization conditions",
|
||||
"No administrative submission or entitlement decision",
|
||||
"No full sector licensing, foreign-currency equivalence or fiscal benefits engine",
|
||||
"ZEE/ZFI routing predicate uses QA references; no real zone visual recognition proof",
|
||||
"Dossier attachment association does not approve content; synthetic C fixture pending scan not delivered as seed",
|
||||
"Final C context v4 registration differs from frozen draft/report context v3 authorization; no silent rewrite or repeated export."
|
||||
],
|
||||
"historical_report_persistence": {
|
||||
"case_id": "4133311b-5426-4cec-900d-1f1484484083",
|
||||
"case_version": 7,
|
||||
"context_revision": 3,
|
||||
"mode": "authorization",
|
||||
"association": {
|
||||
"lease": "e5ce142a-1178-46a2-b039-f187139c62cd"
|
||||
},
|
||||
"documents": 1,
|
||||
"context_events": 3,
|
||||
"draft_id": "beba1dec-fc05-4820-b099-24952c5ab36c",
|
||||
"draft_version": 4,
|
||||
"draft_sha256": "4136e82a3b9420ab8f980ec0d7bda6dc2a67b568c18535b5ba0ecc80efd37e37",
|
||||
"saved_context_revision": 3,
|
||||
"saved_preparation_mode": "authorization",
|
||||
"currency": "MZN",
|
||||
"date": "2026-01-01",
|
||||
"preparation_version": "private-investment-preparation-2026-v1"
|
||||
},
|
||||
"directness_control": "Lei 8/2023 art14(b), R13: direct declared before using direct-investment monetary threshold; indirect/unknown unresolved only for dependent results; PPP independent condition remains executable"
|
||||
},
|
||||
"private_investment_report_alignment_20261022": {
|
||||
"functional_revision": "a1fbda06",
|
||||
"status": "targeted missing evidence completed; no functional code changes",
|
||||
"current_persistence": {
|
||||
"case_id": "4133311b-5426-4cec-900d-1f1484484083",
|
||||
"case_version": 8,
|
||||
"context_revision": 4,
|
||||
"mode": "registration",
|
||||
"association": {
|
||||
"lease": "e5ce142a-1178-46a2-b039-f187139c62cd"
|
||||
},
|
||||
"documents": 1,
|
||||
"context_events": 4,
|
||||
"draft_id": "beba1dec-fc05-4820-b099-24952c5ab36c",
|
||||
"draft_version": 5,
|
||||
"draft_sha256": "ba824fe0362ca87b301da55298122796f44fdfb11507830b3fe8f54cf88d132d",
|
||||
"saved_context_revision": 4,
|
||||
"saved_preparation_mode": "registration",
|
||||
"currency": "MZN",
|
||||
"date": "2026-01-01",
|
||||
"preparation_version": "private-investment-preparation-2026-v1"
|
||||
},
|
||||
"reports": {
|
||||
"historic_v3": {
|
||||
"path": "C:\\Mac\\Home\\Downloads\\relatorio-investmoz-sintetico-qa-c-pedido-de-investimento-privado.docx",
|
||||
"sha256": "28cfe4b75116c892d3e29fde389de7d067b456761756bab732af625178ee69ce",
|
||||
"bytes": 16674,
|
||||
"rows": [
|
||||
[
|
||||
"Contexto do investimento",
|
||||
"Revisão 3; sem ZEE, ZFI ou parque seleccionado",
|
||||
"Contexto versionado; não é reconhecimento jurídico. Referência do mesmo dossier no momento da gravação"
|
||||
],
|
||||
[
|
||||
"Preparação do investimento privado",
|
||||
"Pedido de autorização",
|
||||
"Declarações e requisitos de preparação; sem aprovação jurídica. Contexto congelado v3; private-investment-preparation-2026-v1"
|
||||
],
|
||||
[
|
||||
"Estudo de viabilidade técnica e económico-financeira",
|
||||
"Documento por associar",
|
||||
"Preparação informativa — simulação, sem submissão à Administração. Integra a preparação do pedido de autorização. R14(1)–(2); https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf#page=45"
|
||||
],
|
||||
[
|
||||
"Comprovação da capacidade financeira",
|
||||
"Documento por associar",
|
||||
"Preparação informativa — simulação, sem submissão à Administração. Integra a preparação do pedido de autorização. R14(1)–(2); https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf#page=45"
|
||||
]
|
||||
]
|
||||
},
|
||||
"current_v4": {
|
||||
"path": "C:\\Mac\\Home\\Downloads\\relatorio-investmoz-sintetico-qa-c-pedido-de-investimento-privado (1).docx",
|
||||
"sha256": "a2849427f4a91fa6ab78e433be8b919baca80a0f4f452a4546a5b169b921d753",
|
||||
"bytes": 16678,
|
||||
"rows": [
|
||||
[
|
||||
"Contexto do investimento",
|
||||
"Revisão 4; sem ZEE, ZFI ou parque seleccionado",
|
||||
"Contexto versionado; não é reconhecimento jurídico. Referência do mesmo dossier no momento da gravação"
|
||||
],
|
||||
[
|
||||
"Preparação do investimento privado",
|
||||
"Pedido de mero registo",
|
||||
"Declarações e requisitos de preparação; sem aprovação jurídica. Contexto congelado v4; private-investment-preparation-2026-v1"
|
||||
],
|
||||
[
|
||||
"Estudo de viabilidade técnica e económico-financeira",
|
||||
"Não aplicável; histórico preservado",
|
||||
"Preparação informativa — simulação, sem submissão à Administração. Integra a preparação do pedido de autorização. R14(1)–(2); https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf#page=45"
|
||||
],
|
||||
[
|
||||
"Comprovação da capacidade financeira",
|
||||
"Não aplicável; histórico preservado",
|
||||
"Preparação informativa — simulação, sem submissão à Administração. Integra a preparação do pedido de autorização. R14(1)–(2); https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf#page=45"
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"proof_sha256": {
|
||||
"C-report-HISTORICO-contexto-v3.docx": "28cfe4b75116c892d3e29fde389de7d067b456761756bab732af625178ee69ce",
|
||||
"C-report-contexto-v4.docx": "a2849427f4a91fa6ab78e433be8b919baca80a0f4f452a4546a5b169b921d753",
|
||||
"C-report-v3-v4-comparison.json": "722154fd38b2915ec087f804abe66bc4c98c6eff2c4403d8e670d65d257e3446",
|
||||
"C-persistence-v4-draft5.json": "a10505a52c553b4c0e03ece59d20d5a0a20204ae694bcf7174ee0574272ec1c4",
|
||||
"C-new-report-context-v4.png": "b0cf159e6be5a457efdc46eb5e273895c43010caa9727e3b75668f916aa11f8d"
|
||||
},
|
||||
"historic_label": "C-report-HISTORICO-contexto-v3.docx; original bytes unchanged, embedded Contexto congelado v3",
|
||||
"current_label": "C-report-contexto-v4.docx; embedded Contexto congelado v4; registration; study/capacity not applicable",
|
||||
"accepted_tests_repeated": false,
|
||||
"integration_dependencies": [
|
||||
"a76369eb common continuity, required ancestor of a1fbda06",
|
||||
"52fbb146 documentary intermediate, no executable delta",
|
||||
"a1fbda06 adaptive private-investment functionality",
|
||||
"Current branch includes ancestors; do not cherry-pick a1fbda06 alone without a76369eb/equivalent changes"
|
||||
],
|
||||
"integration_procedure": "After Tempo evaluation/decision, fetch current main, merge preserving concurrent changes and review actual conflicts. a76369eb can integrate independently first; shared ancestry avoids duplicate application. No reset or force-push main. Confirm deployed revision and only changed destination behavior. PRIVATE_INVESTMENT_PREPARATION_ENABLED is separate from tax publication/activation.",
|
||||
"remote_main_read": "57fc914821d0cefa02caa80a74b3d42e663f2742",
|
||||
"main_integrated": false,
|
||||
"fiscal_rules_changed": false
|
||||
},
|
||||
"adaptive_private_path_homologation_20261022": {
|
||||
"authorized_by": "Tempo, user authorization 22/10/2026",
|
||||
"previous_main": "57fc914821d0cefa02caa80a74b3d42e663f2742",
|
||||
"authorized_ancestry": [
|
||||
"a76369eb",
|
||||
"52fbb146",
|
||||
"a1fbda06",
|
||||
"064296e1"
|
||||
],
|
||||
"integration": "fast-forward, no conflicts or concurrent commits observed at push; no forced update",
|
||||
"first_attempt": {
|
||||
"status": "failed",
|
||||
"commit": "064296e1fbf7b17f932df23c7b3ee5508719dec5",
|
||||
"error": "Verification command failed: node (see protected build log)",
|
||||
"active_release_preserved": true,
|
||||
"requested_commits": [
|
||||
"064296e1fbf7b17f932df23c7b3ee5508719dec5"
|
||||
],
|
||||
"finished_at": "2026-10-22T08:11:54.420414+00:00"
|
||||
},
|
||||
"compatibility_fix": {
|
||||
"commit": "042ffbde44cac27704d6f8b7ec25b525a42e3575",
|
||||
"cause": "Empty inheritance metadata appended when restoring a legacy draft with no compatible source",
|
||||
"change": "Return original draft when both compatible source and prior inheritance metadata are absent",
|
||||
"targeted_tests": "9 inheritance controls + exact failing legacy partition/recovery test, all passed"
|
||||
},
|
||||
"installed": {
|
||||
"status": "installed",
|
||||
"commit": "042ffbde44cac27704d6f8b7ec25b525a42e3575",
|
||||
"release": "/var/www/investmoz-homologacao/releases/042ffbde44cac27704d6f8b7ec25b525a42e3575-6dfae60c",
|
||||
"previous_release": "/var/www/investmoz-homologacao/releases/57fc914821d0cefa02caa80a74b3d42e663f2742-f563f9a7",
|
||||
"database_migrations": 0,
|
||||
"content_imports": 0,
|
||||
"requested_commits": [
|
||||
"042ffbde44cac27704d6f8b7ec25b525a42e3575"
|
||||
],
|
||||
"finished_at": "2026-10-22T08:16:16.903501+00:00"
|
||||
},
|
||||
"http": {
|
||||
"/build/revision.json": {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"commit": "042ffbde44cac27704d6f8b7ec25b525a42e3575",
|
||||
"branch": "main",
|
||||
"built_at": "2026-10-22T08:16:14.331119+00:00",
|
||||
"manifest_sha256": "655eef6e6a5e71c5138c68465a5b2ffc36d2cadeff96e6b35682dbfd19d42e79"
|
||||
}
|
||||
},
|
||||
"/api/v1/fiscal-consumer": {
|
||||
"status": 200,
|
||||
"body": {
|
||||
"data": {
|
||||
"enabled": true,
|
||||
"coverage_start": "2026-01-01",
|
||||
"coverage_end": "2026-12-31",
|
||||
"message": "IVA geral sobre base líquida disponível; enquadramento declarado."
|
||||
}
|
||||
}
|
||||
},
|
||||
"/investimentos": {
|
||||
"status": 302,
|
||||
"location": "https://teste-investidor.tempo.co.mz/login"
|
||||
},
|
||||
"/simulador-fiscal": {
|
||||
"status": 200,
|
||||
"body": "HTML available"
|
||||
},
|
||||
"checked_utc": "2026-10-22T08:17:20.250804+00:00"
|
||||
},
|
||||
"configuration_before": {
|
||||
"read_only": true,
|
||||
"utc": "2026-10-22T08:16:34.265947+00:00",
|
||||
"revision": {
|
||||
"commit": "042ffbde44cac27704d6f8b7ec25b525a42e3575",
|
||||
"branch": "main",
|
||||
"built_at": "2026-10-22T08:16:14.331119+00:00",
|
||||
"manifest_sha256": "655eef6e6a5e71c5138c68465a5b2ffc36d2cadeff96e6b35682dbfd19d42e79"
|
||||
},
|
||||
"live": "/var/www/investmoz-homologacao/releases/042ffbde44cac27704d6f8b7ec25b525a42e3575-6dfae60c",
|
||||
"env_path": "/var/www/html/teste-investidor.tempo.co.mz/.env",
|
||||
"env_sha256": "7cdaba591bb5787485a349c24db2fd7c8279436da0eb31502e44db2fffa21d03",
|
||||
"env_mode": "0o640",
|
||||
"uid": 1001,
|
||||
"gid": 33,
|
||||
"private_preparation_env": "unset",
|
||||
"config": {
|
||||
"private_preparation": false,
|
||||
"investor_path": true,
|
||||
"fiscal_consumer": {
|
||||
"enabled": true,
|
||||
"coverage_start": "2026-01-01",
|
||||
"coverage_end": "2026-12-31",
|
||||
"message": "IVA geral sobre base líquida disponível; enquadramento declarado."
|
||||
},
|
||||
"preparation_version": "private-investment-preparation-2026-v1"
|
||||
}
|
||||
},
|
||||
"configuration_enable": {
|
||||
"utc": "2026-10-22T08:16:49.343216+00:00",
|
||||
"revision": {
|
||||
"commit": "042ffbde44cac27704d6f8b7ec25b525a42e3575",
|
||||
"branch": "main",
|
||||
"built_at": "2026-10-22T08:16:14.331119+00:00",
|
||||
"manifest_sha256": "655eef6e6a5e71c5138c68465a5b2ffc36d2cadeff96e6b35682dbfd19d42e79"
|
||||
},
|
||||
"changed": true,
|
||||
"env_before_sha256": "7cdaba591bb5787485a349c24db2fd7c8279436da0eb31502e44db2fffa21d03",
|
||||
"env_after_sha256": "a6b4e9064cf6dbb2d945b53418a6c9252f9c736ff3fde4016b95b74885a9ee0f",
|
||||
"backup": "/var/lib/investmoz-deploy/backups/private-preparation-20261022/env-7cdaba591bb5787485a349c24db2fd7c8279436da0eb31502e44db2fffa21d03.private",
|
||||
"backup_bytes_verified": true,
|
||||
"only_key_changed": "PRIVATE_INVESTMENT_PREPARATION_ENABLED",
|
||||
"mode_preserved": true,
|
||||
"owner_preserved": true,
|
||||
"config": {
|
||||
"private_preparation": true,
|
||||
"investor_path": true,
|
||||
"fiscal_consumer": {
|
||||
"enabled": true,
|
||||
"coverage_start": "2026-01-01",
|
||||
"coverage_end": "2026-12-31",
|
||||
"message": "IVA geral sobre base líquida disponível; enquadramento declarado."
|
||||
},
|
||||
"preparation_version": "private-investment-preparation-2026-v1"
|
||||
},
|
||||
"fiscal_configuration_changed": false,
|
||||
"database_operations": false,
|
||||
"recovery": "Restore previous key value only against recorded post-change hash, rebuild config cache; preserve subsequent changes and all data"
|
||||
},
|
||||
"configuration_after": {
|
||||
"read_only": true,
|
||||
"utc": "2026-10-22T08:17:21.883131+00:00",
|
||||
"revision": {
|
||||
"commit": "042ffbde44cac27704d6f8b7ec25b525a42e3575",
|
||||
"branch": "main",
|
||||
"built_at": "2026-10-22T08:16:14.331119+00:00",
|
||||
"manifest_sha256": "655eef6e6a5e71c5138c68465a5b2ffc36d2cadeff96e6b35682dbfd19d42e79"
|
||||
},
|
||||
"live": "/var/www/investmoz-homologacao/releases/042ffbde44cac27704d6f8b7ec25b525a42e3575-6dfae60c",
|
||||
"env_path": "/var/www/html/teste-investidor.tempo.co.mz/.env",
|
||||
"env_sha256": "a6b4e9064cf6dbb2d945b53418a6c9252f9c736ff3fde4016b95b74885a9ee0f",
|
||||
"env_mode": "0o640",
|
||||
"uid": 1001,
|
||||
"gid": 33,
|
||||
"private_preparation_env": "true",
|
||||
"config": {
|
||||
"private_preparation": true,
|
||||
"investor_path": true,
|
||||
"fiscal_consumer": {
|
||||
"enabled": true,
|
||||
"coverage_start": "2026-01-01",
|
||||
"coverage_end": "2026-12-31",
|
||||
"message": "IVA geral sobre base líquida disponível; enquadramento declarado."
|
||||
},
|
||||
"preparation_version": "private-investment-preparation-2026-v1"
|
||||
}
|
||||
},
|
||||
"effective_procedural_version": "private-investment-preparation-2026-v1",
|
||||
"new_fiscal_rules_published_or_activated": false,
|
||||
"qa_fixtures_copied": false,
|
||||
"accepted_scenarios_repeated_manually": false,
|
||||
"limitations": "No new scenario executed in homologation and no real dossier or personal data created. QA functional evidence reused; destination verification limited to revision, configuration and entry routes. Sector/fiscal exclusions remain explicit."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"functional_candidate": "1bcf34c0",
|
||||
"installation_procedure": "e46f9731",
|
||||
"installed_revision": "f4d0970917e7e8d141f74c136b66138b929141fc",
|
||||
"activated_utc": "2026-10-22T20:51:27.874333+00:00",
|
||||
"counts": {
|
||||
"tenders": 1,
|
||||
"versions": 1,
|
||||
"published_versions": 0,
|
||||
"watches": 0
|
||||
},
|
||||
"verification_draft": {
|
||||
"id": "b970f0f0-9257-42e1-8365-3358237b55db",
|
||||
"created_by": 15,
|
||||
"version": 1,
|
||||
"published_version_id": null
|
||||
},
|
||||
"verification": {
|
||||
"real_account_id": 15,
|
||||
"mfa_renewed_by_user": true,
|
||||
"management_hub": true,
|
||||
"draft_saved_recovered": true,
|
||||
"preview_opened": true,
|
||||
"publication_performed": false,
|
||||
"qa_proofs_reused": "8 tests / 285 assertions; 6 navigation tests; build and TypeScript"
|
||||
},
|
||||
"links": {
|
||||
"management": "https://teste-investidor.tempo.co.mz/gestao-conteudos",
|
||||
"catalogue": "https://teste-investidor.tempo.co.mz/concursos"
|
||||
},
|
||||
"backup": {
|
||||
"database_sha256": "69de817b288ecb25db4b341375475f2408a47642a8ba69d338b217dcc982854f",
|
||||
"files_sha256": "7b75c22e730b5600d6e97e1c6dbb4cd93982bdd47e9dd2a63de2359974132674",
|
||||
"tables_restored_verified": 171,
|
||||
"files_archive_verified": 341
|
||||
},
|
||||
"preservation": {
|
||||
"protected_tables_unchanged": [
|
||||
"users",
|
||||
"portal_role_assignments",
|
||||
"documents",
|
||||
"portal_resources",
|
||||
"opportunities",
|
||||
"geo_locations",
|
||||
"geo_geometries"
|
||||
],
|
||||
"original_files_preserved": 340
|
||||
},
|
||||
"private_document_anonymous_status": 404
|
||||
}
|
||||
@@ -1,5 +1,19 @@
|
||||
# Rastreabilidade, utilização e formação
|
||||
|
||||
## Guia curto — entrada única de gestão
|
||||
|
||||
**Link da equipa:** https://teste-investidor.tempo.co.mz/gestao-conteudos. Entre com a conta habitual e o seu MFA. As funções e âmbitos aprovados determinam os botões apresentados; não é necessário acesso à linha de comandos.
|
||||
|
||||
| Trabalho | Percurso pela interface |
|
||||
| --- | --- |
|
||||
| Oportunidades | **Nova oportunidade** → escolher promotor real e modalidade → preencher → **Guardar rascunho**. Recuperar em **Gerir oportunidades e histórico**; abrir a ficha, completar **Imagens, documentos e localizações** e **Submeter para validação**. O Validador usa a fila de revisão e as acções **Iniciar validação**, **Guardar revisão especializada**, pedir correcções ou **Aprovar versão**, conforme a tarefa atribuída. O Publicador usa a fila de publicação e **Publicar no catálogo público** na versão aprovada com visibilidade pública. A equipa conclui as funções que já lhe foram atribuídas; a criação não concede revisão nem publicação. |
|
||||
| Documentos | **Novo documento** → escolher ficheiro e origem conhecida → **Guardar em quarentena** → conferir a análise indicada no ecrã → completar **Registar documento na biblioteca** → **Guardar como rascunho**. Recuperar pela pesquisa em **Gerir conteúdos, versões e histórico**; usar **Editar metadados** para rever; guardar as alterações e usar **Publicar**. Se o scanner comunicar falha, usar **Tentar novamente**; avaria persistente é manutenção técnica, não uma nova aprovação documental. |
|
||||
| Legislação | **Nova legislação** → mesmo percurso documental, com tipo Legislação pré-seleccionado → registar entidade, número, edição, fonte, datas e situação conhecida → guardar → pesquisar/reabrir → rever metadados/associações → **Publicar**. Fonte oficial identificada mantém a aceitação da Tempo; não exige segunda certificação. A publicação permite consulta e não activa regras fiscais. |
|
||||
|
||||
Para retirar um documento, abrir o registo e usar **Retirar da publicação**; original e histórico ficam preservados. Para oportunidades, usar as acções de suspensão/arquivo apresentadas à função responsável. Não se apagam registos para os retirar do público.
|
||||
|
||||
Este guia descreve os percursos existentes; a instalação de código é manutenção pelo processo Git automático. Contas ainda não criadas conservam as designações aprovadas e não suspendem os acessos das contas existentes. Conferência real desta instalação registada na secção de entrega, abaixo; o guia não substitui essa evidência.
|
||||
|
||||
## Parques — disponibilizados em18/10/2026
|
||||
|
||||
Revisão `parques-integral-reconciliado-639ed9624a48` disponibilizada em `https://portal-investidor.test`, com autorização expressa da Tempo. Sete fontes (cinco funcionais e dois auxiliares gerados) e170 ficheiros de build; nenhuma migration executada. L1/140000, navegação/150000 e editorial160000 preservados. Backup/restauro fresco e preservação de dados conferidos. Os dados reais desconhecidos mantêm-se desconhecidos; não se encerram todos os requisitos da ficha pelo nome integral.
|
||||
@@ -2041,3 +2055,429 @@ Limites concretos: este é o primeiro incremento utilizável de continuidade e e
|
||||
Revisão: abrir o simulador QA com o dossier A `92d0836c-0ffb-4e07-a7f5-0e8ca3f594b9` ou B `1872412f-251a-4f6d-bf18-70388e71a16a`; usar a conta sintética QA existente, nunca actores sintéticos na homologação. O router privado de QA usa a instalação já existente do percurso, activada apenas neste ensaio isolado; não há migration ou nova exigência de funções. Entregar a candidata para revisão pelo processo Git existente, preservando alterações concorrentes. Uma integração futura exige rever o delta relativamente a main e o estado de instalação/configuração do percurso no destino; não publicar regras ou alterar interruptores fiscais como consequência desta correcção. Preservar todos os históricos na suspensão ou reversão de código.
|
||||
|
||||
Fecho da observação deste incremento: o regresso A ao dossier gravou a evidência financeira `43317c22-50c8-40cf-98e4-e5d4d108c372`, rascunho `cf4dec46-c96c-4aa6-aa9c-1a8d6ecb1bea` v8, contexto v1, com hash preservado. A ligação de inspecção do Edge interrompeu-se depois da navegação; o inventário mostrou o endereço do mesmo dossier e a leitura autenticada do serviço confirmou a gravação. A parte visual final do regresso não foi observada e fica como limitação da ferramenta, não falha do simulador. B recuperou contexto v2 e gravou rascunho v6, mantendo USD e sem execução fiscal. O relatório gerado destes dados recuperados conserva a versão do parque e benefício indeterminado. O enriquecimento automático do rascunho privado é exclusivo do proprietário autenticado; o acesso de gestão ao dossier não concede por esta alteração acesso ao rascunho privado da conta.
|
||||
|
||||
|
||||
### Revisão de a76369eb e primeiro percurso adaptativo fundamentado — v1, 22/10/2026
|
||||
|
||||
**Revisão encerrada no âmbito de a76369eb, sem integração.** O último regresso tem prova equivalente: destino do mesmo dossier observado pelo inventário do navegador e evidência `43317c22-50c8-40cf-98e4-e5d4d108c372` confirmada no serviço, ligada ao rascunho v8/contexto v1 e ao hash `a21fa5779bf8b8df366f16683c8a9bb9817a6d2921c7384f85df02135307fa13`. A observação visual final continuou limitada pela ferramenta. Não se repetiram A/B, cálculos, exportações ou nova sessão; reutilizam-se as provas aceites. Leitura dirigida do delta: isolamento por proprietário/dossier, edições/apagamentos, origem monetária, controlo de escrita e ausência de publicação fiscal. Não foi identificada nova preparação técnica que condicione a decisão de integração deste âmbito. A adaptação jurídica seguinte é independente.
|
||||
|
||||
**Percurso delimitado preparado:** preparação de um pedido de investimento privado para empreendimento económico em Moçambique, com actividade concreta declarada, instalação identificada e operações mensais enquadráveis no consumidor IVA geral existente. Trata a preparação do pedido de investimento; não se apresenta como licenciamento sectorial completo, certificado emitido, direito a incentivo ou submissão à Administração. Investidor nacional/estrangeiro, actividade, modalidade e localização são condições distintas. A saída é formulário orientado, etapas/documentos aplicáveis e simulação identificada pelo seu âmbito. Não se escolhe “Turismo” ou uma ZEE para presumir requisitos ou benefícios.
|
||||
|
||||
**Fonte primária aplicada:** compilação oficial APIEX “Legislação de Investimentos em Moçambique”, exemplar obtido de https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf — 6467655 bytes, SHA256 `ba1e9eaabfec5d0c4cb2d9f2444a6860f7a90f222034764197829be402d408ed`. Contém Lei8/2023 (pp.8–32), Decreto8/2024/Regulamento (pp.36–59), CBF (pp.64–92). Aceitação oficial de08/10 mantida; não há exigência de segunda autenticação ou aprovação jurídica da Tempo. Inventários existentes de604 registos/exemplares/anexos e9901 caminhos do acervo reutilizados antes da selecção; cópia privada, texto extraído e citações em `outputs/adaptive-path-20261022`. Não importar/publicar automaticamente. As pendências históricas genéricas de nova certificação nos levantamentos de28/09 e03/10 não são gates deste lote.
|
||||
|
||||
**Período:** Decreto8/2024, artigo5 do diploma preambular, indica aplicação desde a publicação de7/03/2024; Lei8/2023, artigo32, indica90dias após publicação. O lote proposto cobre datas de projecto em2026 e preserva estes inícios, exemplar e versão. Não foi encontrada alteração nas consultas oficiais dirigidas desta execução; isso não certifica inexistência de actos posteriores. A compilação de2025 não constitui consolidação universal de2026. Na ligação executável, uma fonte/versão sem cobertura temporal não autoriza fallback para a versão mais recente. Os artigos citados são do **Regulamento anexo** quando identificado “R”; “L” designa a Lei8/2023. Rastrear esta distinção evita confundir os dois artigos9 ou o artigo5 preambular com o do Regulamento.
|
||||
|
||||
| Condição | Fonte e período | Efeito no formulário | Efeito no percurso | Efeito no cálculo | Informação em falta / estado da ligação |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Projecto económico em Moçambique que procura garantias/incentivos; actividade efectiva conhecida | L2(1)–(3), pp.8–9; período2026 deste lote | Reutilizar localização e actividade; perguntar a finalidade jurídica apenas se ausente | S02 identifica cobertura do procedimento; actividades excluídas recebem encaminhamento próprio | Não inferir benefício ou exclusão de IVA a partir deste âmbito | Falta ligação executável da cobertura ao perfil; legislação sectorial especial fica fora deste lote |
|
||||
| Capital/recursos do exterior ou nacionais; pessoa, residência e constituição separadas | L3/anexo, pp.28–29; R4–6, pp.38–40; período2026 | Reutilizar os dados declarados; pedir origem/forma do aporte quando faltar, sem transformar nacionalidade em classificação do capital | Acrescentar preparação da prova/registo do aporte externo quando aplicável | Nenhuma mudança automática de taxa IVA ou conversão; transferências cambiais não calculadas | Falta dado estruturado de aporte e ligação à documentação; requisitos cambiais concretos não abrangidos |
|
||||
| Implementadora em constituição ou já registada | R9(1)(b), p.41; R14(1)(b), p.45; desde7/03/2024, recorte2026 | Mostrar a alternativa documental correspondente, sem exigir novo preenchimento da entidade | S05/S06: reserva de denominação ou certidão comercial, com estado declarado/associado por conferir | Não altera IVA | Falta associar categorias documentais existentes à alternativa e ao estado do cadastro |
|
||||
| Implantação em instalações arrendadas ou com DUAT | R9(1)(c)–(d), pp.41–42; R14(1)(c)–(d), p.46; recorte2026 | Reutilizar local; perguntar apenas a modalidade/evidência ainda ausente | S06 identifica esboço/planta e alternativa contrato/DUAT; sem converter ausência de título em ausência de direito | Não atribui taxa, custo fundiário ou benefício | Falta modalidade estruturada e associação ao documento; sem decidir titularidade/disponibilidade |
|
||||
| Pedido por representação comercial estrangeira, distinto de qualquer investidor estrangeiro | R9(2)–(3), p.42; recorte2026 | Pergunta de representação apenas quando a implantação declarada o permita; depois referência da licença e documento | S06 acrescenta licença de representação e identifica formalidades documentais pertinentes | Sem alteração da taxa geral | Falta modalidade e origem documental estruturadas; não exigir licença a todo investidor estrangeiro |
|
||||
| Nenhuma condição de autorização; ou dimensão/actividade/terra que a exija | L22(1)(b)–(2), pp.20–21; R13(2), R14 e R21, pp.45–50; recorte2026 | Reutilizar montante/moeda/actividade/área; perguntar apenas PPP/concessão, implicações ou dados ainda desconhecidos | Seleccionar preparação de mero registo ou autorização; na segunda, S02/S06 acrescentam estudo de viabilidade e capacidade financeira | Comparação do montante exige MZN ou equivalência documentada; não converter nem inventar taxa; IVA permanece componente separada | Falta ligar os predicados e evidências ao perfil. “Não sei” não equivale a “não”. Limite de32mil milhões MZN só resolve a condição de montante, não todas as restantes |
|
||||
| Localização geral versus ZEE/ZFI com referência e âmbito documentados | R13(1)(a)–(b), p.45; R10(4), p.43; recorte2026 | Reutilizar zona/versionamento; pedir reconhecimento/condições pertinentes em falta | Diferenciar entidade de encaminhamento e articulação aplicável; S08/S17 conservam evidências e pendências | Sem regra executável do benefício, mostrar resultado especial indeterminado e cenário geral separado | a76369eb já apresenta referência/limite; faltam ligações jurídicas de encaminhamento e condições. Parque industrial não equivale automaticamente a ZEE/ZFI |
|
||||
| Operação mensal interna geral, base líquida, MZN e período coberto pela versão publicada | Regra IVA já fundamentada/autorizada; Lei22/2022 art.17(1), fontes/limites no recibo existente; recorte fiscal2026 | Reutilizar vendas/mês/moeda; perguntar apenas pressupostos fiscais ainda ausentes | Ligar cálculo e relatório à mesma versão de contexto/regra | Consumidor existente calcula por versão servida; zero difere de falta; preço com IVA incluído não vira base líquida | Ligação mensal já demonstrada. Dedução, reembolso, saldo final e benefício especial continuam fora |
|
||||
| Mudança de título/contacto ou outra condição irrelevante para a regra aplicada | Decisão Tempo22/10 e contratos existentes; escolha técnica de dependências identificada | Preservar a edição e preencher o que continuar compatível | Não reiniciar revisões/etapas sem dependência relevante | Não criar outra execução nem mudar taxa/base | Prova de título e testes de dependências já aceites; estender por dependência, não por “qualquer alteração” |
|
||||
|
||||
**Diferença útil que a implementação deste lote deve demonstrar, sem repetir A/B:** um caso C exclusivamente sintético, empresa em constituição, actividade económica não extractiva declarada, instalações arrendadas, capital emMZN, período2026. Primeiro completar as condições de enquadramento; depois a preparação de mero registo mostra a alternativa reserva de denominação/arrendamento. Alterar a condição de autorização para verdadeira troca o resultado procedimental, os campos necessários e os documentos para estudo/capacidade financeira. Com condição desconhecida, a selecção fica indeterminada e pede apenas essa informação. Alterar para representação comercial estrangeira acrescenta a licença pertinente; mudar apenas nacionalidade não produz esse efeito. Alterar de localização geral para ZEE/ZFI documentada altera o encaminhamento e as verificações, sem conceder benefício. O IVA geral continua matematicamente igual quando a mudança não afecta a sua regra; mudar vendas/base/moeda/período altera ou impede o cálculo conforme a cobertura existente. Não forçar um resultado numérico diferente quando a lei não o determina.
|
||||
|
||||
**Cadastro e código separados:** o conteúdo jurídico acima está preparado, com artigos, exemplar e início temporal. Falta cadastrar as correspondências versionadas campo/condição/etapa/documento e ligar a sua execução ao perfil existente. Usar o mesmo dossier, prévia/confirmar, definições/questionários versionados e suporte condicional `when` já utilizado nos formulários; reutilizar resolvedor temporal, avaliação e aritmética exacta nas interfaces em que sejam compatíveis, com adaptador explícito para os predicados procedimentais, sem criar outro motor nem armazenar procedimentos como imposto. O contexto já tem sector/actividade/localização/moeda/período/entidade; as respostas de modalidade e evidências ainda não ligadas devem ser acrescentadas ao mesmo contexto cifrado, com origem e alterações deliberadas. Reutilizar vínculos documentais/históricos e permissões existentes. Não alterar a definição publicada dos19passos nem activar outra regra fiscal para executar este cadastro.
|
||||
|
||||
**Estado desta entrega:** revisão de a76369eb concluída no âmbito aceite; **percurso seguinte fundamentado e delimitado, ainda não implementado/demonstrado de ponta a ponta**. A tabela não é prova de funcionalidade nem declara licenciamento sectorial completo. Não integrar este documento como se instalasse o percurso. Próxima execução técnica concreta: ligar as condições acima, persistir decisões/documentos e verificar apenas as transições de C, recuperação e relatório; conservar as provas aceites de contexto e IVA. Código, publicação de parâmetros/regras e activação continuam operações separadas. Não há nova autorização organizacional ou validação jurídica da Tempo presumida.
|
||||
|
||||
|
||||
### Percurso de investimento privado executável em QA — 22/10/2026
|
||||
|
||||
Implementação do delta de `52fbb146`, na branch `codex/private-investment-path-executable-QA`. A integração independente de `a76369eb` não foi repetida nem condicionada por este lote. Não houve escrita na homologação, integração em main, publicação ou activação de regras fiscais.
|
||||
|
||||
**Orientação permanente aplicada:** decisão expressa da Tempo de 22/10, registada também em AGENTS.md: legislação, modelos e procedimentos oficiais disponibilizados são a base aceite para implementar; decisões da Tempo orientam a implementação. Não existe nova aprovação jurídica genérica, aprovador adicional ou validação de credibilidade como condição para executar este percurso. Dados concretamente em falta são identificados por campo e não anulam os restantes requisitos. Associação documental não equivale a reconhecimento administrativo; esta distinção é informativa, sem bloquear a gravação ou a preparação.
|
||||
|
||||
**Ligação executável:** `PrivateInvestmentPreparation` é o adaptador delimitado das condições procedimentais já documentadas para o contexto cifrado do dossier. Correspondência `private-investment-preparation-2026-v1`, exemplar APIEX/hash anteriormente registados, recorte de início previsto em 2026. Reutiliza campos conhecidos do mesmo projecto/rascunho, permissões, vínculos documentais, prévia assinada, controlo de concorrência, chave idempotente, eventos antes/depois e os 19 passos existentes. Não é outro motor fiscal, catálogo paralelo de benefícios ou novo processo de aprovação. O resolvedor fiscal publicado e a aritmética decimal existentes continuam responsáveis pelos seus cálculos; não se armazenaram predicados procedimentais como impostos.
|
||||
|
||||
No dossier normal, «Preparar o pedido de investimento privado» abre o editor de contexto. Valores já declarados ficam recuperados e editáveis, sem nova confirmação por ecrã. Dados condicionais de área só aparecem para extensão de terra/concessão florestal. A prévia mostra ramo antes/depois, documentos novos/inactivos, passos afectados e resultados que precisam de revisão. Apenas a confirmação grava uma revisão; cancelamento conserva o estado anterior. Os requisitos correspondentes aparecem nas etapas S02/S05/S06 e o próximo passo acompanha mero registo/autorização. As dependências formais da definição publicada não foram substituídas por novos bloqueios.
|
||||
|
||||
**Fundamento executado:** Lei 8/2023 artigos 2 e 22; Regulamento aprovado pelo Decreto 8/2024 artigos 5, 9, 13, 14 e 21, conforme a tabela de 52fbb146 acima. Na interface, R designa este Regulamento. As exigências têm razão, artigo, período e ligação ao exemplar oficial. As alternativas certidão/reserva e DUAT/arrendamento não são cumulativas. Representação estrangeira determina a licença correspondente; nacionalidade ou residência, isoladamente, não a determinam. Aporte externo tem origem declarada separadamente da nacionalidade. Montante >32 mil milhões MZN, terra >=10 mil ha e concessão florestal >100 mil ha usam comparação decimal exacta, sem conversão cambial presumida. R21 distingue Finanças/Conselho de Ministros conforme as condições efectivamente conhecidas; uma condição positiva suficiente não espera pelas restantes para identificar o pedido de autorização.
|
||||
|
||||
| Comparação nova, exclusivamente QA | Campos e documentos | Próximo passo / efeito no cálculo | Prova |
|
||||
| --- | --- | --- | --- |
|
||||
| C: empresa em constituição, actividade geral, 1.000.000 MZN, 01/01/2026, modalidade doméstica, capital nacional, instalações arrendadas; condições de autorização negativas | Reserva de denominação, identificação, planta e arrendamento; não exige certidão, DUAT, licença de representação ou estudo/capacidade de autorização | Mero registo; encaminhamento Governador. Nenhuma taxa IVA alterada | UI → prévia → confirmar; contexto v1 |
|
||||
| C: associar suporte sintético já existente ao arrendamento | Mesmo documento associado por ID; não cria cópia nem o aprova | Conserva o pedido; contexto v2 | UI; um vínculo em desk_documents |
|
||||
| C: PPP/concessão empresarial passa a sim, modalidade passa a representação estrangeira e instalações a DUAT | Entram estudo/capacidade, licença de representação e DUAT; arrendamento deixa de ser exigido, conservando o documento anterior | Autorização, Conselho de Ministros; S02/S05/S06/S07 para revisão. Esta mudança meramente procedimental não invalida os inputs do IVA | Prévia visual antes de confirmar; contexto v3 recuperado; PNG C-authorization-preview |
|
||||
| C: recuperação e simulador | MZN, data, título, contexto v3 e preparação por autorização recuperados sem reintrodução; arrendamento inactivo visível no histórico | Mesmo âmbito fiscal geral; nenhuma execução fiscal nova pedida neste ensaio | Dossier normal, simulador e rascunho ligado; leitura persistida e relatório Word |
|
||||
| Regresso autorização → mero registo; entidade em constituição → existente | Estudo/capacidade/licença deixam de ser exigidos; arrendamento anterior volta associado; certidão substitui reserva, sem apagar histórico | Ramo e requisitos recalculados | Controlo HTTP transaccional dirigido, não repetição visual A/B |
|
||||
| ZEE/ZFI versus parque industrial | Encaminhamento APIEX condicionado ao enquadramento reconhecido; parque não é convertido em ZEE/ZFI | Nenhum benefício ou taxa especial concedidos | Predicados com referências sintéticas de QA; não prova de elegibilidade de uma zona real |
|
||||
|
||||
**Provas novas:** três testes feature, 96 asserções, exclusivamente na base `simulator_fiscal_delta_20261021_test`, loopback 55441, com transacções revertidas e guarda do alvo. Cobrem limiares exactos, desconhecidos, exclusões/período, não inferência por nacionalidade, alternativas e preservação de vínculo, recuperação, idempotência, prévia sem escrita, adulteração, documento de outro dossier recusado e consumidor desligado, incluindo edição independente do título sem perder snapshot ou vínculos. Um teste frontend confere somente o novo conteúdo congelado do relatório. TypeScript e build passaram. Não foram repetidos cálculos decimais, exportadores completos, paginação PDF, CSV ou ensaios A/B aceites.
|
||||
|
||||
**Recibo C da fase por autorização (histórico v3):** dossier `4133311b-5426-4cec-900d-1f1484484083`; contexto v3; rascunho `beba1dec-fc05-4820-b099-24952c5ab36c`, v4, SHA256 `4136e82a3b9420ab8f980ec0d7bda6dc2a67b568c18535b5ba0ecc80efd37e37`. Um documento sintético `e5ce142a-1178-46a2-b039-f187139c62cd`, sem pretensão de autenticidade oficial ou reconhecimento; mantém quarentena/scan pendente, sem contornar o carregamento/antimalware. A fixture apenas disponibiliza metadados privados para observar associação, não ensaia novamente uploads e não faz parte de dados/migrações de entrega. Três eventos de mudança do contexto; uma associação documental. C não contém projecção de vendas nem novo resultado fiscal; as provas anteriores dessa componente são reutilizadas.
|
||||
|
||||
**Relatório efectivamente transferido:** `relatorio-investmoz-sintetico-qa-c-pedido-de-investimento-privado.docx`, 16.674 bytes, SHA256 `28cfe4b75116c892d3e29fde389de7d067b456761756bab732af625178ee69ce`. Conferido o novo conteúdo: pedido de autorização, versão, fundamentos R14 e exigências não aplicáveis com histórico preservado. Não se repetiu a verificação de paginação/exportadores. Os anexos do dossier não são inseridos silenciosamente no pacote; mantém-se a selecção explícita existente. O relatório identifica a preparação/simulação e não alega submissão ou aceitação administrativa.
|
||||
|
||||
**Limitação da ferramenta:** a observação do evento de download excedeu o tempo e a extensão perdeu a ligação. O ficheiro terminou e foi encontrado em Transferências antes de qualquer repetição; conteúdo/hash conferidos localmente. Não se repetiu a exportação nem se classificou o incidente como falha do simulador. A prévia de impacto e recuperação já tinham prova visual; a recepção do ficheiro é prova distinta.
|
||||
|
||||
**Limites concretos:** este lote prepara o pedido de garantias/incentivos de investimento privado, não executa submissão administrativa, licenciamento completo por sector ou decisão de elegibilidade. Prospecção/pesquisa/produção extractiva, investimento público/exclusivamente social sem fins lucrativos e períodos fora de 2026 não são cobertos por este adaptador; processamento industrial tem o ramo de autorização documentado. A equivalência de montante noutra moeda não é determinada sem dados/fundamento cambial aplicáveis; condições positivas independentes continuam executáveis. O catálogo visual deste QA contém apenas parques sintéticos, não uma ZEE/ZFI real elegível: encaminhamento especial conferido no predicado, sem declarar prova visual de reconhecimento real. Benefícios territoriais, deduções, reembolsos e apuramento final do IVA continuam indisponíveis; o cálculo geral já autorizado conserva o seu âmbito.
|
||||
|
||||
**Utilização e integração:** abrir `http://127.0.0.1:18580/investimentos/4133311b-5426-4cec-900d-1f1484484083` na conta QA habitual. Usar «Comparar e alterar contexto», editar apenas a condição relevante, comparar e confirmar; para documentos, usar o Balcão do mesmo dossier e associar os existentes. QA habilita `investor-path.private_preparation=true` no router privado. A configuração de entrega `PRIVATE_INVESTMENT_PREPARATION_ENABLED` fica desligada por defeito; a homologação não foi alterada. Integração eventual pelo processo Git existente: rever branch/código e limites, incorporar sobre main actualizado preservando alterações concorrentes, build e conferir apenas o delta no destino; habilitação deste percurso é uma configuração separada, não publicação/activação fiscal. Suspender a configuração oculta o novo consumidor e recusa novas escritas específicas, sem eliminar contexto, documentos, eventos, rascunhos ou relatórios congelados. Sem migração ou seed de fixtures no destino. Nenhuma nova ronda de preparação técnica foi identificada para este recorte; revisão/decisão de integração continuam separadas.
|
||||
|
||||
|
||||
**Distinção final conferida, sem nova validação genérica:** Lei 8/2023 artigo 14(b) distingue investimento directo/indirecto; R13 refere expressamente investimento directo nos limiares monetários. Acrescentado o dado «Tipo deste investimento», separado da origem/nacionalidade. Sem esse dado, a via dependente da dimensão permanece indeterminada; para forma indirecta este lote não determina dimensão/encaminhamento monetário, mas continua a avaliar as condições independentes documentadas. PPP/concessão positiva continua a determinar autorização e Conselho de Ministros, sem esperar por esse campo irrelevante à decisão já suficiente. A interface foi conferida: retirar a condição PPP pede exactamente o tipo em falta; declarar directo resolve mero registo e retira estudo/capacidade financeira. Comparação antes de confirmar em `C-final-directness-preview.png` e percurso recuperado em `C-final-path.png`.
|
||||
|
||||
Estado final C: dossier v8, contexto **v4**, investimento directo declarado, PPP não, pedido de **mero registo**; quatro eventos de contexto e um documento/vínculo conservados. A modalidade estrangeira/DUAT conservou a edição deliberada. O rascunho v4 e o Word já transferido mantêm o contexto **v3 por autorização**, com hash acima: são prova histórica congelada e não o relatório da revisão v4. Não houve reescrita silenciosa nem reexportação. Ao abrir o simulador, o mecanismo comum de continuidade disponibiliza a revisão actual para o próximo rascunho/relatório; as provas anteriores deste mecanismo continuam reutilizadas.
|
||||
|
||||
|
||||
### Correspondência do novo relatório com o contexto v4 e dependências Git — 22/10/2026
|
||||
|
||||
Verificação dirigida solicitada pela Tempo após a avaliação do resumo de `a1fbda06`; nenhuma alteração funcional necessária. A prova anterior só demonstrava o Word congelado v3. Nesta continuação abriu-se o simulador pelo link normal do mesmo dossier C (incluindo o rascunho ligado), recuperou-se automaticamente o contexto v4 e gerou-se **uma** saída Word nova. Não se repetiram cálculos, CSV, paginação, anexos ou ensaios A/B aceites; não houve nova validação jurídica, integração ou alteração fiscal.
|
||||
|
||||
| Elemento | Word histórico v3 | Novo Word do contexto v4 |
|
||||
| --- | --- | --- |
|
||||
| Identificação dentro do documento | «Revisão 3» e «Contexto congelado v3» | «Revisão 4» e «Contexto congelado v4» |
|
||||
| Pedido | Autorização | Mero registo |
|
||||
| Estudo de viabilidade | Documento por associar | Não aplicável; histórico preservado |
|
||||
| Capacidade financeira | Documento por associar | Não aplicável; histórico preservado |
|
||||
| Bytes / SHA256 | 16.674 / `28cfe4b75116c892d3e29fde389de7d067b456761756bab732af625178ee69ce` | 16.678 / `a2849427f4a91fa6ab78e433be8b919baca80a0f4f452a4546a5b169b921d753` |
|
||||
| Cópia identificada para revisão | `outputs/private-path-executable-20261022/C-report-HISTORICO-contexto-v3.docx` | `outputs/private-path-executable-20261022/C-report-contexto-v4.docx` |
|
||||
|
||||
O original v3 conserva exactamente os bytes/hash anteriores; a cópia está explicitamente designada **HISTÓRICO**. A revisão congelada no próprio Word identifica o contexto a que pertence, sem o transformar no estado actual. O novo ficheiro foi encontrado em Transferências após o clique de exportação e conferido por leitura das células efectivamente geradas; recibo em `C-report-v3-v4-comparison.json`. O rascunho passou a v5, SHA256 `ba824fe0362ca87b301da55298122796f44fdfb11507830b3fe8f54cf88d132d`, com contexto v4 e preparação por mero registo; dossier v8/contexto v4 e um documento mantidos. Prova visual de recuperação em `C-new-report-context-v4.png`. Não se altera a validade histórica da prova v3.
|
||||
|
||||
**Dependências para integração após a avaliação da Tempo:** ancestralidade confirmada `57fc9148 → a76369eb → 52fbb146 → a1fbda06`. `a76369eb` fornece a continuidade comum do contexto/edições e é dependência funcional de `a1fbda06`; pode ser integrado independentemente. `52fbb146` é a preparação documental intermédia, sem código executável. A branch `codex/private-investment-path-executable-QA` inclui essa ancestralidade e a presente prova documental; não aplicar apenas o diff de `a1fbda06` num destino sem a continuidade ou alteração equivalente. Se a76369eb já tiver sido integrado, o merge normal reconhece a ancestralidade e não o reaplica como um segundo desenvolvimento.
|
||||
|
||||
Procedimento: após a decisão, recuperar o main actual, rever a comparação e fazer integração Git que preserve as alterações concorrentes; resolver apenas conflitos efectivos, sem reset/force-push de main. A leitura desta continuação encontrou main em `57fc914821d0cefa02caa80a74b3d42e663f2742`; é fotografia do estado, não autorização para ignorar commits posteriores. Conferir a revisão servida e só o delta no destino. A habilitação `PRIVATE_INVESTMENT_PREPARATION_ENABLED` é configuração distinta; não publica nem activa regras fiscais. Nesta execução a branch permanece separada, sem integração/habilitação na homologação. Limitações sectoriais/fiscais explícitas mantêm âmbito próprio e não bloqueiam este recorte concluído.
|
||||
|
||||
|
||||
### Integração autorizada do percurso adaptativo — 22/10/2026
|
||||
|
||||
A Tempo autorizou a implementação após o fecho do recorte. Main remoto foi actualizado por avanço directo de `57fc9148` para `064296e1`, preservando a sequência `a76369eb → 52fbb146 → a1fbda06 → 064296e1`; não havia alteração concorrente ou conflito no momento do envio. A cadeia automática recusou a primeira tentativa no teste frontend «server draft snapshots split private identity from economic values»: 188/189 passaram. A versão servida anterior `57fc9148` foi conservada, conforme o mecanismo de recuperação; não se contornou a verificação.
|
||||
|
||||
Causa concreta: o adaptador de continuidade acrescentava `compatible_fields_snapshot` vazio na recuperação de um rascunho legado sem nenhuma origem compatível. Correcção delimitada na branch `codex/adaptive-delivery-compatibility-QA`: devolver o rascunho original quando não há origem compatível nem metadados de herança anteriores. A herança com origem/metadados continua executada, preservando edições e limites por dossier. Nove controlos dirigidos da herança e o teste exacto da partição/recuperação passaram; não se reabriram os cálculos, relatórios ou cenários aceites. A correcção deve seguir o mesmo processo automático; não altera regras fiscais, esquema ou dados activos. Recibos da tentativa e da disponibilização final em `outputs/investmoz-adaptive-path-delivery-20261022`.
|
||||
|
||||
|
||||
### Percurso adaptativo disponibilizado na homologação — 22/10/2026, 10:17 Maputo
|
||||
|
||||
Autorização expressa da Tempo executada. Main passou de `57fc9148` por avanço directo para `064296e1`, conservando a ancestralidade `a76369eb → 52fbb146 → a1fbda06 → 064296e1`. O remoto não tinha alterações concorrentes adicionais no momento da integração; não houve conflito, reset ou envio forçado. A primeira verificação automática recusou o novo metadado vazio na recuperação legada e conservou a release anterior; a causa e a correcção dirigida constam da secção anterior. A correcção `042ffbde44cac27704d6f8b7ec25b525a42e3575` foi ensaiada em branch e integrada por avanço directo, sem suprimir o teste que detectou a incompatibilidade.
|
||||
|
||||
**Revisão efectivamente servida:** `042ffbde44cac27704d6f8b7ec25b525a42e3575`, branch main, build de 22/10/2026 às 10:16:14 Maputo; instalação automática concluída às 10:16:16. Manifesto SHA256 `655eef6e6a5e71c5138c68465a5b2ffc36d2cadeff96e6b35682dbfd19d42e79`. Conferida em `https://teste-investidor.tempo.co.mz/build/revision.json` e no recibo protegido da cadeia. A cadeia correu as verificações próprias; manualmente só foram repetidos os dez controlos afectados pela incompatibilidade. Zero migrations e zero importações no destino, sem copiar fixtures QA.
|
||||
|
||||
**Funcionalidade utilizável:** `https://teste-investidor.tempo.co.mz/investimentos` (login normal); abrir ou criar o dossier legítimo do investimento e usar «Preparar o pedido de investimento privado» / «Comparar e alterar contexto». Estão disponíveis as perguntas condicionais, fundamentos, comparação antes de confirmação, requisitos/documentos e próximos passos do lote `private-investment-preparation-2026-v1`, com os limites de 2026 já documentados. A conferência do destino limitou-se à revisão, configuração efectiva e entradas: investimentos redirecciona ao login (302), simulador responde 200. Não se criaram dossiers/contas reais ou sintéticas no destino nem se repetiram os cenários funcionais QA aceites.
|
||||
|
||||
**Habilitação concreta, distinta de fiscalidade:** a configuração do percurso `PRIVATE_INVESTMENT_PREPARATION_ENABLED` estava ausente/false; passou a true mediante a sessão SSH habitual autenticada pelo utilizador. Antes da escrita foram conferidos revisão servida e hash da configuração; cópia protegida dos bytes anteriores verificada, alteração limitada a essa chave, proprietário e modo0640 preservados, cache de configuração reconstruída e workers notificados de reinício gracioso. Hash .env anterior `7cdaba591bb5787485a349c24db2fd7c8279436da0eb31502e44db2fffa21d03`; posterior `a6b4e9064cf6dbb2d945b53418a6c9252f9c736ff3fde4016b95b74885a9ee0f`. A leitura posterior confirmou `investor-path.enabled=true` e `private_preparation=true`. A cópia de recuperação permanece privada em `/var/lib/investmoz-deploy/backups/private-preparation-20261022/`; não contém uma publicação de parâmetros e não segue para Git.
|
||||
|
||||
**Estado fiscal preservado:** nenhuma publicação/activação de nova regra e nenhuma operação na base activa. O consumidor IVA geral previamente autorizado manteve a mesma disponibilidade, cobertura2026 e mensagem antes/depois. As exclusões sectoriais, forma indirecta/dimensão monetária não determinada, benefícios territoriais e apuramento fiscal completo continuam explícitos e não bloqueiam o lote procedimental concluído.
|
||||
|
||||
**Suspensão/recuperação:** desligar somente a chave do novo percurso e reconstruir cache/reiniciar graciosamente os workers mantém dossiers, documentos, respostas, versões, relatórios e históricos. Para reverter configuração, usar o valor anterior apenas depois de conferir o hash actual e preservar alterações posteriores; não substituir todo o .env sem conferir concorrência. A reversão de código/build segue o procedimento de releases já existente, sem restaurar dados antigos. A release anterior foi efectivamente conservada na primeira tentativa recusada. Recibo completo local em `outputs/investmoz-adaptive-path-delivery-20261022/delivery-receipt.json`; a presente actualização documental fica na branch de registo, sem novo push main ou nova disponibilização desnecessária.
|
||||
|
||||
|
||||
## Revis?o das 30 fichas de Inhambane e cat?logo vazio ? 22/10/2026
|
||||
|
||||
Leitura remota autenticada, transac??o READ ONLY, base teste_investidor: 30 oportunidades em draft; zero publicadas em toda a base. As consultas independentes de Cabo Delgado e Inhambane e a consulta exacta Inhambane/Vilankulo devolveram zero sem falha de consulta. A alega??o de oportunidades publicadas de Cabo Delgado n?o se confirma neste destino; n?o inventar associa??es para preencher a lista.
|
||||
|
||||
As 30 localiza??es permanecem nos recursos privados opportunity_location_drafts, referidos no cadastro can?nico; os campos e v?nculos p?blicos ainda n?o s?o a projec??o dessas fichas. A inexist?ncia desses v?nculos p?blicos em rascunhos n?o prova perda da localiza??o. Na publica??o, projectar somente a abrang?ncia documentada e conferir a pesquisa territorial.
|
||||
|
||||
Decis?o Tempo nesta conversa: incompletude e autoriza??es necess?rias ? execu??o n?o s?o impedimentos autom?ticos ? divulga??o como oportunidade em prepara??o. Proposta: divulgar resumos das 30 fichas em prepara??o com fonte, edi??o, data-base e lacunas vis?veis; n?o declarar financiamento, terreno, licen?as, promotor autorizado ou execu??o confirmados. Valores hist?ricos, agregados e divergentes devem ser identificados sem somas indevidas. Nenhum impedimento concreto de divulga??o do resumo foi demonstrado nesta leitura. Isto n?o declara o conte?do privado integral public?vel: originais, contactos e evid?ncias continuam separados. N?o houve publica??o, altera??o de pap?is ou escrita remota.
|
||||
|
||||
Prepara??o por ficha: outputs/investmoz-territory-recovery-20261022/publication-review.json (30 identidades reais, localiza??o privada, lacunas e valores). Evid?ncia: read-catalogue-remote.py e remote-read.log no mesmo direct?rio. A tabela hist?rica de lacunas de prepara??o ? reutilizada, sem a converter em bloqueios legais. A prepara??o t?cnica do fluxo de publica??o ainda deve reconciliar essas condi??es com o contrato existente; n?o marcar publica??o como pronta ou executada.
|
||||
|
||||
O URL recebido ? do cat?logo/iniciar por territ?rio, n?o do dossier afectado. N?o permite ler o contexto inicial, a altera??o e a ?ltima ac??o desse processo. A falha da pr?via territorial e do t?rmino sem resposta continua aberta e ainda n?o foi reproduzida no dossier real; n?o encerrar pela disponibiliza??o anterior ou por esta leitura de cat?logo.
|
||||
<!-- search-territory-recovery-20261022 -->
|
||||
## Continuidade territorial da pesquisa — correcção em QA, 22/10/2026
|
||||
|
||||
Esclarecimento da Tempo: o percurso reportado foi navegação pública entre carteira e catálogo, sem clicar em iniciar/continuar ideia. Não existe evidência de alteração de um dossier pessoal neste caso. Retira-se a exigência de URL do dossier e de confirmação de impacto; a alteração é de filtros de pesquisa.
|
||||
|
||||
Causa verificada: ligações da carteira para oportunidades e zonas e ligações dos menus públicos usavam destinos sem província/distrito. O contexto transportado para detalhes por `catalogue_query` também não acompanhava as ligações cruzadas. No ecrã de zonas, os parâmetros de exportação/detalhe omitiam a hierarquia territorial.
|
||||
|
||||
Correcção: `searchTerritoryHref` transporta exclusivamente os quatro níveis territoriais na URL entre carteira, oportunidades, zonas e educação, incluindo os respectivos detalhes. Reutiliza o `catalogue_query` existente e preserva os filtros próprios do destino. Uma selecção explícita de outro ascendente não herda descendentes incompatíveis. Sem armazenamento global, mistura de contas ou alteração de dossiers. Menus, navegação relacionada, breadcrumbs e ligações cruzadas usam a mesma função. O selector partilhado mantém a limpeza dos descendentes; não há confirmação adicional para pesquisar.
|
||||
|
||||
Verificação dirigida: seis testes Node (incluindo o selector partilhado), TypeScript e build isolado. No navegador QA: carteira Cabo Delgado → selecção Inhambane/Vilankulo → pesquisar → catálogo com ambos os campos preenchidos; recarregamento e regresso preservam a selecção; zonas herdam os dois níveis; mudar para Cabo Delgado limpa Vilankulo. Contagem zero, filtros activos e mensagem de ausência coerentes na base QA consultada. Não foram repetidos cálculos ou exportadores fiscais nem publicados registos para produzir resultados artificiais.
|
||||
|
||||
Provas: `outputs/investmoz-territory-recovery-20261022/before-carteira.png` e `after-catalogue.png` no workspace de entrega; testes em `tests/frontend/search-territory.test.mjs` e `territory-path.test.mjs`; logs QA `outputs/territory-types.log` e `territory-build.log`. O router privado QA selecciona o build isolado; não integra a candidata.
|
||||
|
||||
Entrega limitada a branch/QA. A homologação não foi alterada por esta correcção. Publicação das 30 fichas de Inhambane permanece separada: revisão por ficha preparada, nenhuma publicação executada; ainda por reconciliar a preparação técnica da divulgação em preparação com o fluxo existente. A correcção da pesquisa não depende dessa publicação.
|
||||
## Inhambane — divulgação dos 30 resumos executada, 22/10/2026
|
||||
|
||||
Decisão expressa da Tempo na conversa: «publicar» os 30 resumos como oportunidades em preparação; responsável confirmado `nala@tempo.co.mz`. A conta concluiu autenticação normal com MFA. A primeira ligação abriu indevidamente `/management/opportunities`, que exige administração geral; `/minhas-oportunidades` estava acessível e foi confirmado no navegador. A ligação SSH estava pronta; não foi necessário pedir outra autenticação.
|
||||
|
||||
Executada uma operação privada de dados, limitada às 30 identidades e aos hashes das versões autorizadas, sob a decisão de divulgação da Tempo e a sessão real de Nala. Não se atribuiu papel geral de Publicador, administração, revisões técnicas, aprovação jurídica ou autorização de execução. O fluxo genérico de aprovação de projectos completos permaneceu intacto. A publicação promocional tem finalidade própria registada em `workflow_context.publication.purpose=promotional_preparation`; não equivale à conclusão desse fluxo. Cada operação registou responsável, conteúdo anterior, hash do lote e finalidade no histórico restrito.
|
||||
|
||||
O executor preserva o cadastro canónico privado, proprietário e organização, originais e contactos. A projecção pública contém título, resumo, limitações, fonte/edição e indicação «Em preparação». Montantes não confirmados não são apresentados como investimento disponível; os dados técnicos/financeiros históricos são assinalados como tal. A conferência editorial retirou instruções internas dos resumos, usando as limitações da tabela aprovada na conversa.
|
||||
|
||||
Detectado e resolvido o vínculo territorial em falta: as localizações privadas mencionavam Vilankulos, mas só tinham província estruturada. O cadastro público confirma MUNICÍPIO DE VILANKULO → posto VILANKULO → distrito VILANKULO → Inhambane. Foram associados ao distrito apenas 15 registos cuja localização documental menciona expressamente Vilankulos, incluindo programas de abrangência múltipla. Não se inferiu implantação, coordenadas ou relação por proximidade. As restantes fichas conservam a abrangência documentada; não foram associadas indiscriminadamente ao distrito.
|
||||
|
||||
Verificação pública sem sessão da aplicação: **30 resultados em Inhambane, 15 em Vilankulo**, todos com indicação «Em preparação», limitações e sem `management_draft`/`workflow_context` na resposta pública. No navegador foram conferidos catálogo, contagem e a ficha do Hotel Boutique no Terreno INATUR, incluindo a ausência de confirmação da disponibilidade do terreno/concessão. Foram reutilizadas as provas anteriores; o ensaio novo em base QA isolada conferiu publicação, alteração editorial, repetição sem duplicados, preservação do cadastro privado, recusa de alteração concorrente e associação exacta das 15 referências. Todos os dados sintéticos foram revertidos por rollback em QA; nenhum actor QA foi transportado.
|
||||
|
||||
Backup fresco em formato PostgreSQL custom antes de cada revisão de dados, protegido fora da raiz pública; catálogo de restauro verificado e provas anteriores de restauro reutilizadas. A leitura final confirmou as mesmas 30 identidades, cadastro privado/proprietários preservados, zero novos papéis Publicador e administração geral de Nala mantida desligada. A reversão deve partir do backup e do `before_data` restrito, comparar a versão actual e restaurar apenas as projecções/vínculos afectados, preservando edições posteriores; não restaurar toda a base sobre alterações concorrentes. Uma suspensão deve retirar os resumos do público, mantendo cadastro, relações e histórico, como operação expressamente registada; não apagar as fichas.
|
||||
|
||||
Recibos no workspace de entrega: `outputs/investmoz-territory-recovery-20261022/public-verification.json`, `integrity-receipt.log`, `publication-final-result.log`, `published-vilankulo-results.png`; executores privados e manifestos no mesmo directório, arquivados fora da raiz pública no destino. Hash final do lote: `d0a1146a7113aabafc3c2d08c0bdc17d12fc8a51606ba9ff7e79b154556d6e08`.
|
||||
|
||||
Revisão efectivamente servida: `042ffbde44cac27704d6f8b7ec25b525a42e3575`. Nenhuma instalação Git, migration ou activação fiscal foi executada nesta publicação. A candidata `9ac623a4` de continuidade dos filtros permanece separada de `main` e pronta para revisão de integração. A divulgação das 30 fichas deixa de estar pendente; os estudos, financiamento e autorizações de execução continuam como limitações explícitas das oportunidades.
|
||||
## Integração territorial e contacto comercial — 22/10/2026
|
||||
|
||||
Tempo autorizou integrar `9ac623a4` e pediu conferir a revisão servida. `57fc9148` é antepassado de `042ffbde`, e não uma revisão posterior: os hashes não têm ordem cronológica. A conferência remota comparou 685 ficheiros de aplicação/configuração com `042ffbde`, sem diferenças; o manifesto de build também coincide com o identificador publicado. O histórico de instalação confirma a passagem de `57fc9148` para `042ffbde`. Não se identificou reposição de código/build ou desaparecimento da entrega do simulador.
|
||||
|
||||
Integração Git por avanço directo até `1ec31856`, incluindo `9ac623a4` e recibos documentais. A primeira tentativa automática preservou a revisão anterior por falha Node: importação sem extensão em `catalogue-context.ts`. Corrigida a referência explícita `.ts`; passaram os dez testes dirigidos do módulo afectado e da continuidade territorial. Não se repetiram os percursos aceites nem a publicação de oportunidades.
|
||||
|
||||
Decisão expressa da Tempo: o rodapé deve apresentar **comercial@tempo.co.mz**, porque o contacto comercial com a instituição é feito por esse endereço. Actualizado o valor por omissão de `portal.institutional_contact_email`. Na homologação existia uma sobreposição explícita `PORTAL_INSTITUTIONAL_CONTACT_EMAIL=info@tempo.co.mz`: alterada exclusivamente essa chave para `comercial@tempo.co.mz`, com backup privado íntegro, propriedade/permissões preservadas e reconstrução da cache de configuração. A apresentação e o `mailto:` utilizam a mesma configuração existente; não houve envio de email.
|
||||
|
||||
Recibos de destino e integração: `outputs/investmoz-territory-recovery-20261022/revision-before-integration.log`, `footer-email-receipt.log` e recibo final de instalação no mesmo directório do workspace de entrega. A confirmação final deve identificar a revisão efectivamente instalada pelo processo automático, mantendo os 30 resumos e as regras fiscais existentes.
|
||||
## Selectores especializados do catálogo — QA, 22/10/2026
|
||||
|
||||
Pedido da Tempo: explorar estruturas públicas independentemente da existência de oportunidades, sem inventar associações nem divulgar registos privados. A homologação em `7b1c3579` já devolve e apresenta **Parque Industrial de Balama** e **Parque Industrial de Montepuez** no selector de Cabo Delgado; “Todos” é o valor inicial, não uma prova de ausência das opções. A API devolve duas zonas nessa província e oito no âmbito nacional. A base QA local tem outro conjunto de dados: não continha parques públicos ligados a essa província. Esta diferença de dados não é uma regressão demonstrada do selector da homologação.
|
||||
|
||||
Falhas verificadas no código: actividades e cadeias eram reduzidas às oportunidades publicadas encontradas no território/sector; estruturas públicas podiam depender da existência adicional de uma ficha publicada da carteira; os selectores vazios só apresentavam “Todos”; a consulta da ficha de um parque sem oportunidades não era apresentada no resultado vazio da grelha.
|
||||
|
||||
Correcção na branch `codex/catalogue-specialised-selectors-QA`: opções provenientes dos cadastros públicos existentes, preservando os critérios de consulta e os vínculos territoriais. Estruturas com território principal documentado não dependem de uma oportunidade nem de uma ficha adicional da carteira. Mantêm-se os vínculos territoriais já resolvidos pelas fichas publicadas. Cadeias acompanham os clusters públicos do âmbito seleccionado. Actividades são o catálogo público de classificação, explicitamente separado da disponibilidade local de investimentos; não se inventa uma associação territorial ou sectorial. Os oito selectores apresentam contagem ou explicação de ausência. Uma selecção indisponível permanece identificada, com possibilidade de remoção, em vez de parecer “Todos”.
|
||||
|
||||
Quando a estrutura seleccionada não tem correspondências, o resultado informa “Sem oportunidades publicadas associadas com estes filtros” e mantém ligação à ficha pública existente, em qualquer vista. Um registo mestre sem ficha pública detalhada é identificado como tal; não se cria uma ficha ou publicação automaticamente. A pesquisa continua a combinar todos os filtros, sem alargamento automático ou atribuição de benefícios. A alteração territorial mantém a limpeza dos níveis e selecções incompatíveis já implementada.
|
||||
|
||||
Provas novas, sem repetir os percursos anteriormente aceites: Cabo Delgado com dois parques sintéticos identificados; selecção de Balama, zero oportunidades, abertura da ficha, regresso e recarregamento com selecção conservada; limpeza; Nampula com parque sintético Topuito; ausência de restrição territorial com ambos os conjuntos disponíveis e selecção anterior limpa. Fixtures limitadas à base `simulator_fiscal_delta_20261021_test`, porta 55441, excluídas do Git e da disponibilização. Testes `CatalogueSelectorReferencesTest`: três testes, 27 verificações, transacções revertidas, incluindo exclusão de privados/outro território, catálogo de actividades independente dos resultados, estruturas/cadeia sem oportunidades ou fichas adicionais. `npm run types:check` e build QA passaram. Provas locais: `outputs/catalogue-selectors-20261022/audit.json`, `qa-balama.png`, `qa-nampula.png`, `qa-national.png` e logs de build/tipos no mesmo directório.
|
||||
|
||||
Diagnóstico separado do mapa: API pública de Cabo Delgado devolve território cadastrado, zero limites e nenhuma geometria acessível. Leitura SSH em transacção **READ ONLY** confirmou zero linhas em `geo_geometries` para essa província; neste caso não é uma camada existente ocultada pelo perfil. Não foram criadas coordenadas, geometrias, vínculos ou permissões. Este diagnóstico não equivale à implementação dos limites administrativos.
|
||||
|
||||
Integração pendente da revisão prevista: comparar com `origin/main`, preservar alterações concorrentes e integrar pelo processo Git automático existente. Não há migrations, publicação de oportunidades, activação fiscal ou transporte das fixtures. Conferir apenas o delta e a revisão servida depois de autorizada a integração; não repetir as provas já aceites.
|
||||
### Revisão dirigida da interacção do selector — 22/10/2026
|
||||
|
||||
Na candidata `1d921c9d`, ensaio pelo navegador Edge em QA, sem usar `selectOption` como prova desta interacção: clique no campo/seta deu foco; teclado `Alt+Down`, seta para cima e Enter mudou a selecção de Montepuez para Balama, actualizando o filtro e a ligação à ficha. Prova local `outputs/catalogue-selectors-20261022/qa-keyboard-balama.png`. Dados sintéticos explicitamente identificados como QA.
|
||||
|
||||
A ferramenta não observou o menu nativo aberto pelo rato; a tentativa de clicar na opção nativa devolveu opção não visível. Assim, a escolha completa **apenas pelo rato permanece não demonstrada** nesta revisão. Trata-se de limitação de observação/interacção da ferramenta, sem evidência suficiente para atribuir falha ao portal. A presença das opções na API/DOM e o uso anterior de `selectOption` não encerram esse ponto. Sem alteração funcional, repetição dos restantes testes ou integração em `main`; geometrias continuam como pendência distinta.
|
||||
### Decisão de integração — 22/10/2026
|
||||
|
||||
A Tempo encerrou a revisão de `1d921c9d` com as provas existentes e autorizou integrar a candidata e o registo documental `916819eb`. A interacção exclusivamente pelo rato permanece não observada pela ferramenta, **sem carácter bloqueante**; não constitui falha comprovada do portal, não exige substituir o selector nem repetir ensaios. A ausência de geometrias administrativas de Cabo Delgado permanece independente. Executar o processo Git automático existente, preservando alterações concorrentes; não transportar fixtures QA, publicar dados ou activar regras fiscais.
|
||||
|
||||
## Entrada única de gestão — preparação em QA, 22/10/2026
|
||||
|
||||
Origem: prioridade expressa da Tempo para a equipa cadastrar oportunidades e carregar, editar e publicar documentos e legislação pela interface, sem depender do Codex. A decisão de 22/10 sobre fontes e execução mantém-se. Geometrias administrativas ficam no saldo separado.
|
||||
|
||||
Implementada `/gestao-conteudos`, acessível pelo painel, menu lateral, Balcão e editores. Reutiliza os cadastros, biblioteca, armazenamento, perfis e histórico existentes. Nova oportunidade abre o editor operacional para quem tem contexto de criação; acesso legado é identificado como tal, sem o apresentar como publicação operacional completa. A entrada não atribui papéis. Escolhas técnicas deste delta: navegação agregada, contagens no âmbito do utilizador, pesquisa documental e pré-selecção do ficheiro acabado de carregar. Não são novas regras organizacionais.
|
||||
|
||||
Percurso diário: **Gestão de conteúdos e oportunidades → Nova oportunidade**, escolher promotor/modalidade, preencher e guardar; recuperar em **Gerir oportunidades e histórico**. Para documentos: **Novo documento** ou **Nova legislação → Receber documento em privado → Guardar em quarentena → Registar documento na biblioteca**. O original recém-carregado fica seleccionado; título e emitente conhecidos são recuperados. Completar os metadados, guardar, pesquisar o registo e usar **Editar metadados**, **Publicar** ou **Retirar da publicação**. Associações apresentam nomes dos registos existentes. Campos ainda não conhecidos não são inventados. Publicação para consulta não activa cálculos.
|
||||
|
||||
Verificação nova: quatro testes, 99 asserções, com transacções revertidas exclusivamente em `simulator_fiscal_delta_20261021_test`, loopback 55441, guarda explícita. Demonstrados acesso de equipa sem administração geral, recusa a utilizador sem autorização e ciclo documental completo (upload, ClamAV real, recuperação por pesquisa, edição, publicação/download, retirada, preservação do hash e quatro revisões). Exemplo documental exclusivamente sintético, sem origem oficial ou valor jurídico. Corrigida a classificação de retirados: o histórico existente usa `unpublished`; o filtro e o rótulo reconhecem também `withdrawn` histórico. TypeScript e build QA passaram.
|
||||
|
||||
No navegador, login e MFA normais da conta sintética de equipa; entrada → legislação; entrada → nova oportunidade → guardar preparação incompleta → recarregar, com título recuperado e ficha QA 241. Não foi repetido o workflow integral de revisão/publicação de oportunidades já existente; a prova nova cobre a entrada e a gravação/recuperação. O ensaio documental completo foi HTTP transaccional, não uma prova visual de cada clique. A conta QA tem autorizações explicitamente sintéticas; não comprova as designações das contas reais. Não transportar fixtures, configurações QA ou concessões de acesso.
|
||||
|
||||
Provas locais: `outputs/content-management-20261022/entrada-final.png`, `build-final.log`, e `tests/Feature/ContentManagementEntryTest.php`. Estado: candidata em branch `codex/content-management-entry-QA`, ainda sem integração ou instalação na homologação. Não declarar encerrada a autonomia operacional de todas as contas/categorias apenas por esta entrada. Não houve migration, publicação remota, alteração de papéis activos ou activação fiscal.
|
||||
|
||||
|
||||
## Recuperação da demarcação territorial — QA, 22/10/2026
|
||||
|
||||
Retomada expressa da Tempo como recuperação de funcionalidade anteriormente demonstrada, nos requisitos existentes AUD-GEO-02/03 e na experiência de cadastro. Não é criação de outra cartografia. O incremento de divulgação promocional permanece na sua branch independente.
|
||||
|
||||
**Causa comprovada:** leitura SSH autenticada, em transacção READ ONLY, da release `8286d750`/base `teste_investidor`: zero linhas em `geo_geometries`, incluindo eliminadas. Cadastro preservado: 11 províncias, 148 distritos, 437 postos e 1324 localidades. Directório dos originais privados ausente; importador desabilitado. Serviço de consulta, controlador e correspondências iguais por SHA256 ao código QA. Hash do cadastro (IDs, nomes, pais, nível, origem/códigos): `9de4181c42055bc6ef11b56333b64dd463d0ebe16bb2d1ea3238fc79e69024f7`, igual no QA e destino. Não se confirmou recusa de acesso nem perda de associação: faltam os próprios dados.
|
||||
|
||||
**Histórico:** as provas de 09–10/10 em `docs/investmoz/v2/auditoria-portal/correspondencias-cartograficas.md` registam 361→393→408 correspondências em bases QA anteriores. Os originais e recibos privados permanecem disponíveis. Os transportes de 19–20/10 deste manual excluíram expressamente geometrias; a disponibilização Git também exclui dados e ficheiros privados (`deploy/homologacao/README.md`). Em 22/10 a leitura de Cabo Delgado já encontrara zero linhas; esta passagem conferiu a tabela inteira. Prova-se uma diferença de composição entre QA cartográfico e homologação. Não foi localizado recibo da instalação anterior destes limites na mesma base; não se atribui data a uma suposta eliminação nem se descarta a observação anterior no mesmo endereço.
|
||||
|
||||
**Recuperação executada em QA:** o clone actual também tinha zero geometrias. Reutilizados seis originais INE/UNOCHA de 07/06/2019 (três GeoJSON e três metadados), hashes preservados e correspondências v2 existentes. O serviço `TerritoryGeometryImport` inseriu 11 províncias, 133 distritos e 264 postos, internos, com recibos e auditoria. Todos válidos/SRID4326. Nos exemplos, a consulta pública continua sem devolver limites internos. ClamAV real, assinaturas de 22/10: três originais limpos. ADM3, 26773796 bytes, excedeu o stream de 25 MiB e passou pelo scanner de ficheiro GIS com orçamento de 40 MiB nesse processo; tentativa inconclusiva preservada. Sem alterar limites globais ou configuração do daemon.
|
||||
|
||||
O mapa enquadra o limite seleccionado; se não existir, enquadra o nível superior disponível mais próximo e informa que é contexto. A lista distingue selecção de contexto superior. Fonte, edição e actualidade jurídica indeterminada preservadas. Limite administrativo não representa parcela nem autorização de investimento.
|
||||
|
||||
**Prova de interface:** oportunidade sintética QA268 criada no portal. Cabo Delgado → ANCUABE → posto ANCUABE → ANCUABE-SEDE; limites e aproximação progressiva nos três primeiros níveis. Recurso de contexto/principal guardado; após recarregar e abrir “Ver / editar / restaurar”, os quatro níveis foram recuperados. A localidade sem limite próprio apresenta aviso dos níveis superiores. A fixture QA255 anterior tinha versão fechada/payload sintético mínimo incompatível com gravação do contrato; não foi usada como prova de persistência nem corrigida silenciosamente. Uma navegação durante a recompilação QA conservava referência a asset removido; recarregar resolveu o carregamento, sem alteração no destino.
|
||||
|
||||
**Cobertura:** 408/581 elementos do exemplar aceite: 11/11 províncias, 133/159 distritos, 264/411 postos. Mantêm-se 26+147 correspondências pendentes, com nomes nas prévias e no documento de correspondências existente. O denominador do cadastro é diferente (148 distritos, 437 postos). Este exemplar não contém geometrias próprias de localidades ou parcelas; não atribuir limites superiores às 1324 localidades.
|
||||
|
||||
**Reposição preparada, não aplicada na homologação:** `outputs/investmoz-territory-recovery-20261022/demarcation/retained-geometry-restoration.zip`, SHA256 `53a5d8aa2be57603c928279cbd14561741dd2ec4b44b138fbc785380de6f951d`, contém seis originais e manifesto. Não contém dump de substituição, actores QA ou limites sintéticos. Procedimento existente:
|
||||
|
||||
1. Rever e integrar pelo Git o ajuste visual; conferir revisão efectivamente servida.
|
||||
2. Colocar originais no armazenamento privado partilhado, conservando hashes. Executar scanner actual ligado aos originais. Conferir backup/restauro e prova de activação da revisão/alvo, como exige o importador já existente; disponibilizar a configuração do importador.
|
||||
3. Gestor já autorizado, com MFA, abre `/management/territory-geometry-import`: prévia e aplicação de províncias, depois distritos, depois postos, com motivo/confirmar. Reconciliar prévia fresca no destino; não executar hashes antigos do QA. Recibos e reversão dos lotes novos existentes preservados.
|
||||
4. Conferir a cadeia territorial no formulário do destino e a recuperação após guardar, mantendo lacunas explícitas.
|
||||
|
||||
O staging dos originais, backup/restauro e configuração inicial ainda exigem operação técnica. Depois da reposição, seleccionar, guardar e recuperar a localização/mapa usam a interface. Esta passagem só leu o servidor remoto: sem alterar dados, permissões, camadas ou configuração. Não declarar a reposição no destino concluída.
|
||||
|
||||
Origem das restrições: decisões expressas da Tempo e requisitos anteriores do importador para preservação/backup e autenticação. Enquadramento e mensagens são escolhas de implementação, sem novo papel ou aprovador. Fonte oficial aceite reutilizada sem exigir nova validação da credibilidade.
|
||||
|
||||
Provas na pasta desta recuperação: inventário dos seis ficheiros, prévias/recibos por nível, `verification.json`, `homologation-readonly.json` e capturas. TypeScript, build QA e sintaxe PHP verificados; provas editoriais anteriores não repetidas.
|
||||
|
||||
|
||||
### Ajuste de acesso encontrado na disponibilização — 22/10/2026
|
||||
|
||||
A conta real de Superutilizador encontrou HTTP403 no importador: o middleware não incluía a cartografia no âmbito administrativo já aprovado e o serviço exigia exclusivamente o gate legado `manage-portal`. Corrigidos ambos para reutilizar `ScopedPortalAdministration`, com MFA de sessão e as exclusões existentes de Auditor. Não foram alteradas atribuições, papéis, contas ou o gate global. Origem do bloqueio: escolha antiga de implementação, não decisão organizacional da Tempo. Três testes transaccionais novos/15 asserções em QA: Superutilizador com MFA chega à validação de fonte/aplicação; sem MFA e conta comum são recusados; nenhum grant legado criado. Provas QA cartográficas aceites reutilizadas sem repetição.
|
||||
|
||||
|
||||
## Demarcação territorial reposta na homologação — 22/10/2026
|
||||
|
||||
Pedido expresso da Tempo executado. Candidata `15d82611` revista e integrada em `73c046e1`; ajuste de acesso `95d4cf44`, testado em QA, integrado em `30aed7cd`. A revisão `30aed7cd12ef65f797191467232087643c396751` foi instalada pelo processo Git existente e confirmada em `/build/revision.json?demarcation=30aed7cd`. O transporte de dados foi executado separadamente do Git.
|
||||
|
||||
### Dados, backup e importação
|
||||
|
||||
Seis originais INE/UNOCHA de 07/06/2019 transportados para o armazenamento privado partilhado: hashes iguais ao manifesto aceite, ficheiros 0640 e directório 0750, fora da árvore pública. Análise real dos seis pelo ClamAV 1.4.3, assinaturas de 22/10/2026, resultado limpo. Utilizado o protocolo local FILDES do daemon existente, sem instalar scanner, alargar permissões dos originais ou alterar limites globais: [documentação oficial ClamAV](https://docs.clamav.net/manual/Usage/ClamdProtocol.html).
|
||||
|
||||
Backup completo de `teste_investidor`, snapshot exportado consistente, restaurado em `127.0.0.1:55441/demarcation_hml_restore_20261022_complete_test`. As 171 tabelas coincidem integralmente por contagem e checksum canónico; arquivo dos 333 ficheiros verificado por conteúdo/hash. Recibo remoto em `/var/backups/investmoz-demarcation-20261022-complete/backup-receipt.json`, dump SHA256 `f80551de827e954a35b66ee4c8f2fcc559a4c88b48b3dc799c33b74907079c9b`. A comparação inicial revelou diferenças de ordenação Linux/Windows e a visão RLS reduzida da conta da aplicação; a comparação final usou ordenação canónica e a mesma visão completa do backup, preservando também os registos fiscais. Não se desactivou RLS.
|
||||
|
||||
A conta real `nala@tempo.co.mz` (ID15), com login e MFA normais renovados pelo titular, abriu as prévias frescas e aplicou os três níveis pela interface `/management/territory-geometry-import`:
|
||||
|
||||
| Nível | Criados | Pendentes | Lote auditado |
|
||||
| --- | ---: | ---: | --- |
|
||||
| Províncias | 11 | 0 | ec2c7c27-e4a3-466e-91a4-60aba939d3a0 |
|
||||
| Distritos | 133 | 26 | 142667c7-7309-4461-8d0a-fd00cb421577 |
|
||||
| Postos administrativos | 264 | 147 | b6d5a7e2-2f44-4bd6-9634-d97ee58ab718 |
|
||||
|
||||
Total: **408 limites internos**, válidos, não vazios, SRID4326; três recibos e três eventos `geometry_import_applied` ligados ao actor real. As geometrias foram repostas para consulta autorizada no cadastro; não foram publicadas camadas internas. Reversão existente disponível por lote, preservando originais, histórico e reutilizações e recusando alterações posteriores. Não se executou reversão no destino.
|
||||
|
||||
### Delta e acessos conferidos
|
||||
|
||||
Na oportunidade real #30, «Centro de Emergência Turística e Medicina de Mergulho de Tofo», a sessão real abriu Recursos → Ver recurso. O mapa desenha o limite da **província de Inhambane**, com fonte/edição e identificação de território seleccionado. Não é um polígono próprio de Tofo: distrito/posto/localidade continuam por indicar nessa ficha, como antes. A oportunidade, o recurso e a publicação não foram gravados nem alterados. Captura: `hml-demarcacao-tofo.png`. A prova QA aceite de guardar/reabrir quatro níveis foi reutilizada, sem repetição.
|
||||
|
||||
Consultas de controlo no destino recuperaram 1/2/3 limites na cadeia Cabo Delgado → distrito Ancuabe → posto Ancuabe. A localidade Ancuabe-Sede continua sem limite próprio e com três limites ancestrais de contexto. O cadastro territorial conserva exactamente o hash anterior `9de4181c42055bc6ef11b56333b64dd463d0ebe16bb2d1ea3238fc79e69024f7`.
|
||||
|
||||
Conferidos os acessos das contas existentes: IDs15 e16 mantêm a designação aprovada de Superutilizador; ID15 mantém Promotor e Validador. Não foram criadas contas ou atribuições. O endpoint do cadastro aceita os perfis já autorizados; a demonstração visual autenticada foi com ID15. A conferência das atribuições de ID16 foi de política, sem simular outro login. Quinze contas sem o perfil correspondente não satisfazem essa autorização. Pedido HTTP sem sessão ao endpoint privado devolveu 401; API pública devolveu zero limites internos; tentativa de obter o original pelo caminho público devolveu 403. Originais privados e regras de acesso preservados. Contas ainda não criadas não bloquearam a reposição.
|
||||
|
||||
O importador tinha um bloqueio legado de implementação: exigia `manage-portal` e não reconhecia a administração já aprovada. Corrigido por reutilização de `ScopedPortalAdministration`, com MFA de sessão e exclusões de Auditor existentes. Três testes novos/15 asserções passaram; não foi criado novo papel, aprovador ou gate global. Não houve repetição das provas QA cartográficas aceites.
|
||||
|
||||
Após a importação, **166/171 tabelas integralmente iguais** ao backup. Apenas `geo_geometries` (0→408), `geo_write_receipts` (0→3), `audit_events` (+6), `cache` e `sessions` diferem. Oportunidades, documentos, cadastro territorial, atribuições e par de Superutilizadores preservados integralmente.
|
||||
|
||||
### Guia curto e estado final
|
||||
|
||||
Entrar em [Gestão de conteúdos e oportunidades](https://teste-investidor.tempo.co.mz/gestao-conteudos) → Gerir oportunidades e histórico → abrir a ficha → Imagens, documentos e localizações → abrir ou preparar a localização. Seleccionar a hierarquia documentada; o mapa enquadra o limite disponível. Se faltar geometria própria, a mensagem distingue o contexto superior. Guardar o recurso e reabrir pela mesma lista recupera a preparação; não é necessário comando ou Codex para a consulta ou cadastro corrente.
|
||||
|
||||
**Reposição concluída neste recorte.** Mantidas as 26 correspondências de distritos e 147 de postos pendentes, com nomes nos documentos/prévias existentes. O exemplar não contém polígonos próprios das localidades ou parcelas. Essas lacunas não bloqueiam os 408 limites recuperados. A data de eventual desaparecimento anterior no mesmo endereço continua não determinada. A divulgação promocional mantém o seu incremento separado; este fecho não a declara integrada.
|
||||
|
||||
Provas: `homologation-restored-readonly.json`, `staging-receipt.json`, `preservation-receipt.json`, `http-access-verification.json`, captura e recibos locais na pasta desta recuperação; backup/restauro e prova de activação em caminhos privados. Uma interrupção do transporte local foi resolvida por escrita atómica da mensagem, antes de qualquer importação. Não ficaram passos técnicos pendentes para esta reposição; futuras importações usam as salvaguardas de backup/MFA já existentes.
|
||||
|
||||
## Concursos e anúncios — implementação em QA, 22/10/2026
|
||||
|
||||
Origem: pedido expresso da Tempo no anexo «O InvesteMoz deve ter uma secção pública Concursos e anúncios», com continuação autónoma autorizada. Aplicam-se as decisões permanentes da Tempo de 12/10 e 22/10/2026. Este incremento trata a divulgação de procedimentos; não abre recepção electrónica de propostas nem altera os investimentos associados. A estimativa apresentada antes da implementação foi de 16–24 horas; não representa tempo efectivamente consumido.
|
||||
|
||||
Reutilizados: entrada `/gestao-conteudos`, autenticação/MFA, autorização editorial e documental, entidades, oportunidades, projectos, sectores, território, biblioteca, originais privados, análise ClamAV, download verificado e `audit_events`. Acrescentados apenas três cadastros: procedimentos com rascunho, versões/actos imutáveis e acompanhamento individual. Uma oportunidade/projecto pode ter vários procedimentos; cada um admite lotes e actos associados. Não foram criados papéis, designações de funcionários ou aprovadores. Contas ainda não criadas não bloqueiam as contas já autorizadas.
|
||||
|
||||
### Fontes, âmbito e escolhas de implementação
|
||||
|
||||
- A UFSA identifica o Decreto n.º 79/2022 na [página oficial do regulamento](https://www.ufsa.gov.mz/divnovoreg.php). O [quadro oficial do Banco Mundial](https://www.worldbank.org/ext/en/what-we-do/project-procurement/framework) disponibiliza regulamentos e documentos normalizados, incluindo a edição de Setembro/2025. Cada procedimento regista a sua edição e acordo aplicáveis; a edição mais recente não é imposta a acordos anteriores.
|
||||
- Inventário deste recorte: instrução anexada, páginas oficiais referidas, pesquisa dos ficheiros acessíveis do repositório e referências da biblioteca existentes. Não foi identificado neste recorte um modelo oficial de concurso já fornecido como original local utilizável. Não se declarou importado ou analisado o articulado integral/modelos que não foram obtidos. Os documentos de demonstração são explicitamente sintéticos, sem classificação de autenticidade oficial. Os modelos disponibilizados pela Tempo usam a biblioteca existente, sem segunda validação genérica da credibilidade de fontes oficiais aceites.
|
||||
- Decisão expressa da Tempo: separar regimes Estado/externo/privado/outro; não generalizar UGEA, Cadastro Único, garantia ou abertura pública; guardar preparação incompleta; distinguir publicação de origem e publicação do portal; preservar actos, privacidade e auditoria.
|
||||
- Escolhas técnicas: só a prévia de publicação exige identificação, entidade, objecto, modalidade, instrumento/edição/fonte, calendário coerente, canal e motivo público. Rascunhos permanecem incompletos. Prazos mínimos são opcionais, citados por fonte/edição/modalidade; dias úteis usam exclusivamente o calendário documentado no registo. Não foi parametrizada uma tabela universal de mínimos legais nem presumido um calendário de feriados.
|
||||
- Escolhas técnicas de preservação: concorrência por versão, prévia assinada pelo servidor, nova verificação dos anexos na publicação, documentos por ID/hash e histórico imutável. Retirar ou substituir um binário na biblioteca torna indisponível o anexo antigo, sem apontar silenciosamente para outros bytes. Contacto sem autorização, notas internas e anexos privados não integram o conteúdo público. Ligações a oportunidades/projectos privados não divulgam os seus títulos ou fichas.
|
||||
- Autorizações reutilizadas: edição/publicação por contas já elegíveis para gestão documental completa ou edição editorial; auditor e conta simples não obtêm escrita. Acompanhamento exige apenas autenticação, incluindo conta sem email verificado. Estas escolhas não atribuem novos papéis e não dispensam os requisitos existentes de cada autorização.
|
||||
|
||||
### Guia curto da equipa
|
||||
|
||||
**Entrada QA:** [Gestão de conteúdos e oportunidades](http://127.0.0.1:18580/gestao-conteudos). Abrir **Novo concurso/anúncio**; escolher a entidade já cadastrada e, quando aplicável, oportunidade/projecto, sector e território. Indicar o regime e a edição do instrumento aplicável. Guardar mesmo com preparação incompleta; recuperar em **Gerir concursos e anúncios**. O motivo interno fica no histórico de gestão.
|
||||
|
||||
1. Preencher objecto, lotes, participação, financiamento, calendário, fuso horário e canal efectivo de apresentação. A data de origem é a referência do procedimento; o portal regista separadamente a sua publicação.
|
||||
2. Usar **Carregar / gerir documentos e scan na biblioteca**, que abre outra página sem perder o editor. Carregar o original, acompanhar o scan, editar os metadados e disponibilizar explicitamente os documentos autorizados. Voltar ao concurso, usar **Actualizar op??es da biblioteca** e seleccionar os anexos. Marcar públicos só os destinados à divulgação; os restantes permanecem privados.
|
||||
3. Guardar, escolher **Anúncio inicial** e **Preparar prévia pública**. Rever contacto, documentos, objecto, entidade, prazo e canal. O motivo público aparece também no histórico público. Usar **Publicar este acto**; alterações posteriores no rascunho não modificam essa publicação.
|
||||
4. Para esclarecer ou rectificar, guardar a revisão e publicar o acto correspondente. Para prorrogar, indicar o novo prazo, guardar e escolher **Prorrogação**; a prévia mostra o anterior e o novo. Resultados/adjudicação ou cancelamento usam o mesmo percurso, com texto e motivo documentados. O fim do prazo muda a indicação para **Prazo de propostas terminado**, sem adjudicação automática. Actos informativos posteriores não reabrem um cancelamento/resultado.
|
||||
5. Consultar o [catálogo público QA](http://127.0.0.1:18580/concursos); pesquisar por palavras, entidade, sector, localização, modalidade, financiador, regime, estado ou prazo. Abrir a ficha e **Voltar aos resultados** conserva os filtros. **Relatório imprimível → Imprimir anúncio** permite imprimir/guardar pelo navegador, incluindo o histórico dos actos.
|
||||
6. Uma conta simples usa **Acompanhar concurso** e **Os concursos que acompanho**; a lista identifica novos actos desde o registo de acompanhamento. **Deixar de acompanhar** remove só a subscrição individual. Não há promessa de alerta por email nem entrega de proposta: apresentar a proposta no canal indicado no anúncio/documentos.
|
||||
|
||||
Publicação documental disponibiliza um original da biblioteca; publicação de oportunidade disponibiliza a ficha pelas etapas existentes; divulgação promocional divulga o resumo autorizado de preparação; publicação de concurso divulga um procedimento e os seus actos. Nenhuma dessas operações autoriza execução do investimento, activa cálculo fiscal ou comprova envio à UFSA/financiador.
|
||||
|
||||
### Evidência e limites de entrega
|
||||
|
||||
Base exclusivamente QA `simulator_fiscal_delta_20261021_test`, porta 55441, com guarda de identidade antes de migração/fixtures/testes. Testes transaccionais preservam a base existente. `ProcurementTest` cobre preparação incompleta, concorrência, três regimes, filtros e retorno, prévia desactualizada, publicação anónima, prorrogação, expiração sem adjudicação, resultados, cancelamento persistente, conta simples, recusas de escrita, scan real, anexo privado, download anónimo e retirada documental. Ver resultado final em `outputs/procurement-20261022/tests.log`. Navegação existente: seis testes aprovados; compilação QA e TypeScript aprovados.
|
||||
|
||||
Exemplos persistentes, todos **SINTÉTICO QA**, sem concursos reais: estatal `17f44d8d-d077-4dca-9a63-5eb1c85a6926` (inicial + prorrogação, prazo de 20/11 15h para 16/12 17h, Africa/Maputo); externo `e85861cd-ffbb-4348-9cce-d588a6e09b86` (aberto); privado `fe271b02-0bfd-4fe2-8db2-856567b5111f` (inicial + cancelamento). Os rascunhos foram preparados por fixture identificada; guardar/reabrir, prévias e publicações destes actos foram executados no navegador pela conta sintética existente, não por comando de publicação. Resultado/adjudicação e expiração foram demonstrados pelos testes HTTP, não por adjudicação de um registo real.
|
||||
|
||||
Consulta anónima confirmada depois de terminar sessão: três regimes/estados no catálogo, ficha prorrogada, filtros preservados, relatório e download real do anexo 38. Anexo 39 e contacto sem autorização ficam privados. Acompanhamento confirmado pela conta simples já existente, sem MFA ou papel editorial. Evidências locais: `management.png`, `catalogue.png`, `print-notice.png`, `watch.png`, `fixtures.json` e resultados em `outputs/procurement-20261022/`. Estes artefactos QA não contêm credenciais na entrega Git.
|
||||
|
||||
Operações correntes deste incremento são feitas pela interface: cadastrar, guardar, recuperar, carregar/editar documentos pela biblioteca, pré-visualizar, publicar, actualizar/prorrogar, publicar resultado/cancelamento, consultar/imprimir e acompanhar. Não requerem executor privado ou Codex por concurso. **Pendente técnico de disponibilização fora de QA:** revisão e integração Git, migração pelo mecanismo de entrega e activação de `PROCUREMENT_ENABLED` no destino autorizado. O código usa flag desligada por defeito. Nenhum anúncio real foi publicado, nenhuma base activa foi migrada e não há integração automática com UFSA/financiadores. Geometrias não foram alteradas.
|
||||
|
||||
### Proposta de instalação compatível com deploy automático — candidata `1bcf34c0`
|
||||
|
||||
**Estado: proposta para revisão da Tempo, sem execução no destino.** O guia e os links QA da secção anterior foram conferidos pelo executor; a Tempo informou que ainda não os conferiu. A evidência QA aceite não equivale a aceitação pela Tempo nem a disponibilização na homologação. Destino previsto: `https://teste-investidor.tempo.co.mz`; confirmar a configuração instalada antes de executar. Esta passagem não integra em `main`, não altera configuração remota e não publica anúncios.
|
||||
|
||||
**Dependência de entrega encontrada.** Em `deploy/homologacao/pipeline.py`, `source_contract()` calcula SHA-256 dos ficheiros de migrations com normalização CRLF→LF; `build()` compara o conjunto completo com `schema_contract` e recusa qualquer diferença antes do build. O pipeline não executa migrations de produção nem importa conteúdos. A promoção troca atomicamente a release, verifica a revisão servida e a página inicial e reinicia a fila; estas verificações gerais não provam o acesso ao módulo. A configuração de exemplo aponta para `/etc/investmoz-deploy/schema-contract.json` e `/etc/investmoz-deploy/delivery.json`. Os ficheiros instalados podem divergir do repositório e devem ser lidos e comparados. Não se deve remover o gate nem converter o deploy em migração global automática.
|
||||
|
||||
**Delta exacto de schema.** Uma única migration: `database/migrations/2026_10_22_230000_create_portal_tenders.php`. Não altera tabelas existentes, extensões, utilizadores, papéis, grants, documentos ou dados territoriais; não contém seeds nem publicação de conteúdo.
|
||||
|
||||
| Tabela nova | Colunas | Chaves e relações efectivamente declaradas |
|
||||
|---|---|---|
|
||||
| `portal_tenders` | `id` UUID; `draft` JSONB obrigatório; `version` integer com default 1; `published_version_id` UUID anulável; `created_by` bigint obrigatório; `created_at`, `updated_at` timestamptz anuláveis | PK `id`; FK `created_by → users.id`, `RESTRICT` na eliminação |
|
||||
| `portal_tender_versions` | `id` UUID; `tender_id` UUID; `version` integer; `act` varchar(24); `payload` JSONB; `reason` text; `actor_id` bigint; `published_at` timestamptz anulável; `created_at` timestamptz obrigatório com default actual | PK `id`; UNIQUE (`tender_id`,`version`); FK `tender_id → portal_tenders.id` e `actor_id → users.id`, ambas `RESTRICT` na eliminação |
|
||||
| `portal_tender_watches` | `tender_id` UUID; `user_id` bigint; `seen_version_id` UUID anulável; `created_at`, `updated_at` timestamptz anuláveis | PK (`tender_id`,`user_id`); FK para `portal_tenders.id` e `users.id`, ambas `RESTRICT` na eliminação |
|
||||
|
||||
`published_version_id` e `seen_version_id` não têm FK nesta candidata; a associação de publicação é verificada pela aplicação, incluindo pertença da versão ao concurso. O histórico é imutável pelas operações da aplicação; esta migration não acrescenta trigger de imutabilidade. Índices novos: os das três PK e do UNIQUE indicado. O executor Laravel regista também a migration na tabela `migrations` existente. Confirmar tipos e restrições no catálogo PostgreSQL após instalação, não apenas a existência de uma tabela.
|
||||
|
||||
Dependências já existentes: PostgreSQL/JSONB, `users`, entidades `organizations`, `opportunities`, `projects`, `sectors`, `geo_locations`, biblioteca `portal_resources`/`documents` e relações documentais, `audit_events`, autenticação/verificação/MFA e autorização documental/editorial conforme a conta. A FK de instalação depende directamente de `users`; as restantes tabelas são dependências de execução do cadastro, prévia, consulta ou publicação. Manter os originais no armazenamento persistente privado e o scanner/fila da biblioteca operacionais; a publicação de anexos exige o scan e os direitos já aprovados. Sem nova dependência Composer/NPM ou serviço externo. `composer.lock` e `package-lock.json` não mudam. As provas locais de `ProcurementTest` usam a base QA nominal e ClamAV real; não adicionar esse teste ao CI remoto sem adaptar a sua guarda de ambiente e disponibilidade do scanner. O pipeline conserva a sua suite isolada existente; não se repete a bateria QA aceite.
|
||||
|
||||
**Sequência proposta, com pontos de paragem verificáveis:**
|
||||
|
||||
1. **Revisão e identificação.** Rever `1bcf34c0` e este plano. Confirmar SHA integral da fonte, revisão/release actualmente servida, configuração real do pipeline, contrato actual, ligação à base e estado da migration. Conferir as contas reais já existentes contra `DocumentManagementAccess`/`Procurement::access`: gestão completa ou designação editorial existente, email verificado e requisitos MFA aplicáveis; auditor e conta simples sem escrita. Não inferir acesso por nome do papel nem criar designações. Se uma conta ainda não existe, registar só essa pendência e continuar com as já autorizadas.
|
||||
2. **Coordenar a janela antes do merge.** Pausar o observador `teste-investidor-deploy.path` e impedir novo processamento durante a operação; aguardar o serviço em curso e coordenar com `delivery.lock`. Preservar os pedidos em fila e reconciliar qualquer integração concorrente antes de retomar. Não parar à força um build nem manipular a release servida. Confirmar `PROCUREMENT_ENABLED=false` na configuração efectiva; não colocar esta variável no ficheiro de flags CI, cujo validador só aceita prefixos `PORTAL_`/`INVESTMOZ_`.
|
||||
3. **Backup recuperável.** Guardar dump consistente da base de destino, cópia/snapshot do armazenamento persistente privado e público e manifesto de hashes, revisão/release anterior, contrato, configuração e recibos. Configuração e segredos ficam em local privado. Ensaiar o restauro desse backup numa instância isolada, sem substituir a base activa nem publicar ficheiros; conferir integridade, tabelas e amostra de binários. A prova de restauro antiga não substitui o backup desta janela. Registar hora/identidade da base e preservar os recibos.
|
||||
4. **Instalar schema sem activar.** Preparar a fonte exacta revista fora da árvore servida, sem fixtures QA, com dependências do lock e ligação autenticada ao destino conferida. Pela sessão técnica autenticada e com os privilégios de instalação existentes, executar **apenas** `php artisan migrate --path=database/migrations/2026_10_22_230000_create_portal_tenders.php --force` nessa fonte/configuração. Não executar `migrate` global, `migrate:fresh`, seeders ou scripts de demonstração. Se a migration já estiver aplicada, conferir o schema e não duplicar a instalação; tabelas parciais ou divergentes exigem reconciliação concreta, não DROP/recriação. Confirmar registo da migration e as três tabelas vazias, incluindo versões e acompanhamento. A release antiga continua compatível com estas tabelas adicionais.
|
||||
5. **Actualizar o contrato de migrations.** Guardar o contrato anterior e acrescentar exclusivamente o caminho/hash normalizado desta migration, após a instalação conferida. Comparar o contrato completo com a fonte revista: qualquer outra diferença deve ser explicada antes de prosseguir. Não regenerar/aprovar cegamente um contrato para o HEAD mais recente. Este passo é uma operação técnica explícita e não corre automaticamente por Git.
|
||||
6. **Integrar e transportar código, ainda desligado.** Integrar pelo processo Git após a revisão, identificar o SHA resultante em `main` e retomar o pipeline/pedidos preservados. Conferir que o contrato corresponde exactamente à fonte resultante; só então o build pode passar. Usar o build isolado e a promoção atómica existentes, sem editar ficheiros da release servida. Confirmar recibo, `/build/revision.json`, página inicial, biblioteca e gestão única. O módulo permanece desligado e as tabelas vazias; a existência do código não constitui activação.
|
||||
7. **Activar separadamente.** Com código/schema compatíveis conferidos, definir `PROCUREMENT_ENABLED=true` na configuração persistente do destino por acção técnica autenticada. Actualizar a cache efectiva de configuração e recarregar processos persistentes conforme a configuração instalada. O pipeline versionado não faz `config:cache` para a configuração real: não presumir que editar `.env` basta quando houver cache. Se se gerarem caches dentro de uma release sujeita a `tree_hashes`, reconciliar os hashes pelo mecanismo de entrega antes de novo deploy/rollback, sem mascarar outras diferenças. Não executar testes com `RefreshDatabase` no destino.
|
||||
8. **Conferir só o delta no destino.** Aplicar a lista abaixo, registar revisão, URLs, resultados e actor autenticado sem credenciais. Entregar o link único `https://teste-investidor.tempo.co.mz/gestao-conteudos` e o guia desta secção com os links QA substituídos pelos correspondentes do destino. Não declarar disponibilização enquanto esta passagem não estiver concluída.
|
||||
|
||||
**Verificação no destino sem publicar anúncios reais como efeito da instalação:**
|
||||
|
||||
- Sem sessão: `/concursos` responde 200, incluindo catálogo vazio quando não houver anúncios; navegação pública mostra a secção; não surge autenticação HTTP adicional. UUID de rascunho/inexistente não permite obter ficha ou relatório públicos; anexos privados continuam inacessíveis.
|
||||
- Com conta real já autorizada: gestão única mostra **Novo concurso/anúncio** e **Gerir concursos e anúncios**; abrir formulário, guardar um rascunho de verificação claramente identificado, recuperar e pré-visualizar, sem publicar. Registar ID e preservar o rascunho/histórico; não o converter num anúncio real. Conferir scanner/biblioteca existentes por diagnóstico e estado, reutilizando as provas de upload aceites.
|
||||
- Conta simples: pode abrir a consulta e área de acompanhamento, mas recebe recusa na escrita/gestão; auditor não recebe escrita. Utilizador sem sessão é encaminhado para autenticação nas rotas de gestão. Não alargar grants para passar a prova.
|
||||
- Se já houver anúncio autorizado no destino, consultar sem sessão ficha, relatório, anexo autorizado e filtros/retorno. Se não houver, a consulta positiva de conteúdo publicado mantém a prova QA aceite e o catálogo vazio é o resultado esperado no destino; não importar exemplos nem publicar um anúncio só para preencher o catálogo. Prorrogação/publicação/resultado já estão cobertos em QA e não são repetidos sobre anúncios reais durante a instalação.
|
||||
- Conferir que instalação/activação não criou qualquer versão com `published_at` preenchido, não mudou `published_version_id` de anúncios existentes e não alterou documentos/grants. Anúncios que a equipa publicar posteriormente são acções distintas, identificadas pela auditoria.
|
||||
|
||||
**Reversão com preservação.** Em falha de instalação antes do merge, manter flag desligada e release antiga; conservar tabelas eventualmente criadas até diagnosticar a falha e repor o contrato anterior se necessário. Em falha após disponibilização, desligar primeiro a flag na configuração efectiva e preservar um novo snapshot/dump dos dados e ficheiros actuais. Preferir correcção progressiva. Se for preciso reverter código, usar o rollback atómico existente do pipeline para a release anterior compatível, com recibo e configuração desligada; conservar as três tabelas, `migrations`, `audit_events`, subscrições e armazenamento partilhado. Esse rollback de código não restaura a base. Conservar o contrato compatível com a linha de código que voltará a ser entregue e documentar qualquer reposição do ficheiro de contrato; a verificação por hashes continua obrigatória.
|
||||
|
||||
Não executar `down`, `migrate:rollback` global nem restaurar por cima da base activa o backup anterior à instalação: isso eliminaria anúncios, actos, acompanhamentos, auditoria e ficheiros criados depois. O `down()` da candidata recusa a remoção quando há concursos, mas essa guarda não substitui o plano de preservação. Um restauro extraordinário exige primeiro recuperar numa instância separada e reconciliar os dados posteriores, conservando IDs, versões, relações, hashes e originais; só trocar o destino com um plano próprio revisto. Desligar o módulo torna-o temporariamente indisponível, não elimina nem retira selectivamente anúncios. Ao reactivar a release corrigida, recuperar e conferir os dados existentes.
|
||||
|
||||
Origem dos limites: QA/revisão antes de integração e ausência de publicação real na instalação são decisões expressas da Tempo; backup/restauro e autenticação de alterações no destino são requisitos permanentes existentes; pausa coordenada, schema antes de código, flag e contrato são escolhas técnicas impostas pela compatibilidade observada do pipeline. Não constituem nova cadeia organizacional de aprovação. Depois da instalação, as operações correntes de cadastro, prévia, publicação, prorrogação e actualização permanecem na gestão única, sem comandos ou Codex por concurso. As únicas intervenções técnicas deste plano são a instalação/activação e eventual recuperação da plataforma.
|
||||
|
||||
### Instalação concluída na homologação — 22/10/2026
|
||||
|
||||
Autorização expressa da Tempo nesta conversa: **«pode instalar»**, abrangendo a candidata `1bcf34c0` e o procedimento `e46f9731`. A instalação foi executada por SSH autenticado já disponível; a verificação da interface foi feita pela conta real Osmane Nala, ID 15, após renovação de login/MFA pelo utilizador. Não houve criação de contas, papéis ou aprovadores. A ausência de outras contas não bloqueou esta autorização existente. O guia continua sem declaração de conferência/aceitação pela Tempo.
|
||||
|
||||
Sequência efectivamente concluída: pausa coordenada do observador de deploy → backup → restauro isolado conferido → migration selectiva com flag desligada → conferência do schema e contrato → integração/push Git → build/promoção automática → activação separada. Integração funcional `f4d0970917e7e8d141f74c136b66138b929141fc`, confirmada em `/build/revision.json`. Activação às **22h51 de 22/10/2026 (África/Johannesburgo; 20h51 UTC)**. Os commits documentais posteriores conservam o mesmo código funcional. O pipeline está novamente activo e conserva o gate do contrato de migrations; não foi alterado para executar SQL de produção.
|
||||
|
||||
Backup privado no servidor: `/var/backups/investmoz-procurement-20261022/backup`. Dump SHA-256 `69de817b288ecb25db4b341375475f2408a47642a8ba69d338b217dcc982854f`; arquivo de ficheiros/configuração `7b75c22e730b5600d6e97e1c6dbb4cd93982bdd47e9dd2a63de2359974132674`. Restauro realizado num PostgreSQL isolado em container sem rede, porta interna 55441: **171 tabelas**, contagens e checksums iguais ao snapshot exportado. Arquivo conferido: **341 ficheiros**, incluindo configuração privada; preservados os **340 ficheiros de conteúdo** no destino. Não foi copiada uma base QA para a homologação.
|
||||
|
||||
Schema instalado: **três tabelas, 21 colunas, três PK, cinco FK e um UNIQUE**, correspondentes à migration revista; registo em `migrations` conferido. A conferência inicial do executor tinha uma contagem de colunas incorrecta (22); foi corrigida para as 21 efectivamente declaradas e conferidas antes de actualizar o contrato, sem repetir ou desfazer a migration. Não se alteraram grants. Comparação dos dados preexistentes antes da prova de rascunho: apenas `migrations`, `cache` e `sessions` diferiam do backup; utilizadores, atribuições, biblioteca, oportunidades, cadastro territorial e geometrias permaneciam iguais por checksum. Scanner ClamAV real disponível, assinaturas de 22/10/2026.
|
||||
|
||||
Conferência do delta no destino:
|
||||
|
||||
- `/concursos` responde 200 sem sessão ou autenticação HTTP adicional; catálogo vazio é esperado, porque não foram publicados anúncios. Gestão sem sessão redirecciona para login. Conta simples existente não tem autorização de escrita; recusas por perfil e operações editoriais conservam as provas QA aceites, sem criar uma conta de teste no destino.
|
||||
- Gestão única da conta real apresenta **Novo concurso/anúncio**. Rascunho `b970f0f0-9257-42e1-8365-3358237b55db`, referência `INSTALL-CHECK-20261022`, claramente identificado como **VERIFICAÇÃO DE INSTALAÇÃO — NÃO PUBLICAR**: guardado, recuperado pela lista e pré-visualizado na interface. Um acto de preparação auditado (`tender_draft_saved`, actor 15). Preservado como rascunho privado; não é um concurso real.
|
||||
- Ficha e impressão desse rascunho devolvem 404 sem sessão; download de documento privado devolve 404. Não se alterou a disponibilidade dos documentos existentes. **Uma preparação, uma versão de rascunho, zero versões publicadas e zero acompanhamentos.** Nenhum anúncio real foi publicado pela instalação ou como prova. Não foram transportados exemplos QA.
|
||||
- Publicação/prorrogação/resultados/cancelamento positivos conservam os 8 testes/285 asserções e a passagem visual QA aceites; não foram repetidos sobre anúncios reais na homologação. O build de entrega executou as suas verificações isoladas normais.
|
||||
|
||||
Recibo resumido versionado: [procurement-installation-receipt-20261022.json](procurement-installation-receipt-20261022.json). Recibos técnicos completos, logs e backups permanecem privados; capturas e recibo local em `outputs/procurement-install-20261022/`. A reversão continua a ser desligar a flag e, se necessário, reverter código compatível mantendo tabelas, versões, auditoria e armazenamento; não substituir dados novos pelo backup anterior.
|
||||
|
||||
**Links finais e guia de operação:** entrar na [gestão única](https://teste-investidor.tempo.co.mz/gestao-conteudos) → **Novo concurso/anúncio** → preencher regime/edição, entidade, objecto, calendário/fuso e canal → **Guardar rascunho**. Recuperar em **Gerir concursos e anúncios → Recuperar e editar**. Carregar/editar originais pela biblioteca ligada no formulário, acompanhar o scan e autorizar a divulgação dos anexos; voltar ao editor e actualizar as opções. **Preparar prévia pública** → rever conteúdo e motivo público → **Publicar este acto**, quando a equipa pretender publicar um anúncio autorizado. Para actualizar ou prorrogar, guardar a revisão e escolher o acto correspondente; a prorrogação mostra prazo anterior e novo. Resultado ou cancelamento seguem o mesmo percurso. O rascunho de instalação não se destina a publicação.
|
||||
|
||||
Consulta: [Concursos e anúncios](https://teste-investidor.tempo.co.mz/concursos). Abrir ficha para documentos e relatório imprimível; uma conta simples pode acompanhar, sem apresentar proposta. Documentos, oportunidades, divulgação promocional e concursos mantêm as distinções do guia anterior. **Não restam comandos ou intervenção do Codex para cada concurso** neste âmbito; a instalação e activação técnica estão concluídas. Geometrias e permissões foram preservadas.
|
||||
|
||||
### Legado — correspondência integral e avanço privado QA, 22/10/2026
|
||||
|
||||
Fonte do saldo: conferência documental fornecida pela Tempo e [InvesteMoz — entregas, Fintech e classificação](https://chatgpt.com/space/page_d6f090dba9048191a92c9175b3605dee), consultada nesta passagem; originais e reconciliação de 15/10 preservados. O CSV antigo ainda tinha 17 publicados/31 por integrar. Reconciliados os três artigos posteriores, sem os duplicar: **20 publicados, 28 por integrar na homologação, um vazio e um sem título = 50 identidades**. Os 29 recursos da biblioteca e as onze variantes de URL não são contagens de artigos distintos.
|
||||
|
||||
Lista única para a Tempo: [título original → endereço original → endereço no novo portal → estado → pendência](../v2/auditoria-portal/legado-correspondencia-integral.md). Actualizado também o [CSV existente dos 50 artigos](../v2/auditoria-portal/legado-conteudos-reconciliados.csv); mantém IDs, títulos, origens, hashes e referências anteriores, com campos explícitos para o estado consolidado e data da conferência. Não se criou nova matriz/backlog. Endereços novos ausentes ficam em branco; não foram fabricadas páginas para os conteúdos ainda não disponibilizados.
|
||||
|
||||
**Nova conferência pública, não nova aceitação funcional:** 20/20 páginas na homologação responderam sem login, com componente `portal/legacy`, ID legado, origem e corpo não vazio correspondentes. A identidade não foi deduzida apenas de HTTP 200 nem do título. IDs da biblioteca foram obtidos da resposta real do destino, sem transportar IDs QA. Consulta HTTP em 22/10/2026 às 23h05 (21h05 UTC). Não é revisão visual integral de cada artigo nem confirmação de actualidade de todas as afirmações. Imagens anteriores não foram reimportadas ou reanalisadas; as referências públicas das fichas foram registadas, sem repetir as provas aceites dos binários.
|
||||
|
||||
**Cartões educativos:** a branch `codex/educational-content-cards` já é ancestral de `main`, e o componente com imagem maior/resumo/«Ler o guia» está servido. Conferência visual no destino [Explorar Turismo](https://teste-investidor.tempo.co.mz/explorar/turismo): 12 cartões/links «Ler o guia». Corrige a indicação histórica de revisão ainda pendente, sem atribuir à Tempo uma conferência que não fez. Captura `outputs/legacy-correspondence-20261022/guides-homologation.png`.
|
||||
|
||||
**28 conteúdos avançados em QA, sem publicar:** dois sectoriais, oito de Trabalho Doméstico e 18 históricos/noticiosos. Preservado o corpo HTML de cada captura, hash do corpo, ID, autor, categoria, datas e origem. Derivadas JSON identificadas pelo formato existente, armazenadas privadamente; imagens não resolvidas não foram substituídas por um fundo genérico nem marcadas como disponíveis. O mecanismo existente `documents:import-batch` foi reutilizado exclusivamente em `simulator_fiscal_delta_20261021_test`, porta 55441, com guarda da identidade real. Não foi alargado o importador activo ou o seu âmbito nominal. Conta sintética QA já existente 239; sem novo grant persistente. **28 rascunhos internos, 28 scans reais limpos e 28 reutilizações por SHA-256 na repetição**, sem duplicações; publicações e atribuições preservadas.
|
||||
|
||||
A primeira passagem importou dez e detectou colisões de identidade nos restantes: o manifesto usava emissor/data/versão iguais para artigos distintos. Corrigida a versão documental dos itens restantes para incluir o ID original do artigo; os dez já criados foram reutilizados por bytes. Sem apagar/recriar originais, simular scan ou classificar artigos como diplomas oficiais. Esta era uma escolha de metadados da preparação, não uma decisão organizacional da Tempo nem uma razão para impedir o cadastro. O importador não foi alterado e os títulos originais foram preservados. Preparações, manifesto, logs e recibo completos mantidos em caminhos privados QA; o recibo versionado contém somente metadados de rastreabilidade.
|
||||
|
||||
**Lacunas originais conferidas na rede:** [Mercado Financeiro, ID 171994](https://investemoz.tempo.co.mz/mercado-financeiro), HTTP 200, corpo vazio, hash igual à captura. [Post 74364](https://investemoz.tempo.co.mz/74364-2), HTTP 200, título vazio, mesmo corpo capturado (274 caracteres HTML, 21 úteis no inventário). Não se encontrou conteúdo/título novo a recuperar. A resolução editorial destes dois exemplares depende do texto/título da Tempo; não se inventou um título nem se usou Fintech/imagem para preencher o artigo vazio. Não bloqueiam os outros 48.
|
||||
|
||||
**Saldo de integração conservado:** os 28 rascunhos QA ainda exigem fechar edição/leitor e entrega/publicação por lotes na homologação. Trabalho Doméstico e notícias mantêm âmbito/data históricos; não foram convertidos em orientação jurídica actual, regras executáveis ou oportunidades vigentes. As dependências de 15/10 estão identificadas como históricas, não como novos requisitos de validação. Nenhum conteúdo novo foi publicado no destino, não houve alteração de schema, mapas, permissões ou integração em `main` nesta passagem. A integração integral continua aberta; não confundir preparação privada QA com publicação dos 28.
|
||||
|
||||
Evidência consolidada: [recibo de correspondência, consulta e importação QA](../v2/auditoria-portal/legado-correspondencia-recibo-20261022.json). Ferramenta `tools/legacy-correspondence-audit.py`; `--reuse-http` regenera a lista sem repetir as consultas públicas. Os hashes das capturas/respostas são distintos dos hashes dos corpos/originais; não foram usados para alegar uma nova edição editorial.
|
||||
|
||||
### Legado — edição e leitura dos 28 rascunhos em QA, 22/10/2026
|
||||
|
||||
Actualiza o saldo anterior: **20 públicos já conferidos, 28 rascunhos privados com editor/leitor disponíveis em QA e dois originais incompletos**. Não houve nova importação, repetição da conferência dos 20, integração em `main`, transporte ou publicação na homologação nesta passagem. A Tempo não declarou ter conferido o guia local ou esta candidata.
|
||||
|
||||
**Guia curto da equipa.** Em [gestão única QA](http://127.0.0.1:18580/gestao-conteudos), abrir o acervo e localizar o título. **Editar artigo** recupera o corpo do original, a origem e as datas; corrigir o texto e escolher conservar a imagem de origem, seleccionar uma imagem pública verificada da biblioteca ou não mostrar imagem. **Pré-visualizar** mostra as alterações ainda sem guardar e não publica. **Guardar edição** cria uma revisão, preserva o original e retira eventual publicação até nova decisão expressa. **Ler revisão guardada** recupera a edição persistida. Metadados, classificação/direitos, histórico e publicação continuam no acervo ligado pelo editor; **Publicar** é uma acção distinta, sujeita às permissões e campos já existentes. Para actualizar um artigo publicado, guardar a revisão, rever a prévia e publicar novamente. A consulta pública usa `/artigos/{id-do-destino}` e o acesso pela biblioteca; rascunhos não ficam acessíveis por esse endereço.
|
||||
|
||||
O leitor mantém a estrutura HTML sanitizada do original enquanto o corpo não for alterado; uma correcção textual fica como edição separada, sem reescrever o ficheiro capturado. Datas e autor de origem continuam identificados. Conteúdos históricos não se tornam oportunidades actuais, diplomas oficiais ou regras de cálculo. A revisão QA do artigo 62413 contém uma nota de demonstração explicitamente marcada **QA**, guardada pela interface: deve ser revista/corrigida antes de publicação real; não foi removida silenciosamente do pacote.
|
||||
|
||||
**Imagens e lacunas.** Preservadas as 16 referências a imagens no corpo e a referência de capa dos 28 originais. Não se descarregam automaticamente imagens externas. Nesta QA não há exemplar público verificado da capa comum: a prévia informa a ausência e conserva a referência. A capa já aceite no destino deve ser reutilizada pelo SHA-256 `28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa`, sem confundir igualdade de URL com igualdade de bytes. Pode seleccionar-se outra imagem autorizada pela interface; nenhuma imagem foi marcada como disponível sem binário verificado. Os dois casos incompletos na origem ficam fora deste lote.
|
||||
|
||||
**Permissões e origem das restrições.** Reutilizados os gestores e as designações editoriais existentes, email verificado, MFA aplicável e exclusão de escrita para auditor/conta simples. Nenhum papel ou aprovador novo. A inclusão destes 28 originais no âmbito editorial decorre do pedido expresso da Tempo. A correspondência por hash exacto dos originais é uma escolha técnica: não depende de IDs ou etiquetas de importação QA que mudem no destino. Validação dos bytes, scan e classificação/direitos seguem requisitos anteriores; controlo de versão e prévia privada são escolhas técnicas para evitar sobreposição de edições e publicação acidental.
|
||||
|
||||
**Evidência delimitada.** Seis testes específicos, **1.000 asserções**, passaram na base isolada `simulator_fiscal_delta_20261021_test:55441`, com transacções revertidas. Percorrem os 28 editores/prévias, edição/recuperação/histórico, conflito de versão, publicação explícita e retirada, classificação, imagem verificada/retirada, sanitização e permissões nominais/MFA. A prova de publicação dos testes foi revertida; os 28 continuam privados. TypeScript e build QA passaram. Na interface, conta sintética QA existente guardou/recuperou a revisão 2 do artigo 62413; leituras de Alumínio “Verde” e do guia de Trabalho Doméstico demonstradas. Não houve alteração de contas reais. Capturas e logs em `outputs/legacy-editorial-20261022/`; [recibo resumido](../v2/auditoria-portal/legado-editorial-qa-20261022.json).
|
||||
|
||||
**Preparação concreta da entrega privada.** `outputs/legacy-editorial-20261022/legacy-editorial-private-v2.zip`, 85 614 bytes, **57 entradas**: 28 originais, 28 snapshots/históricos de proveniência e manifesto. SHA-256 `177c50d1cc3b1610e62123a2f8b532711a15f875aac2ee0e372636c2f747e8a2`. Conferidos os 28 hashes e a integridade do ZIP; zero imagens binárias incluídas e zero escritas de base pelo exportador. Manifesto com `publish=false`, edições actuais e vínculos de imagem por hash; sem credenciais ou cópia da base. `tools/prepare-legacy-editorial-delivery.php` só exporta a QA nominal, não instala/importa no destino. Originais e histórico completo ficam privados; IDs/actores QA são proveniência, não identidades ou autores de auditoria no destino.
|
||||
|
||||
**Sequência para homologação, ainda não executada:**
|
||||
|
||||
1. Rever e integrar a candidata pelo processo Git existente. Não há migration, alteração de schema ou nova dependência de pacote; o deploy de código não transporta estes conteúdos nem publica artigos.
|
||||
2. Antes de escrever no destino, conferir revisão servida, identidade da base e backup consistente de base/armazenamento com restauro isolado demonstrado para a janela. Transportar o ZIP para armazenamento privado, fora da árvore pública, e conferir o hash. Preservar o recibo e o manifesto.
|
||||
3. Com autenticação e permissões existentes, usar o cadastro/upload da biblioteca para cada original ainda ausente, com scan real e classificação privada. Resolver por hash e identidade/versão: reutilizar originais iguais, registar IDs reais e parar apenas o item com conflito, sem sobrescrever. Não repetir a importação QA nem copiar a sua base. Não usar os importadores nominais de Agronegócio/lotes antigos ou um `--actor` para contornar os limites activos; o exportador não alarga esses executores.
|
||||
4. Recuperar os recursos pelo portal; aplicar metadados/edição actual pelos formulários autenticados, usando a versão do destino e criando histórico atribuído ao actor real. Conservar os snapshots QA privadamente como proveniência, sem inserir actores QA na auditoria activa. Reutilizar a capa aceite por hash; referências sem binário mantêm aviso. Conferir **28 privados, zero publicações**, correspondência original → recurso real e hash, revisão guardada e acessos previstos. Actualizar a lista única com os endereços efectivamente demonstrados.
|
||||
5. A equipa decide e executa a publicação individual no portal, após rever/corrigir/pré-visualizar. Transporte privado, autorização dos metadados e publicação são etapas distintas. Não publicar artigos como efeito da instalação ou da conferência do transporte. Verificar só o delta do destino, sem repetir os 20 aceites ou as provas QA.
|
||||
|
||||
**Reversão e dependências remanescentes.** Para retirar conteúdo, usar a retirada pela interface e preservar originais, revisões e auditoria. Se for necessário reverter código, manter dados e ficheiros; não restaurar um backup antigo por cima de edições/publicações posteriores. Preservar novo snapshot e reconciliar o delta antes de qualquer restauro. A revisão/integração Git, transporte privado, backup/restauro e upload/scan autenticado iniciais ainda são trabalho de entrega; não estão declarados concluídos nem constituem tarefas recorrentes de cada edição. Depois de disponibilizado este delta, rever, corrigir, pré-visualizar, guardar, recuperar, publicar e retirar estes conteúdos não exigem comandos ou Codex. A revisão editorial efectiva dos textos pela equipa continua distinta da demonstração do percurso. Geometrias não foram alteradas.
|
||||
|
||||
@@ -1,51 +1,51 @@
|
||||
ID legado,Título,Autor,Categoria,Publicado,Modificado,SHA256 conteúdo,URLs observadas,Recurso QA,Situação,Situação anterior à reconciliação 15/10/2026,Recurso ativo,Grupo restante,Imagem atual,Dependência concreta,Próxima ação,Evidência atual
|
||||
171985,Agronegocio,tempo,Oportunidades,2026-04-06T09:19:07+00:00,2026-04-20T12:41:43+00:00,28383a11d95f13894381357d8f1227b33031d1d3ff74507da4ea8e5ce2470913,https://investemoz.tempo.co.mz/agronegocio,6,publicado,recurso único QA; não é oportunidade,5,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 6, ""sha256"": ""5e3840705cf98226d1f189cf665aaa98cb05da96c645159a1332a1d429d642eb""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
62413,15/30 costa do sol com dua na zona de mupulene muito lindo,tempo,Categoria,2017-06-26T11:44:21+00:00,2017-06-26T11:44:21+00:00,c04ae821317ef556c01c3141ce75eeeeb5da7787e0c6849b1b3709b757edb9eb,https://investemoz.tempo.co.mz/1530-costa-do-sol-com-dua-na-zona-de-mupulene-muito-lindo,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/817802064_1_644x461_15-30-costa-do-sol-com-dua-na-zona-de-mupulene-muito-lindo-maputo.jpg | Referência não recolhida: https://www.olx.co.mz/imoveis-casas/terrenos/terrenos-venda/maputo-maputo/?search%5Bprivate_business%5D=private | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
173220,Alumínio “Verde”,tempo,Oportunidades,2026-06-16T12:41:02+00:00,2026-06-16T12:41:06+00:00,c61f05a56bb5a5f13d9d5d54dbefd270cf9ac40320917d77aaad76cac56c7b97,https://investemoz.tempo.co.mz/aluminio-verde,,existente por integrar,inventariado; cadastro/versão por reconciliar,,setorial,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Ausente da biblioteca ativa e QA; falta exemplar versionado no manifesto de um novo lote e extensão nominal do importador. | Slug ausente no mapa do leitor; falta ligação ao percurso/dossier fundamentada para este artigo. | Não há setor mestre Alumínio Verde com correspondência exata; fundamentar o agrupamento sem inventar vínculo a projeto ou criar setor automaticamente.,Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
73945,Banco Mundial classifica de proibitivos juros praticados em Moçambique,tempo,Nas Redes,2017-07-31T19:55:24+00:00,2017-07-31T19:55:24+00:00,36ba82b8885b00a4ea2fd9e4b1d8cf304a8e78e591972682f0aa53717f648d19,https://investemoz.tempo.co.mz/banco-mundial-classifica-de-proibitivos-juros-praticados-em-mocambique,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://opais.sapo.mz/index.php/economia/38-economia/45893-banco-mundial-classifica-de-proibitivos-juros-praticados-em-mocambique.html | Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/Banco-Mundial-classifica-de-proibitivos-juros-praticados-em-Mo%C3%A7ambique.jpg | Referência não recolhida: https://www.facebook.com/196034093767187/posts/1460805843956666 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
173213,Bens de Consumo e Franquias,tempo,Oportunidades,2026-06-16T11:31:06+00:00,2026-06-16T11:32:10+00:00,abd5f36a0c2b89d2989334137aae5d59b3158da38d556013ccd55deaa5069e92,https://investemoz.tempo.co.mz/bens-de-consumo-franquias,29,"Publicado no ativo — setores-fintech-QA-d5b4dc354265 DOC01; importação privada, scan, metadados e publicação separados",inventariado; cadastro/versão por reconciliar,29,publicado,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Entrega concluída; nenhum novo envio ou republicação da imagem. Regulação específica Fintech permanece na matriz.,"Preservar publicação; restante acervo separado, sem repetir aceitação.",outputs/investmoz-fintech-investimento-QA-20261015/RESULTADO-QA.md; PROVA-DELTA-QA.json; HTTP-DELTA-QA.json; PROVA-FINAL-DELTA-QA.json; HTTP-RECUPERACAO-FINAL-QA.json; MANIFESTO-ENTREGA.json | outputs/investmoz-setores-fintech-ativacao-20261015/RELATORIO-ENTREGA.md; PUBLISHED.json; PRESERVACAO-FINAL.json; HTTP-OPERACIONAL.json; FRESH-BACKUP.json; RECIBO-FINAL.json
|
||||
172223,"Cabo Delgado - Pemba, A capital do turismo costeiro do norte de Moçambique",tempo,Turismo,2026-04-14T10:21:20+00:00,2026-04-14T10:41:05+00:00,c96b2430c44b8edc9cc1d0a7974c23290dd0207cff366fea8326bb28bcab8285,https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique/ | https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique,20,publicado,RecursoQA20; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,20,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
173482,Checklist para Contratação e Direitos do Trabalho Doméstico,tempo,Trabalho Doméstico,2026-09-15T12:36:11+00:00,2026-09-15T12:36:14+00:00,309b51ebc53bf840f9014da733bc09f8d162138be4e6e9d5a72670e26cc8e482,https://investemoz.tempo.co.mz/checklist-para-contratacao-e-direitos-do-trabalho-domestico,,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Conteúdo editorial não oficial; falta recorte e ligação ao consumidor/apoio apropriado. Eventual uso jurídico ou cálculo requer citação/âmbito, sem revalidar credibilidade de fontes oficiais já aceites.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
76474,"Coloque o seu número para o grupo de whatsapp, venda de acessórios de computador",tempo,Categoria,2017-08-08T17:29:51+00:00,2017-08-08T17:29:51+00:00,c8beeb94f936b3bfc69c720770bd84376e9f19b668d96185aaefbada05727010,https://investemoz.tempo.co.mz/coloque-o-seu-numero-para-o-grupo-de-whatsapp-venda-de-acessorios-de-computador,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: https://www.facebook.com/photo.php?fbid=741844132690980&set=gm.1566816196708735&type=3 | Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/Coloque-o-seu-n%C3%BAmero-para-o-grupo-de-whatsapp-venda-de-acess%C3%B3rios-de-computador.jpg | Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/1502293415_298_Coloque-o-seu-n%C3%BAmero-para-o-grupo-de-whatsapp-venda-de-acess%C3%B3rios-de-computador.jpg | Referência não recolhida: https://www.facebook.com/179463555444013/posts/1566816196708735 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
173484,Direitos Especiais da Trabalhadora Doméstica no Regulamento,tempo,Trabalho Doméstico,2026-09-15T12:38:08+00:00,2026-09-15T12:38:11+00:00,34c31bb02a3ad0886f12ac2853283de2ffb89a925be3cda73b7ce9385c6caec4,https://investemoz.tempo.co.mz/direitos-especiais-da-trabalhadora-domestica-no-regulamento,,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Conteúdo editorial não oficial; falta recorte e ligação ao consumidor/apoio apropriado. Eventual uso jurídico ou cálculo requer citação/âmbito, sem revalidar credibilidade de fontes oficiais já aceites.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
171981,Energia & Mineração,tempo,Oportunidades,2026-04-06T09:17:46+00:00,2026-04-30T13:35:32+00:00,e9a95f0c8176eea48a75bd2c5bce69f15308806988fa5203a688af7c40305e9c,https://investemoz.tempo.co.mz/energia-mineracao,7,publicado,RecursoQA7; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,7,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 23, ""sha256"": ""0a2c50d91dad3a6007544634540e0f61b11d3506682a5a19066bae03cdcba490""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
173222,Energia e Recursos Naturais,tempo,Oportunidades,2026-06-16T12:47:16+00:00,2026-06-16T12:51:30+00:00,6f8236b0441d61fce5171994ba6b82ffc8b5f2de3e8242de7be29248ed046ad2,https://investemoz.tempo.co.mz/energia-recursos-naturais,,existente por integrar,inventariado; cadastro/versão por reconciliar,,setorial,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Ausente da biblioteca ativa e QA; falta exemplar versionado no manifesto de um novo lote e extensão nominal do importador. | Slug ausente no mapa do leitor; falta ligação ao percurso/dossier fundamentada para este artigo. | Texto distinto de Energia/Mineração já publicado; preservar a ligação declarada /energia-mineracao e delimitar agrupamento, sem fundir os exemplares.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
62414,Frelimo e Zanu-PF: Evocados laços de irmandade,tempo,Categoria,2017-06-26T12:03:20+00:00,2017-06-26T12:03:20+00:00,013307965b8fcb349a0d936d844ed12437bda4c90389f8efe958d4ffdb9ce62b,https://investemoz.tempo.co.mz/frelimo-e-zanu-pf-evocados-lacos-de-irmandade,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência indisponível na captura (HTTP 404): https://investemoz.tempo.co.mz/images/ANO-2017/JUNHO/21/GOVERNADOR-in.gif | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
62418,Governo dará todo O apoio à PGR no seguimento da auditoria,tempo,Categoria,2017-06-26T12:30:37+00:00,2017-06-26T12:30:45+00:00,65299b6711499e417be778b51c7ab7880616b0ccbf362854370eb775fc54caf3,https://investemoz.tempo.co.mz/governo-dara-todo-o-apoio-a-pgr-no-seguimento-da-auditoria,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/a84c6c1e4d1780f63086a86817521b40 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
75002,Governo pretende construir 100 mil habitações em dois anos,tempo,Nas Redes,2017-08-04T20:01:55+00:00,2017-08-04T20:01:55+00:00,b32ea95039c2af387687738ed601f0947384eb7e19ca4bd282d603c60ca26afa,https://investemoz.tempo.co.mz/governo-pretende-construir-100-mil-habitacoes-em-dois-anos,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://opais.sapo.mz/index.php/sociedade/45-sociedade/45963-governo-pretende-construir-100-mil-habitacoes-em-dois-anos.html | Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/Governo-pretende-construir-100-mil-habita%C3%A7%C3%B5es-em-dois-anos.jpg | Referência não recolhida: https://www.facebook.com/196034093767187/posts/1464602060243711 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
173478,Guia Prático para Empregadores sobre Contratação e Direitos no Trabalho Doméstico,tempo,Trabalho Doméstico,2026-09-15T12:30:34+00:00,2026-09-15T12:30:37+00:00,8038d8c221ba3cbbc4712304a6b17a6bba8ad946c8bac1a7836d472944aaf930,https://investemoz.tempo.co.mz/guia-pratico-para-empregadores-sobre-contratacao-e-direitos-no-trabalho-domestico,,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Conteúdo editorial não oficial; falta recorte e ligação ao consumidor/apoio apropriado. Eventual uso jurídico ou cálculo requer citação/âmbito, sem revalidar credibilidade de fontes oficiais já aceites.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
173218,Imobiliário,tempo,Oportunidades,2026-06-16T11:34:45+00:00,2026-06-16T11:34:49+00:00,be8b606f087a939bd88a86f4436a0ff18c5fb774a179e6e915d4abb10a95f930,https://investemoz.tempo.co.mz/imobiliario,28,"Publicado no ativo — setores-fintech-QA-d5b4dc354265 DOC01; importação privada, scan, metadados e publicação separados",inventariado; cadastro/versão por reconciliar,28,publicado,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Entrega concluída; nenhum novo envio ou republicação da imagem. Regulação específica Fintech permanece na matriz.,"Preservar publicação; restante acervo separado, sem repetir aceitação.",outputs/investmoz-fintech-investimento-QA-20261015/RESULTADO-QA.md; PROVA-DELTA-QA.json; HTTP-DELTA-QA.json; PROVA-FINAL-DELTA-QA.json; HTTP-RECUPERACAO-FINAL-QA.json; MANIFESTO-ENTREGA.json | outputs/investmoz-setores-fintech-ativacao-20261015/RELATORIO-ENTREGA.md; PUBLISHED.json; PRESERVACAO-FINAL.json; HTTP-OPERACIONAL.json; FRESH-BACKUP.json; RECIBO-FINAL.json
|
||||
171992,Infraestruturas,tempo,Oportunidades,2026-04-06T09:25:07+00:00,2026-04-30T12:44:37+00:00,9716da16aba58eec173343f0bbd582d62d23a5eee7d20522c080c2da5da8ba66,https://investemoz.tempo.co.mz/infraestruturas,22,publicado,RecursoQA22; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,22,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 26, ""sha256"": ""8e58544e49e719f9c865c91e07ef1030155a9e3d59260e6dc93e9c9e25674e39""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
172240,"Maputo Cidade – Plataforma Estratégica de Turismo, Negócios e Investimento Sustentável",tempo,Turismo,2026-04-14T10:58:53+00:00,2026-04-14T11:03:28+00:00,0866639d09c5c0b3b00e8527554065587c793f60df978b66b6a578b3c741bc0d,https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel/ | https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel,16,publicado,RecursoQA16; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,16,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
171994,Mercado Financeiro,tempo,Oportunidades,2026-04-06T09:27:00+00:00,2026-04-17T09:53:42+00:00,e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,https://investemoz.tempo.co.mz/mercado-financeiro,23,vazio — dependência concreta,"RecursoQA23; original/hash/ClamD/replay demonstrados. Corpo vazio na origemHTML/Inertia200;privado,sem página publicada.",,mercado_financeiro,"{""status"": ""capturada mas não publicada no ativo"", ""sha256"": ""417b4a33468f360acef272ce3e4180c1d0a171980befa73ddb9a33de5a9d4c4e""}",Corpo do artigo vazio; imagem não substitui conteúdo.,"Manter pendente e separado, sem bloquear outros lotes.",outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
173480,Modelo de Contrato de Trabalho Doméstico para Pequenas Famílias,tempo,Trabalho Doméstico,2026-09-15T12:34:17+00:00,2026-09-15T12:34:19+00:00,5b95ad9010fcbdc1000052f47766ca54409d3e0649211729ced9ef142f5c6db0,https://investemoz.tempo.co.mz/modelo-de-contrato-de-trabalho-domestico-para-pequenas-familias,,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Conteúdo editorial não oficial; falta recorte e ligação ao consumidor/apoio apropriado. Eventual uso jurídico ou cálculo requer citação/âmbito, sem revalidar credibilidade de fontes oficiais já aceites.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
172219,Niassa - O último grande destino virgem do turismo africano,tempo,Turismo,2026-04-14T10:17:14+00:00,2026-04-14T10:43:56+00:00,47d82a59a86c3cbdef04ea065e711e7139d50137e440f03efbd710e3c4220ef7,https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano/ | https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano,21,publicado,RecursoQA21; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,21,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
173476,Procedimentos para Rescisão do Contrato de Trabalho Doméstico segundo o Regulamento de Moçambique,tempo,Trabalho Doméstico,2026-09-15T11:39:21+00:00,2026-09-15T11:39:24+00:00,bb634a9021da9d7b46ed105317aa54e4af42a16dab042e0ceb2bdc0d702d1a67,https://investemoz.tempo.co.mz/procedimentos-para-rescisao-do-contrato-de-trabalho-domestico-segundo-o-regulamento-de-mocambique,,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Conteúdo editorial não oficial; falta recorte e ligação ao consumidor/apoio apropriado. Eventual uso jurídico ou cálculo requer citação/âmbito, sem revalidar credibilidade de fontes oficiais já aceites.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
172263,"Província da Zambézia: Onde os Corredores de Moçambique Convergem com a Natureza, a Cultura e o Turismo Sustentável",tempo,Turismo,2026-04-14T14:21:52+00:00,2026-04-14T14:26:27+00:00,9e7ca4e9542135d5969f5c008ce8e29b037cc06a53875cfa7dc3e7452ec47744,https://investemoz.tempo.co.mz/provincia-da-zambezia-onde-os-corredores-de-mocambique-convergem-com-a-natureza-a-cultura-e-o-turismo-sustentavel,11,publicado,RecursoQA11; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,11,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
172233,Província de Gaza: Oportunidades mais sólidas e estratégicas de investimento no turismo no sul de Moçambique.,tempo,Turismo,2026-04-14T10:52:30+00:00,2026-04-17T08:28:56+00:00,2bf2df9bd3b7ab8656209a48f4928b5d8bbf2959e77c292fc4c9987e67ba2fea,https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique/ | https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique,18,publicado,RecursoQA18; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,18,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
172253,Província de Inhambane: Um Destino Estratégico para Investimento Turístico,tempo,Turismo,2026-04-14T14:15:12+00:00,2026-04-14T14:25:44+00:00,2d1cc729481ce65a5e50898c0eea4a1eb2418273b03fc749009e2ffaaa72fe71,https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico/ | https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico,15,publicado,RecursoQA15; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,15,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
172258,"Província de Manica: Onde o Turismo de Montanha, Natureza e Cultura Encontra o Próximo Ciclo de Investimento",tempo,Turismo,2026-04-14T14:18:28+00:00,2026-04-17T10:42:16+00:00,4c8bdd79212ba98fa347733e2f58aef8796b29cc8cb8b649d200bd37435df1c3,https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento/ | https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento,14,publicado,RecursoQA14; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,14,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
172237,"Província de Maputo: Natureza, Cultura e Conectividade como Motores de Crescimento Sustentável",tempo,Turismo,2026-04-14T10:55:21+00:00,2026-04-17T08:27:24+00:00,d112d0e535912f4487c2b8d41978e5e663f1db43f1a2cdf170dbbec535f13f27,https://investemoz.tempo.co.mz/provincia-de-maputo-turismo/ | https://investemoz.tempo.co.mz/provincia-de-maputo-turismo,17,publicado,RecursoQA17; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,17,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
172230,Província de Nampula - Perfil de investimento no setor do turismo,tempo,Turismo,2026-04-14T10:50:32+00:00,2026-04-14T11:10:23+00:00,d18ccc1e036b3480605462ce7176be75153f6aa988bc9b37d8d395b01e555a7a,https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo/ | https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo,19,publicado,RecursoQA19; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,19,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
172256,"Província de Sofala: Onde a Natureza, a História e o Futuro do Turismo Convergem",tempo,Turismo,2026-04-14T14:16:47+00:00,2026-04-14T14:26:17+00:00,12b46a34ba61c6ef0058a6c410ebc17329d897fe4b06a13b4ea3fe3c85a28342,https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem/ | https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem,12,publicado,RecursoQA12; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,12,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
172261,"Província de Tete: Onde o Rio Zambeze, a Cultura Viva e a Conservação Comunitária Criam o Futuro do Turismo",tempo,Turismo,2026-04-14T14:19:48+00:00,2026-04-14T14:26:25+00:00,e78fce2931260a7d6f82270897e906feb640f65d084c2d4ffb5ba19303858275,https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo/ | https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo,13,publicado,RecursoQA13; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,13,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
73246,Rofino Licuco preso cinco meses depois de condenado por agressão,tempo,Nas Redes,2017-07-28T20:23:11+00:00,2017-07-28T20:23:11+00:00,806c54948641f4f5933a2ea4a6935dfb7a67202ab227960451d62435880289f6,https://investemoz.tempo.co.mz/rofino-licuco-preso-cinco-meses-depois-de-condenado-por-agressao,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://opais.sapo.mz/index.php/sociedade/45-sociedade/45870-rofino-licuco-preso-cinco-meses-depois-de-condenado-por-agressao-.html | Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/Rofino-Licuco-preso-cinco-meses-depois-de-condenado-por-agress%C3%A3o.jpg | Referência não recolhida: https://www.facebook.com/196034093767187/posts/1458270617543522 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
77232,Sete indivíduos mortos a tiro na Matola,tempo,Nas Redes,2017-08-11T20:30:32+00:00,2017-08-11T20:30:32+00:00,e657a97d5d6db7fa0ea6b7d14029ff873ea5d36dfdadda5f8b9bc8f1ef5dd011,https://investemoz.tempo.co.mz/sete-individuos-mortos-a-tiro-na-matola,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://opais.sapo.mz/index.php/sociedade/45-sociedade/46074-sete-individuos-mortos-a-tiro-na-matola-.html | Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/Sete-indiv%C3%ADduos-mortos-a-tiro-na-Matola.jpg | Referência não recolhida: https://www.facebook.com/196034093767187/posts/1471269102910340 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
62416,Sete pessoas morrem e 150 são resgatados em acidente com eléctrico na Índia,tempo,Categoria,2017-06-26T12:14:48+00:00,2017-06-26T12:14:48+00:00,0fe20a9ca9dd99c58072f43b3dece1de347ef4063ee5f75bcffe8b2f4bf253b5,https://investemoz.tempo.co.mz/sete-pessoas-morrem-e-150-sao-resgatados-em-acidente-com-electrico-na-india,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://www.verdade.co.mz/internacional/62604-sete-pessoas-morrem-e-150-sao-resgatados-em-acidente-com-electrico-na-india | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
173209,Fintech,tempo,Oportunidades,2026-06-16T11:24:22+00:00,2026-06-23T10:03:06+00:00,8db9206bf5192b68e7c9bdfb87e9dc9c7063a4b7eca56630685b0e6c294f55ab,https://investemoz.tempo.co.mz/tech-fintech,27,"Publicado no ativo — setores-fintech-QA-d5b4dc354265 DOC01; importação privada, scan, metadados e publicação separados",inventariado; cadastro/versão por reconciliar,27,publicado,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Entrega concluída; nenhum novo envio ou republicação da imagem. Regulação específica Fintech permanece na matriz.,"Preservar publicação; restante acervo separado, sem repetir aceitação.",outputs/investmoz-fintech-investimento-QA-20261015/RESULTADO-QA.md; PROVA-DELTA-QA.json; HTTP-DELTA-QA.json; PROVA-FINAL-DELTA-QA.json; HTTP-RECUPERACAO-FINAL-QA.json; MANIFESTO-ENTREGA.json | outputs/investmoz-setores-fintech-ativacao-20261015/RELATORIO-ENTREGA.md; PUBLISHED.json; PRESERVACAO-FINAL.json; HTTP-OPERACIONAL.json; FRESH-BACKUP.json; RECIBO-FINAL.json
|
||||
171990,Turismo,tempo,Oportunidades,2026-04-06T09:22:51+00:00,2026-06-16T13:07:59+00:00,604d30a740cc6ed20d24e3447806e733aa4aefb9f339f81d3dc57b644dd19fb7,https://investemoz.tempo.co.mz/turismo,10,publicado,RecursoQA10; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,10,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 25, ""sha256"": ""fe9a2e382e385b35f6e61ae320e199a979678677fd327fb7d4b07d8ce0111c11""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
74364,,tempo,Categoria,2017-07-26T21:20:33+00:00,2017-07-26T21:20:33+00:00,2a7f0a5b09e0136fec9eae357ac8a88a159ead23b25ed7bb735c75997b10ec5e,https://investemoz.tempo.co.mz/74364-2,,dependência concreta — título ausente,inventariado; cadastro/versão por reconciliar,,identidade_incompleta,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/1501695692_133_notitle.jpg | Referência não recolhida: https://www.facebook.com/179463555444013/posts/1552815684775453 | Título vazio no post 74364; corpo com 20 caracteres; identidade editorial a esclarecer sem inventar título.,Preservar exemplar e referências; delimitar recuperação editorial antes de integrar.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
62929,Daúde Razaque já não é treinador da Liga Desportiva de Maputo,tempo,Nas Redes,2017-06-27T16:41:43+00:00,2017-06-27T16:41:43+00:00,c3b57ea685290bfb01c50403bc83db98cf1a42ad51c04d5b8d288c99b8c0a808,https://investemoz.tempo.co.mz/daude-razaque-ja-nao-e-treinador-da-liga-desportiva-de-maputo,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://opais.sapo.mz/index.php/desporto/74-desporto/45358-daude-razaque-ja-nao-e-treinador-da-liga-desportiva-de-maputo.html | Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/Da%C3%BAde-Razaque-j%C3%A1-n%C3%A3o-%C3%A9-treinador-da-Liga-Desportiva-de-Maputo.jpg | Referência não recolhida: https://www.facebook.com/196034093767187/posts/1425759480794636 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
62649,Daviz Simango não se incomoda com possível sucessão na Beira,tempo,Nas Redes,2017-06-26T19:38:09+00:00,2017-06-26T19:38:09+00:00,090e5f41ed11c8e680d2e30ea5c8310b7c6b165ddec318a8b0cf94927f78da74,https://investemoz.tempo.co.mz/daviz-simango-nao-se-incomoda-com-possivel-sucessao-na-beira,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://opais.sapo.mz/index.php/politica/63-politica/45340-daviz-simango-nao-se-incomoda-com-possivel-sucessao-na-beira-.html | Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/Daviz-Simango-n%C3%A3o-se-incomoda-com-poss%C3%ADvel-sucess%C3%A3o-na-Beira.jpg | Referência não recolhida: https://www.facebook.com/196034093767187/posts/1424683617568889 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
173470,Direitos e Deveres do Trabalhador Doméstico,tempo,Trabalho Doméstico,2026-09-15T11:28:25+00:00,2026-09-15T11:28:28+00:00,f69fd6fd8509913f2d79df0267c469dc7a9d0d30420ce8428749d53ba21446ab,https://investemoz.tempo.co.mz/direitos-e-deveres-do-trabalhador-domestico,,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Conteúdo editorial não oficial; falta recorte e ligação ao consumidor/apoio apropriado. Eventual uso jurídico ou cálculo requer citação/âmbito, sem revalidar credibilidade de fontes oficiais já aceites.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
172679,Energia,tempo,Featured,2026-04-30T13:03:57+00:00,2026-04-30T13:28:48+00:00,e2a7eb4be0a2cf4139737f6428c147ac35de99899478ce79195dc5e1b0a960f3,https://investemoz.tempo.co.mz/energia | https://investemoz.tempo.co.mz/energia/,8,publicado,RecursoQA8; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,8,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
62739,"“Exigimos responsabilização dos agentes do crime”, Daviz Simango",tempo,Nas Redes,2017-06-26T23:13:36+00:00,2017-06-26T23:13:36+00:00,e37ea18176246dab529d2ecec7734f0fb2b0a54cadc507fbd789033d5b8e5b93,https://investemoz.tempo.co.mz/exigimos-responsabilizacao-dos-agentes-do-crime-daviz-simango-2,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://opais.sapo.mz/index.php/politica/63-politica/45345-exigimos-responsabilizacao-dos-agentes-do-crime-daviz-simango.html | Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/%E2%80%9CExigimos-responsabiliza%C3%A7%C3%A3o-dos-agentes-do-crime%E2%80%9D-Daviz-Simango.jpg | Referência não recolhida: https://www.facebook.com/196034093767187/posts/1424873994216518 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
62854,Governo e parceiros reforçam mecanismos de controlo de HIV/Sida,tempo,Nas Redes,2017-06-27T11:07:53+00:00,2017-06-27T11:07:53+00:00,0073816b1fa4bd50ec6417b07272da06565230d06a19079ca76cb006c882d2f1,https://investemoz.tempo.co.mz/governo-e-parceiros-reforcam-mecanismos-de-controlo-de-hivsida,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://opais.sapo.mz/index.php/sociedade/45-sociedade/45352-governo-e-parceiros-reforcam-mecanismos-de-controlo-de-hivsida.html | Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/Governo-e-parceiros-refor%C3%A7am-mecanismos-de-controlo-de-HIVSida.jpg | Referência não recolhida: https://www.facebook.com/196034093767187/posts/1425451974158720 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
63128,Graça Machel diz que desnutrição é um problema de desenvolvimento,tempo,Nas Redes,2017-06-27T22:01:59+00:00,2017-06-27T22:01:59+00:00,711f3707e3a0d72ec919216fc50823de9060349104ee98fe0a6b20d6684b6031,https://investemoz.tempo.co.mz/graca-machel-diz-que-desnutricao-e-um-problema-de-desenvolvimento,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://opais.sapo.mz/index.php/sociedade/45-sociedade/45368-graca-machel-diz-que-desnutricao-e-um-problema-de-desenvolvimento.html | Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/Gra%C3%A7a-Machel-diz-que-desnutri%C3%A7%C3%A3o-%C3%A9-um-problema-de-desenvolvimento.jpg | Referência não recolhida: https://www.facebook.com/196034093767187/posts/1426040144099903 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
63181,In dependência,tempo,Nas Redes,2017-06-27T21:38:06+00:00,2017-06-27T21:38:06+00:00,b0d87c0d58d6d1343d102cd3141bda1215c5afd54f3780a1c6c262d786dbfe27,https://investemoz.tempo.co.mz/in-dependencia,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://opais.sapo.mz/index.php/opiniao/182-helder-faife/45364-in-dependencia.html | Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/In-depend%C3%AAncia.jpg | Referência não recolhida: https://www.facebook.com/196034093767187/posts/1426022220768362 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
63146,"Kroll não descobriu como foram gastos 2 biliões de dólares; Governo de Nyusi não apoiou a Auditoria; criação da Proindicus, EMATUM e MAM não foi para proteção da Zona Económica Exclusiva",tempo,Nas Redes,2017-06-27T19:30:01+00:00,2017-06-27T19:30:01+00:00,4ccbad5c92ddc3884f58181a4f1ed3c1d3d6a59f005bfd99b197a6fc1fc3c280,https://investemoz.tempo.co.mz/kroll-nao-descobriu-como-foram-gastos-2-bilioes-de-dolares-governo-de-nyusi-nao-apoiou-a-auditoria-criacao-da-proindicus-ematum-e-mam-nao-foi-para-protecao-da-zona-economica-exclusiva-2,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://www.verdade.co.mz/tema-de-fundo/35/62615 | Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/Kroll-n%C3%A3o-descobriu-como-foram-gastos-2-bili%C3%B5es-de-d%C3%B3lares-Governo-de-Nyusi-n%C3%A3o-apoiou-a-Auditoria-cria%C3%A7%C3%A3o-da-Proindicus-EMATUM-e-MAM-n%C3%A3o-foi-para-prote%C3%A7%C3%A3o-da-Zona-Econ%C3%B3mica-Exclusiva.jpg | Referência não recolhida: https://www.facebook.com/113487338672967/posts/1492451004109920 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
172674,Mineração,tempo,Featured,2026-04-30T13:12:09+00:00,2026-04-30T13:30:13+00:00,8d3acc292ed806bc4cc7bcb6a3a6a5759f30b610766a5ef4b3d6a0b7c56d5861,https://investemoz.tempo.co.mz/mineracao,9,publicado,RecursoQA9; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,9,publicados,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",,Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
62688,O Instituto Nacional de Meteorologia prevê o seguinte estado do tempo para esta ...,tempo,Nas Redes,2017-06-26T18:21:18+00:00,2017-06-26T18:21:18+00:00,396fe2575641622015505eb11d4c7152b6e3e1b741966f14c0cd1673fd455fd3,https://investemoz.tempo.co.mz/o-instituto-nacional-de-meteorologia-preve-o-seguinte-estado-do-tempo-para-esta-29,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://www.verdade.co.mz/newsflash/62607 | Referência não recolhida: https://www.facebook.com/113487338672967/posts/1491493337539020 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
173474,Sanções Disciplinares Aplicáveis a Trabalhadores Domésticos segundo o Regulamento de Moçambique,tempo,Trabalho Doméstico,2026-09-15T11:37:04+00:00,2026-09-15T11:37:06+00:00,e09a0fe71397e2b060ff8435111117aa487b476a85ec522fffcdf9e23a6cdac2,https://investemoz.tempo.co.mz/sancoes-disciplinares-aplicaveis-a-trabalhadores-domesticos-segundo-o-regulamento-de-mocambique,,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Conteúdo editorial não oficial; falta recorte e ligação ao consumidor/apoio apropriado. Eventual uso jurídico ou cálculo requer citação/âmbito, sem revalidar credibilidade de fontes oficiais já aceites.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
63248,Dois alunos da 9ª classe caíram nas mãos da Polícia da República de Moçambique (...,tempo,Nas Redes,2017-06-28T11:30:01+00:00,2017-06-28T11:30:01+00:00,2220ca2c07d10f10b2bd6fd0f103ee733b83570d436c751c449c85b42df46a13,https://investemoz.tempo.co.mz/dois-alunos-da-9a-classe-cairam-nas-maos-da-policia-da-republica-de-mocambique,,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Referência não recolhida: http://www.verdade.co.mz/newsflash/62625 | Referência não recolhida: https://www.facebook.com/113487338672967/posts/1493133914041629 | Notícia/anúncio histórico; definir destino de consulta e prioridade pela Tempo, sem converter em orientação atual ou oportunidade vigente.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
173463,Regulamento do Trabalho Doméstico,tempo,Trabalho Doméstico,2026-09-15T10:56:07+00:00,2026-09-15T12:41:44+00:00,eb43b6c1f097631250f0b36d3d01b4d106db2fcae07a56bb154819143fff11e9,https://investemoz.tempo.co.mz/regulamento-do-trabalho-domestico,,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}","Conteúdo editorial não oficial; falta recorte e ligação ao consumidor/apoio apropriado. Eventual uso jurídico ou cálculo requer citação/âmbito, sem revalidar credibilidade de fontes oficiais já aceites.",Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação.,outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY)
|
||||
ID legado,Título,Autor,Categoria,Publicado,Modificado,SHA256 conteúdo,URLs observadas,Recurso QA,Situação,Situação anterior à reconciliação 15/10/2026,Recurso ativo,Grupo restante,Imagem atual,Dependência concreta,Próxima ação,Evidência atual,Endereço no novo portal,Estado actual consolidado,Pendência actual,Conferência HTTP UTC,Grupo de preparação QA
|
||||
171985,Agronegocio,tempo,Oportunidades,2026-04-06T09:19:07+00:00,2026-04-20T12:41:43+00:00,28383a11d95f13894381357d8f1227b33031d1d3ff74507da4ea8e5ce2470913,https://investemoz.tempo.co.mz/agronegocio,6,publicado,recurso único QA; não é oportunidade,13,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/12/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/agronegocio,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
62413,15/30 costa do sol com dua na zona de mupulene muito lindo,tempo,Categoria,2017-06-26T11:44:21+00:00,2017-06-26T11:44:21+00:00,c04ae821317ef556c01c3141ce75eeeeb5da7787e0c6849b1b3709b757edb9eb,https://investemoz.tempo.co.mz/1530-costa-do-sol-com-dua-na-zona-de-mupulene-muito-lindo,43,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. Rever/corrigir a nota de demonstração QA antes de publicar.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. Rever/corrigir a nota de demonstração QA antes de publicar.,,historico_noticioso
|
||||
173220,Alumínio “Verde”,tempo,Oportunidades,2026-06-16T12:41:02+00:00,2026-06-16T12:41:06+00:00,c61f05a56bb5a5f13d9d5d54dbefd270cf9ac40320917d77aaad76cac56c7b97,https://investemoz.tempo.co.mz/aluminio-verde,51,existente por integrar,inventariado; cadastro/versão por reconciliar,,setorial,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,setorial
|
||||
73945,Banco Mundial classifica de proibitivos juros praticados em Moçambique,tempo,Nas Redes,2017-07-31T19:55:24+00:00,2017-07-31T19:55:24+00:00,36ba82b8885b00a4ea2fd9e4b1d8cf304a8e78e591972682f0aa53717f648d19,https://investemoz.tempo.co.mz/banco-mundial-classifica-de-proibitivos-juros-praticados-em-mocambique,44,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
173213,Bens de Consumo e Franquias,tempo,Oportunidades,2026-06-16T11:31:06+00:00,2026-06-16T11:32:10+00:00,abd5f36a0c2b89d2989334137aae5d59b3158da38d556013ccd55deaa5069e92,https://investemoz.tempo.co.mz/bens-de-consumo-franquias,29,publicado,inventariado; cadastro/versão por reconciliar,29,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/bens-de-consumo-franquias,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
172223,"Cabo Delgado - Pemba, A capital do turismo costeiro do norte de Moçambique",tempo,Turismo,2026-04-14T10:21:20+00:00,2026-04-14T10:41:05+00:00,c96b2430c44b8edc9cc1d0a7974c23290dd0207cff366fea8326bb28bcab8285,https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique/ | https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique,20,publicado,RecursoQA20; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,25,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
173482,Checklist para Contratação e Direitos do Trabalho Doméstico,tempo,Trabalho Doméstico,2026-09-15T12:36:11+00:00,2026-09-15T12:36:14+00:00,309b51ebc53bf840f9014da733bc09f8d162138be4e6e9d5a72670e26cc8e482,https://investemoz.tempo.co.mz/checklist-para-contratacao-e-direitos-do-trabalho-domestico,52,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,trabalho_domestico
|
||||
76474,"Coloque o seu número para o grupo de whatsapp, venda de acessórios de computador",tempo,Categoria,2017-08-08T17:29:51+00:00,2017-08-08T17:29:51+00:00,c8beeb94f936b3bfc69c720770bd84376e9f19b668d96185aaefbada05727010,https://investemoz.tempo.co.mz/coloque-o-seu-numero-para-o-grupo-de-whatsapp-venda-de-acessorios-de-computador,45,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
173484,Direitos Especiais da Trabalhadora Doméstica no Regulamento,tempo,Trabalho Doméstico,2026-09-15T12:38:08+00:00,2026-09-15T12:38:11+00:00,34c31bb02a3ad0886f12ac2853283de2ffb89a925be3cda73b7ce9385c6caec4,https://investemoz.tempo.co.mz/direitos-especiais-da-trabalhadora-domestica-no-regulamento,64,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,trabalho_domestico
|
||||
171981,Energia & Mineração,tempo,Oportunidades,2026-04-06T09:17:46+00:00,2026-04-30T13:35:32+00:00,e9a95f0c8176eea48a75bd2c5bce69f15308806988fa5203a688af7c40305e9c,https://investemoz.tempo.co.mz/energia-mineracao,7,publicado,RecursoQA7; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,18,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/14/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/energia-mineracao,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
173222,Energia e Recursos Naturais,tempo,Oportunidades,2026-06-16T12:47:16+00:00,2026-06-16T12:51:30+00:00,6f8236b0441d61fce5171994ba6b82ffc8b5f2de3e8242de7be29248ed046ad2,https://investemoz.tempo.co.mz/energia-recursos-naturais,63,existente por integrar,inventariado; cadastro/versão por reconciliar,,setorial,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,setorial
|
||||
62414,Frelimo e Zanu-PF: Evocados laços de irmandade,tempo,Categoria,2017-06-26T12:03:20+00:00,2017-06-26T12:03:20+00:00,013307965b8fcb349a0d936d844ed12437bda4c90389f8efe958d4ffdb9ce62b,https://investemoz.tempo.co.mz/frelimo-e-zanu-pf-evocados-lacos-de-irmandade,53,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
62418,Governo dará todo O apoio à PGR no seguimento da auditoria,tempo,Categoria,2017-06-26T12:30:37+00:00,2017-06-26T12:30:45+00:00,65299b6711499e417be778b51c7ab7880616b0ccbf362854370eb775fc54caf3,https://investemoz.tempo.co.mz/governo-dara-todo-o-apoio-a-pgr-no-seguimento-da-auditoria,54,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
75002,Governo pretende construir 100 mil habitações em dois anos,tempo,Nas Redes,2017-08-04T20:01:55+00:00,2017-08-04T20:01:55+00:00,b32ea95039c2af387687738ed601f0947384eb7e19ca4bd282d603c60ca26afa,https://investemoz.tempo.co.mz/governo-pretende-construir-100-mil-habitacoes-em-dois-anos,46,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
173478,Guia Prático para Empregadores sobre Contratação e Direitos no Trabalho Doméstico,tempo,Trabalho Doméstico,2026-09-15T12:30:34+00:00,2026-09-15T12:30:37+00:00,8038d8c221ba3cbbc4712304a6b17a6bba8ad946c8bac1a7836d472944aaf930,https://investemoz.tempo.co.mz/guia-pratico-para-empregadores-sobre-contratacao-e-direitos-no-trabalho-domestico,65,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,trabalho_domestico
|
||||
173218,Imobiliário,tempo,Oportunidades,2026-06-16T11:34:45+00:00,2026-06-16T11:34:49+00:00,be8b606f087a939bd88a86f4436a0ff18c5fb774a179e6e915d4abb10a95f930,https://investemoz.tempo.co.mz/imobiliario,28,publicado,inventariado; cadastro/versão por reconciliar,7,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/imobiliario,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
171992,Infraestruturas,tempo,Oportunidades,2026-04-06T09:25:07+00:00,2026-04-30T12:44:37+00:00,9716da16aba58eec173343f0bbd582d62d23a5eee7d20522c080c2da5da8ba66,https://investemoz.tempo.co.mz/infraestruturas,22,publicado,RecursoQA22; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,17,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/16/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/infraestruturas,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
172240,"Maputo Cidade – Plataforma Estratégica de Turismo, Negócios e Investimento Sustentável",tempo,Turismo,2026-04-14T10:58:53+00:00,2026-04-14T11:03:28+00:00,0866639d09c5c0b3b00e8527554065587c793f60df978b66b6a578b3c741bc0d,https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel/ | https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel,16,publicado,RecursoQA16; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,21,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
171994,Mercado Financeiro,tempo,Oportunidades,2026-04-06T09:27:00+00:00,2026-04-17T09:53:42+00:00,e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855,https://investemoz.tempo.co.mz/mercado-financeiro,23,vazio — dependência concreta,"RecursoQA23; original/hash/ClamD/replay demonstrados. Corpo vazio na origemHTML/Inertia200;privado,sem página publicada.",,mercado_financeiro,"{""status"": ""capturada mas não publicada no ativo"", ""sha256"": ""417b4a33468f360acef272ce3e4180c1d0a171980befa73ddb9a33de5a9d4c4e""}",Corpo do artigo vazio; imagem não substitui conteúdo.,"Manter pendente e separado, sem bloquear outros lotes.",outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,,Original vazio,Fornecer corpo editorial pela Tempo; imagem e Fintech não substituem o original. Sem novo endereço público.,,
|
||||
173480,Modelo de Contrato de Trabalho Doméstico para Pequenas Famílias,tempo,Trabalho Doméstico,2026-09-15T12:34:17+00:00,2026-09-15T12:34:19+00:00,5b95ad9010fcbdc1000052f47766ca54409d3e0649211729ced9ef142f5c6db0,https://investemoz.tempo.co.mz/modelo-de-contrato-de-trabalho-domestico-para-pequenas-familias,66,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,trabalho_domestico
|
||||
172219,Niassa - O último grande destino virgem do turismo africano,tempo,Turismo,2026-04-14T10:17:14+00:00,2026-04-14T10:43:56+00:00,47d82a59a86c3cbdef04ea065e711e7139d50137e440f03efbd710e3c4220ef7,https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano/ | https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano,21,publicado,RecursoQA21; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,26,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
173476,Procedimentos para Rescisão do Contrato de Trabalho Doméstico segundo o Regulamento de Moçambique,tempo,Trabalho Doméstico,2026-09-15T11:39:21+00:00,2026-09-15T11:39:24+00:00,bb634a9021da9d7b46ed105317aa54e4af42a16dab042e0ceb2bdc0d702d1a67,https://investemoz.tempo.co.mz/procedimentos-para-rescisao-do-contrato-de-trabalho-domestico-segundo-o-regulamento-de-mocambique,67,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,trabalho_domestico
|
||||
172263,"Província da Zambézia: Onde os Corredores de Moçambique Convergem com a Natureza, a Cultura e o Turismo Sustentável",tempo,Turismo,2026-04-14T14:21:52+00:00,2026-04-14T14:26:27+00:00,9e7ca4e9542135d5969f5c008ce8e29b037cc06a53875cfa7dc3e7452ec47744,https://investemoz.tempo.co.mz/provincia-da-zambezia-onde-os-corredores-de-mocambique-convergem-com-a-natureza-a-cultura-e-o-turismo-sustentavel,11,publicado,RecursoQA11; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,3,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/provincia-da-zambezia-onde-os-corredores-de-mocambique-convergem-com-a-natureza-a-cultura-e-o-turismo-sustentavel,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
172233,Província de Gaza: Oportunidades mais sólidas e estratégicas de investimento no turismo no sul de Moçambique.,tempo,Turismo,2026-04-14T10:52:30+00:00,2026-04-17T08:28:56+00:00,2bf2df9bd3b7ab8656209a48f4928b5d8bbf2959e77c292fc4c9987e67ba2fea,https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique/ | https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique,18,publicado,RecursoQA18; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,23,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
172253,Província de Inhambane: Um Destino Estratégico para Investimento Turístico,tempo,Turismo,2026-04-14T14:15:12+00:00,2026-04-14T14:25:44+00:00,2d1cc729481ce65a5e50898c0eea4a1eb2418273b03fc749009e2ffaaa72fe71,https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico/ | https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico,15,publicado,RecursoQA15; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,6,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
172258,"Província de Manica: Onde o Turismo de Montanha, Natureza e Cultura Encontra o Próximo Ciclo de Investimento",tempo,Turismo,2026-04-14T14:18:28+00:00,2026-04-17T10:42:16+00:00,4c8bdd79212ba98fa347733e2f58aef8796b29cc8cb8b649d200bd37435df1c3,https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento/ | https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento,14,publicado,RecursoQA14; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,20,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
172237,"Província de Maputo: Natureza, Cultura e Conectividade como Motores de Crescimento Sustentável",tempo,Turismo,2026-04-14T10:55:21+00:00,2026-04-17T08:27:24+00:00,d112d0e535912f4487c2b8d41978e5e663f1db43f1a2cdf170dbbec535f13f27,https://investemoz.tempo.co.mz/provincia-de-maputo-turismo/ | https://investemoz.tempo.co.mz/provincia-de-maputo-turismo,17,publicado,RecursoQA17; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,22,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/provincia-de-maputo-turismo,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
172230,Província de Nampula - Perfil de investimento no setor do turismo,tempo,Turismo,2026-04-14T10:50:32+00:00,2026-04-14T11:10:23+00:00,d18ccc1e036b3480605462ce7176be75153f6aa988bc9b37d8d395b01e555a7a,https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo/ | https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo,19,publicado,RecursoQA19; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,24,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
172256,"Província de Sofala: Onde a Natureza, a História e o Futuro do Turismo Convergem",tempo,Turismo,2026-04-14T14:16:47+00:00,2026-04-14T14:26:17+00:00,12b46a34ba61c6ef0058a6c410ebc17329d897fe4b06a13b4ea3fe3c85a28342,https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem/ | https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem,12,publicado,RecursoQA12; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,4,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
172261,"Província de Tete: Onde o Rio Zambeze, a Cultura Viva e a Conservação Comunitária Criam o Futuro do Turismo",tempo,Turismo,2026-04-14T14:19:48+00:00,2026-04-14T14:26:25+00:00,e78fce2931260a7d6f82270897e906feb640f65d084c2d4ffb5ba19303858275,https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo/ | https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo,13,publicado,RecursoQA13; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,5,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
73246,Rofino Licuco preso cinco meses depois de condenado por agressão,tempo,Nas Redes,2017-07-28T20:23:11+00:00,2017-07-28T20:23:11+00:00,806c54948641f4f5933a2ea4a6935dfb7a67202ab227960451d62435880289f6,https://investemoz.tempo.co.mz/rofino-licuco-preso-cinco-meses-depois-de-condenado-por-agressao,47,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
77232,Sete indivíduos mortos a tiro na Matola,tempo,Nas Redes,2017-08-11T20:30:32+00:00,2017-08-11T20:30:32+00:00,e657a97d5d6db7fa0ea6b7d14029ff873ea5d36dfdadda5f8b9bc8f1ef5dd011,https://investemoz.tempo.co.mz/sete-individuos-mortos-a-tiro-na-matola,48,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
62416,Sete pessoas morrem e 150 são resgatados em acidente com eléctrico na Índia,tempo,Categoria,2017-06-26T12:14:48+00:00,2017-06-26T12:14:48+00:00,0fe20a9ca9dd99c58072f43b3dece1de347ef4063ee5f75bcffe8b2f4bf253b5,https://investemoz.tempo.co.mz/sete-pessoas-morrem-e-150-sao-resgatados-em-acidente-com-electrico-na-india,55,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
173209,Fintech,tempo,Oportunidades,2026-06-16T11:24:22+00:00,2026-06-23T10:03:06+00:00,8db9206bf5192b68e7c9bdfb87e9dc9c7063a4b7eca56630685b0e6c294f55ab,https://investemoz.tempo.co.mz/tech-fintech,27,publicado,inventariado; cadastro/versão por reconciliar,28,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/tech-fintech,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
171990,Turismo,tempo,Oportunidades,2026-04-06T09:22:51+00:00,2026-06-16T13:07:59+00:00,604d30a740cc6ed20d24e3447806e733aa4aefb9f339f81d3dc57b644dd19fb7,https://investemoz.tempo.co.mz/turismo,10,publicado,RecursoQA10; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,19,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/15/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/turismo,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
74364,,tempo,Categoria,2017-07-26T21:20:33+00:00,2017-07-26T21:20:33+00:00,2a7f0a5b09e0136fec9eae357ac8a88a159ead23b25ed7bb735c75997b10ec5e,https://investemoz.tempo.co.mz/74364-2,,dependência concreta — título ausente,inventariado; cadastro/versão por reconciliar,,identidade_incompleta,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Referência não recolhida: http://www.newsaiep.com/moz_news/wp-content/uploads/1501695692_133_notitle.jpg | Referência não recolhida: https://www.facebook.com/179463555444013/posts/1552815684775453 | Título vazio no post 74364; corpo com 20 caracteres; identidade editorial a esclarecer sem inventar título.,Preservar exemplar e referências; delimitar recuperação editorial antes de integrar.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,,Original sem título,Identificação editorial pela Tempo para o post 74364; corpo com 21 caracteres úteis preservado. Sem título ou endereço novo inventado.,,
|
||||
62929,Daúde Razaque já não é treinador da Liga Desportiva de Maputo,tempo,Nas Redes,2017-06-27T16:41:43+00:00,2017-06-27T16:41:43+00:00,c3b57ea685290bfb01c50403bc83db98cf1a42ad51c04d5b8d288c99b8c0a808,https://investemoz.tempo.co.mz/daude-razaque-ja-nao-e-treinador-da-liga-desportiva-de-maputo,49,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
62649,Daviz Simango não se incomoda com possível sucessão na Beira,tempo,Nas Redes,2017-06-26T19:38:09+00:00,2017-06-26T19:38:09+00:00,090e5f41ed11c8e680d2e30ea5c8310b7c6b165ddec318a8b0cf94927f78da74,https://investemoz.tempo.co.mz/daviz-simango-nao-se-incomoda-com-possivel-sucessao-na-beira,56,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
173470,Direitos e Deveres do Trabalhador Doméstico,tempo,Trabalho Doméstico,2026-09-15T11:28:25+00:00,2026-09-15T11:28:28+00:00,f69fd6fd8509913f2d79df0267c469dc7a9d0d30420ce8428749d53ba21446ab,https://investemoz.tempo.co.mz/direitos-e-deveres-do-trabalhador-domestico,68,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,trabalho_domestico
|
||||
172679,Energia,tempo,Featured,2026-04-30T13:03:57+00:00,2026-04-30T13:28:48+00:00,e2a7eb4be0a2cf4139737f6428c147ac35de99899478ce79195dc5e1b0a960f3,https://investemoz.tempo.co.mz/energia | https://investemoz.tempo.co.mz/energia/,8,publicado,RecursoQA8; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,1,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/energia,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
62739,"“Exigimos responsabilização dos agentes do crime”, Daviz Simango",tempo,Nas Redes,2017-06-26T23:13:36+00:00,2017-06-26T23:13:36+00:00,e37ea18176246dab529d2ecec7734f0fb2b0a54cadc507fbd789033d5b8e5b93,https://investemoz.tempo.co.mz/exigimos-responsabilizacao-dos-agentes-do-crime-daviz-simango-2,57,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
62854,Governo e parceiros reforçam mecanismos de controlo de HIV/Sida,tempo,Nas Redes,2017-06-27T11:07:53+00:00,2017-06-27T11:07:53+00:00,0073816b1fa4bd50ec6417b07272da06565230d06a19079ca76cb006c882d2f1,https://investemoz.tempo.co.mz/governo-e-parceiros-reforcam-mecanismos-de-controlo-de-hivsida,58,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
63128,Graça Machel diz que desnutrição é um problema de desenvolvimento,tempo,Nas Redes,2017-06-27T22:01:59+00:00,2017-06-27T22:01:59+00:00,711f3707e3a0d72ec919216fc50823de9060349104ee98fe0a6b20d6684b6031,https://investemoz.tempo.co.mz/graca-machel-diz-que-desnutricao-e-um-problema-de-desenvolvimento,59,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
63181,In dependência,tempo,Nas Redes,2017-06-27T21:38:06+00:00,2017-06-27T21:38:06+00:00,b0d87c0d58d6d1343d102cd3141bda1215c5afd54f3780a1c6c262d786dbfe27,https://investemoz.tempo.co.mz/in-dependencia,60,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
63146,"Kroll não descobriu como foram gastos 2 biliões de dólares; Governo de Nyusi não apoiou a Auditoria; criação da Proindicus, EMATUM e MAM não foi para proteção da Zona Económica Exclusiva",tempo,Nas Redes,2017-06-27T19:30:01+00:00,2017-06-27T19:30:01+00:00,4ccbad5c92ddc3884f58181a4f1ed3c1d3d6a59f005bfd99b197a6fc1fc3c280,https://investemoz.tempo.co.mz/kroll-nao-descobriu-como-foram-gastos-2-bilioes-de-dolares-governo-de-nyusi-nao-apoiou-a-auditoria-criacao-da-proindicus-ematum-e-mam-nao-foi-para-protecao-da-zona-economica-exclusiva-2,61,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
172674,Mineração,tempo,Featured,2026-04-30T13:12:09+00:00,2026-04-30T13:30:13+00:00,8d3acc292ed806bc4cc7bcb6a3a6a5759f30b610766a5ef4b3d6a0b7c56d5861,https://investemoz.tempo.co.mz/mineracao,9,publicado,RecursoQA9; original/hash/ClamD/replay demonstrados. DivulgaçãoQAseparada;consumidorHTTP/desktop observado;não é oportunidade nem geometria.,2,publicados,"{""scope"": ""homologação"", ""href"": ""/documentos/27/view"", ""status"": ""referência na ficha; binário não reensaiado""}",Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.,Preservar a publicação; gerir edições e versões pela biblioteca existente.,outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado,https://teste-investidor.tempo.co.mz/mineracao,Publicado; identidade e conteúdo consultáveis na homologação,Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.,2026-10-22T21:05:03.369388+00:00,
|
||||
62688,O Instituto Nacional de Meteorologia prevê o seguinte estado do tempo para esta ...,tempo,Nas Redes,2017-06-26T18:21:18+00:00,2017-06-26T18:21:18+00:00,396fe2575641622015505eb11d4c7152b6e3e1b741966f14c0cd1673fd455fd3,https://investemoz.tempo.co.mz/o-instituto-nacional-de-meteorologia-preve-o-seguinte-estado-do-tempo-para-esta-29,62,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
173474,Sanções Disciplinares Aplicáveis a Trabalhadores Domésticos segundo o Regulamento de Moçambique,tempo,Trabalho Doméstico,2026-09-15T11:37:04+00:00,2026-09-15T11:37:06+00:00,e09a0fe71397e2b060ff8435111117aa487b476a85ec522fffcdf9e23a6cdac2,https://investemoz.tempo.co.mz/sancoes-disciplinares-aplicaveis-a-trabalhadores-domesticos-segundo-o-regulamento-de-mocambique,69,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,trabalho_domestico
|
||||
63248,Dois alunos da 9ª classe caíram nas mãos da Polícia da República de Moçambique (...,tempo,Nas Redes,2017-06-28T11:30:01+00:00,2017-06-28T11:30:01+00:00,2220ca2c07d10f10b2bd6fd0f103ee733b83570d436c751c449c85b42df46a13,https://investemoz.tempo.co.mz/dois-alunos-da-9a-classe-cairam-nas-maos-da-policia-da-republica-de-mocambique,50,existente por integrar,inventariado; cadastro/versão por reconciliar,,historico_noticioso,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,historico_noticioso
|
||||
173463,Regulamento do Trabalho Doméstico,tempo,Trabalho Doméstico,2026-09-15T10:56:07+00:00,2026-09-15T12:41:44+00:00,eb43b6c1f097631250f0b36d3d01b4d106db2fcae07a56bb154819143fff11e9,https://investemoz.tempo.co.mz/regulamento-do-trabalho-domestico,70,existente por integrar,inventariado; cadastro/versão por reconciliar,,trabalho_domestico,"{""status"": ""imagem já publicada; reutilizar"", ""resource_id"": 24, ""sha256"": ""28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa""}",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.,legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados,,"Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada",Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.,,trabalho_domestico
|
||||
|
||||
|
@@ -0,0 +1,58 @@
|
||||
# Correspondência integral dos 50 artigos do legado
|
||||
|
||||
Conferência HTTP individual em 2026-10-22T21:05:03.369388+00:00. Lista por identidade editorial; imagens, recursos da biblioteca e variantes de URL não são artigos adicionais. Não altera publicações. Endereço em branco significa que não foi demonstrada uma página nova, não uma ligação a criar por suposição.
|
||||
|
||||
Actualização editorial QA: 28 com editor/leitor/prévia; pacote privado preparado, não transportado. Os 20 públicos conservam a conferência anterior, sem novas consultas. Guia e recibo: `legado-editorial-qa-20261022.json`.
|
||||
|
||||
| ID | Título original | Endereço original | Endereço no novo portal | Estado | Pendência |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| 171985 | Agronegocio | https://investemoz.tempo.co.mz/agronegocio | https://teste-investidor.tempo.co.mz/agronegocio | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 62413 | 15/30 costa do sol com dua na zona de mupulene muito lindo | https://investemoz.tempo.co.mz/1530-costa-do-sol-com-dua-na-zona-de-mupulene-muito-lindo | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. Rever/corrigir a nota de demonstração QA antes de publicar. |
|
||||
| 173220 | Alumínio “Verde” | https://investemoz.tempo.co.mz/aluminio-verde | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 73945 | Banco Mundial classifica de proibitivos juros praticados em Moçambique | https://investemoz.tempo.co.mz/banco-mundial-classifica-de-proibitivos-juros-praticados-em-mocambique | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 173213 | Bens de Consumo e Franquias | https://investemoz.tempo.co.mz/bens-de-consumo-franquias | https://teste-investidor.tempo.co.mz/bens-de-consumo-franquias | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 172223 | Cabo Delgado - Pemba, A capital do turismo costeiro do norte de Moçambique | https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique/ \| https://investemoz.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique | https://teste-investidor.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 173482 | Checklist para Contratação e Direitos do Trabalho Doméstico | https://investemoz.tempo.co.mz/checklist-para-contratacao-e-direitos-do-trabalho-domestico | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 76474 | Coloque o seu número para o grupo de whatsapp, venda de acessórios de computador | https://investemoz.tempo.co.mz/coloque-o-seu-numero-para-o-grupo-de-whatsapp-venda-de-acessorios-de-computador | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 173484 | Direitos Especiais da Trabalhadora Doméstica no Regulamento | https://investemoz.tempo.co.mz/direitos-especiais-da-trabalhadora-domestica-no-regulamento | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 171981 | Energia & Mineração | https://investemoz.tempo.co.mz/energia-mineracao | https://teste-investidor.tempo.co.mz/energia-mineracao | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 173222 | Energia e Recursos Naturais | https://investemoz.tempo.co.mz/energia-recursos-naturais | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 62414 | Frelimo e Zanu-PF: Evocados laços de irmandade | https://investemoz.tempo.co.mz/frelimo-e-zanu-pf-evocados-lacos-de-irmandade | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 62418 | Governo dará todo O apoio à PGR no seguimento da auditoria | https://investemoz.tempo.co.mz/governo-dara-todo-o-apoio-a-pgr-no-seguimento-da-auditoria | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 75002 | Governo pretende construir 100 mil habitações em dois anos | https://investemoz.tempo.co.mz/governo-pretende-construir-100-mil-habitacoes-em-dois-anos | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 173478 | Guia Prático para Empregadores sobre Contratação e Direitos no Trabalho Doméstico | https://investemoz.tempo.co.mz/guia-pratico-para-empregadores-sobre-contratacao-e-direitos-no-trabalho-domestico | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 173218 | Imobiliário | https://investemoz.tempo.co.mz/imobiliario | https://teste-investidor.tempo.co.mz/imobiliario | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 171992 | Infraestruturas | https://investemoz.tempo.co.mz/infraestruturas | https://teste-investidor.tempo.co.mz/infraestruturas | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 172240 | Maputo Cidade – Plataforma Estratégica de Turismo, Negócios e Investimento Sustentável | https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel/ \| https://investemoz.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel | https://teste-investidor.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 171994 | Mercado Financeiro | https://investemoz.tempo.co.mz/mercado-financeiro | | Original vazio | Fornecer corpo editorial pela Tempo; imagem e Fintech não substituem o original. Sem novo endereço público. |
|
||||
| 173480 | Modelo de Contrato de Trabalho Doméstico para Pequenas Famílias | https://investemoz.tempo.co.mz/modelo-de-contrato-de-trabalho-domestico-para-pequenas-familias | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 172219 | Niassa - O último grande destino virgem do turismo africano | https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano/ \| https://investemoz.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano | https://teste-investidor.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 173476 | Procedimentos para Rescisão do Contrato de Trabalho Doméstico segundo o Regulamento de Moçambique | https://investemoz.tempo.co.mz/procedimentos-para-rescisao-do-contrato-de-trabalho-domestico-segundo-o-regulamento-de-mocambique | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 172263 | Província da Zambézia: Onde os Corredores de Moçambique Convergem com a Natureza, a Cultura e o Turismo Sustentável | https://investemoz.tempo.co.mz/provincia-da-zambezia-onde-os-corredores-de-mocambique-convergem-com-a-natureza-a-cultura-e-o-turismo-sustentavel | https://teste-investidor.tempo.co.mz/provincia-da-zambezia-onde-os-corredores-de-mocambique-convergem-com-a-natureza-a-cultura-e-o-turismo-sustentavel | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 172233 | Província de Gaza: Oportunidades mais sólidas e estratégicas de investimento no turismo no sul de Moçambique. | https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique/ \| https://investemoz.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique | https://teste-investidor.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 172253 | Província de Inhambane: Um Destino Estratégico para Investimento Turístico | https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico/ \| https://investemoz.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico | https://teste-investidor.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 172258 | Província de Manica: Onde o Turismo de Montanha, Natureza e Cultura Encontra o Próximo Ciclo de Investimento | https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento/ \| https://investemoz.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento | https://teste-investidor.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 172237 | Província de Maputo: Natureza, Cultura e Conectividade como Motores de Crescimento Sustentável | https://investemoz.tempo.co.mz/provincia-de-maputo-turismo/ \| https://investemoz.tempo.co.mz/provincia-de-maputo-turismo | https://teste-investidor.tempo.co.mz/provincia-de-maputo-turismo | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 172230 | Província de Nampula - Perfil de investimento no setor do turismo | https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo/ \| https://investemoz.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo | https://teste-investidor.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 172256 | Província de Sofala: Onde a Natureza, a História e o Futuro do Turismo Convergem | https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem/ \| https://investemoz.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem | https://teste-investidor.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 172261 | Província de Tete: Onde o Rio Zambeze, a Cultura Viva e a Conservação Comunitária Criam o Futuro do Turismo | https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo/ \| https://investemoz.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo | https://teste-investidor.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 73246 | Rofino Licuco preso cinco meses depois de condenado por agressão | https://investemoz.tempo.co.mz/rofino-licuco-preso-cinco-meses-depois-de-condenado-por-agressao | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 77232 | Sete indivíduos mortos a tiro na Matola | https://investemoz.tempo.co.mz/sete-individuos-mortos-a-tiro-na-matola | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 62416 | Sete pessoas morrem e 150 são resgatados em acidente com eléctrico na Índia | https://investemoz.tempo.co.mz/sete-pessoas-morrem-e-150-sao-resgatados-em-acidente-com-electrico-na-india | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 173209 | Fintech | https://investemoz.tempo.co.mz/tech-fintech | https://teste-investidor.tempo.co.mz/tech-fintech | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 171990 | Turismo | https://investemoz.tempo.co.mz/turismo | https://teste-investidor.tempo.co.mz/turismo | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 74364 | | https://investemoz.tempo.co.mz/74364-2 | | Original sem título | Identificação editorial pela Tempo para o post 74364; corpo com 21 caracteres úteis preservado. Sem título ou endereço novo inventado. |
|
||||
| 62929 | Daúde Razaque já não é treinador da Liga Desportiva de Maputo | https://investemoz.tempo.co.mz/daude-razaque-ja-nao-e-treinador-da-liga-desportiva-de-maputo | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 62649 | Daviz Simango não se incomoda com possível sucessão na Beira | https://investemoz.tempo.co.mz/daviz-simango-nao-se-incomoda-com-possivel-sucessao-na-beira | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 173470 | Direitos e Deveres do Trabalhador Doméstico | https://investemoz.tempo.co.mz/direitos-e-deveres-do-trabalhador-domestico | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 172679 | Energia | https://investemoz.tempo.co.mz/energia \| https://investemoz.tempo.co.mz/energia/ | https://teste-investidor.tempo.co.mz/energia | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 62739 | “Exigimos responsabilização dos agentes do crime”, Daviz Simango | https://investemoz.tempo.co.mz/exigimos-responsabilizacao-dos-agentes-do-crime-daviz-simango-2 | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 62854 | Governo e parceiros reforçam mecanismos de controlo de HIV/Sida | https://investemoz.tempo.co.mz/governo-e-parceiros-reforcam-mecanismos-de-controlo-de-hivsida | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 63128 | Graça Machel diz que desnutrição é um problema de desenvolvimento | https://investemoz.tempo.co.mz/graca-machel-diz-que-desnutricao-e-um-problema-de-desenvolvimento | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 63181 | In dependência | https://investemoz.tempo.co.mz/in-dependencia | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 63146 | Kroll não descobriu como foram gastos 2 biliões de dólares; Governo de Nyusi não apoiou a Auditoria; criação da Proindicus, EMATUM e MAM não foi para proteção da Zona Económica Exclusiva | https://investemoz.tempo.co.mz/kroll-nao-descobriu-como-foram-gastos-2-bilioes-de-dolares-governo-de-nyusi-nao-apoiou-a-auditoria-criacao-da-proindicus-ematum-e-mam-nao-foi-para-protecao-da-zona-economica-exclusiva-2 | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 172674 | Mineração | https://investemoz.tempo.co.mz/mineracao | https://teste-investidor.tempo.co.mz/mineracao | Publicado; identidade e conteúdo consultáveis na homologação | Não foi feita aceitação visual ou conferência pela Tempo nesta passagem. |
|
||||
| 62688 | O Instituto Nacional de Meteorologia prevê o seguinte estado do tempo para esta ... | https://investemoz.tempo.co.mz/o-instituto-nacional-de-meteorologia-preve-o-seguinte-estado-do-tempo-para-esta-29 | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 173474 | Sanções Disciplinares Aplicáveis a Trabalhadores Domésticos segundo o Regulamento de Moçambique | https://investemoz.tempo.co.mz/sancoes-disciplinares-aplicaveis-a-trabalhadores-domesticos-segundo-o-regulamento-de-mocambique | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 63248 | Dois alunos da 9ª classe caíram nas mãos da Polícia da República de Moçambique (... | https://investemoz.tempo.co.mz/dois-alunos-da-9a-classe-cairam-nas-maos-da-policia-da-republica-de-mocambique | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
| 173463 | Regulamento do Trabalho Doméstico | https://investemoz.tempo.co.mz/regulamento-do-trabalho-domestico | | Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada | Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso. |
|
||||
@@ -0,0 +1,545 @@
|
||||
{
|
||||
"checked_utc": "2026-10-22T21:05:03.369388+00:00",
|
||||
"distinct_articles": 50,
|
||||
"publications_documented": 20,
|
||||
"public_pages_identity_verified": 20,
|
||||
"pending_content": 28,
|
||||
"private_preparation": {
|
||||
"historico_noticioso": 18,
|
||||
"setorial": 2,
|
||||
"trabalho_domestico": 8
|
||||
},
|
||||
"empty": 1,
|
||||
"untitled": 1,
|
||||
"database_writes_by_correspondence_script": 0,
|
||||
"active_database_writes": 0,
|
||||
"new_publications": 0,
|
||||
"qa_imports": 28,
|
||||
"qa_clean_scans": 28,
|
||||
"remaining_integration_complete": false,
|
||||
"public_pages": {
|
||||
"checked_utc": "2026-10-22T21:05:03.369388+00:00",
|
||||
"method": "Anonymous HTTP GET; Inertia article identity/source/content checked; no visual acceptance or new publication",
|
||||
"pages": {
|
||||
"173209": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/tech-fintech",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 28,
|
||||
"document_sha256": "926f5e4096737c936c9d5002ebdc64be9e63b0c0822023d17ce618111a65c941",
|
||||
"current_title": "Fintech",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "565f86bb5febd5833d5392588551b8e587b2d65898eda06284dcb33228866b63"
|
||||
},
|
||||
"173218": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/imobiliario",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 7,
|
||||
"document_sha256": "43612c358c7573475cea24ef4825c5b7c7925306fa5861051220eed3f4e333b5",
|
||||
"current_title": "Imobiliário",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "684840410238c5a199c581373d5cab233cdf869ccbf7d0da306a3d26181060b4"
|
||||
},
|
||||
"173213": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/bens-de-consumo-franquias",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 29,
|
||||
"document_sha256": "0ba605ae43dec7f685610f828df884bc82cb4da67131fc9783b5cdbe4cc6d9f0",
|
||||
"current_title": "Bens de Consumo e Franquias",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "65145d3b10d494d7e2a8d09736078f9272733c3ae48b5bce501ccffefd63f9bf"
|
||||
},
|
||||
"171985": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/agronegocio",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 13,
|
||||
"document_sha256": "0929184a6d4645b9046512675394ad4f60575273f6dab0955f71cc7474a6a4e2",
|
||||
"current_title": "Agronegocio",
|
||||
"image_url": "/documentos/12/view",
|
||||
"response_sha256": "13c4aeb4307fa5efefa04a9c2cafe0a36b4ab243d93739aa2c1bfe2b0c4ff252"
|
||||
},
|
||||
"171981": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/energia-mineracao",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 18,
|
||||
"document_sha256": "26a70f16cd511c579f08633655ff312db05122f81e44852dd4b163fa6eef85f1",
|
||||
"current_title": "Energia & Mineração",
|
||||
"image_url": "/documentos/14/view",
|
||||
"response_sha256": "10a0e0c00a291929f8923962f9cb31e90bbbb7d2e7eddb856a2ae43e30ca6d1b"
|
||||
},
|
||||
"172679": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/energia",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 1,
|
||||
"document_sha256": "2003755b7dac53b43f4608e707321db6d2f29878926a0e74f4cf27627b70a9cb",
|
||||
"current_title": "Energia",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "41e8ba52174d616cfd81eaa24e4d9135dcfd69a2e6c48e20bfce7b8de5b40166"
|
||||
},
|
||||
"172674": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/mineracao",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 2,
|
||||
"document_sha256": "3ae780f082a8fa7c767635cce98563f5bc027c9656c9f41f8788843a17fa1f54",
|
||||
"current_title": "Mineração",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "b078b87e56f3d68434573aa4d2a2c780845304f44cc65f5f2237732ab32eca7e"
|
||||
},
|
||||
"171990": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/turismo",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 19,
|
||||
"document_sha256": "8cab862be245382dd4c1fd5fcb423c47aed7be3a22e0a7f8d5521510cc36bc72",
|
||||
"current_title": "Turismo",
|
||||
"image_url": "/documentos/15/view",
|
||||
"response_sha256": "471c561e89bf55642ec6aa43b9a27e1dd1f97d0e11e93c8ab95f508b3824da5b"
|
||||
},
|
||||
"172263": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/provincia-da-zambezia-onde-os-corredores-de-mocambique-convergem-com-a-natureza-a-cultura-e-o-turismo-sustentavel",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 3,
|
||||
"document_sha256": "538495cd5b31047ac1c5993fdaf298ec1303330368248b251c87d201d31ff211",
|
||||
"current_title": "Província da Zambézia: Onde os Corredores de Moçambique Convergem com a Natureza, a Cultura e o Turismo Sustentável",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "8629d5647b8ab3f4752ce7452cc131e5b64c0abc06afd25255231a562e0f534e"
|
||||
},
|
||||
"172256": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/provincia-de-sofala-onde-a-natureza-a-historia-e-o-futuro-do-turismo-convergem",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 4,
|
||||
"document_sha256": "dcc29cbd36cf57e0a5c88dd312c84ee03c32a3d18ae9216d3715dc7b5a6960bf",
|
||||
"current_title": "Província de Sofala: Onde a Natureza, a História e o Futuro do Turismo Convergem",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "16f08eaa50b150d73d85ca0b8dccc17ff2b61179813dd6ad0d78ec4309c599d4"
|
||||
},
|
||||
"172261": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/provincia-de-tete-onde-o-rio-zambeze-a-cultura-viva-e-a-conservacao-comunitaria-criam-o-futuro-do-turismo",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 5,
|
||||
"document_sha256": "88177fccd78d8516dba670c6f71272af062cc981d52d97adefe5bf77c613b16a",
|
||||
"current_title": "Província de Tete: Onde o Rio Zambeze, a Cultura Viva e a Conservação Comunitária Criam o Futuro do Turismo",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "7c24f3bfddf8c5bee5ef1312e6d55f46e3ca48c7e48e3f363eac4c95979e6aca"
|
||||
},
|
||||
"172258": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/provincia-de-manica-onde-o-turismo-de-montanha-natureza-e-cultura-encontra-o-proximo-ciclo-de-investimento",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 20,
|
||||
"document_sha256": "48314a576dea2f2dadf47d8f1eadd7ab58030606b02eebe17aa9bd6badffb7e9",
|
||||
"current_title": "Província de Manica: Onde o Turismo de Montanha, Natureza e Cultura Encontra o Próximo Ciclo de Investimento",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "bac797abb94d97ffdc11f8b8cfe9b7782f2c8a9d2311c96e83f71f4ba86bffcb"
|
||||
},
|
||||
"172253": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/provincia-de-inhambane-um-destino-estrategico-para-investimento-turistico",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 6,
|
||||
"document_sha256": "e90e39e49ba65bbbcbbc303c6eca456e445238daf9acc935ab03690819281678",
|
||||
"current_title": "Província de Inhambane: Um Destino Estratégico para Investimento Turístico",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "a6fbbccb7f9499c5aa0034a3d10810a033c6140f1269bd0074e81f1a5bb8da0c"
|
||||
},
|
||||
"172240": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/maputo-cidade-plataforma-estrategica-de-turismo-negocios-e-investimento-sustentavel",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 21,
|
||||
"document_sha256": "8c5e91c937032680883793a12844f1128f7ac99e97c63b68c23d1396e009ba9d",
|
||||
"current_title": "Maputo Cidade – Plataforma Estratégica de Turismo, Negócios e Investimento Sustentável",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "265afdbc2b8997daa69cb519c7409a74d3dd590f7c57b144723786a3c167034d"
|
||||
},
|
||||
"172237": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/provincia-de-maputo-turismo",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 22,
|
||||
"document_sha256": "a62a43e31da32795a7286f4c152a93d5dc829233e72023b00ea61fd8e2fcb58c",
|
||||
"current_title": "Província de Maputo: Natureza, Cultura e Conectividade como Motores de Crescimento Sustentável",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "7827fa353f9fc126938732d42fa57353b48c58f448e25f8466665086779860dc"
|
||||
},
|
||||
"172233": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/provincia-de-gaza-oportunidades-mais-solidas-e-estrategicas-de-investimento-no-turismo-no-sul-de-mocambique",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 23,
|
||||
"document_sha256": "345dfd72d8ebf459eae8eb025d699afddbae751df55ffa8d053568cd08096c97",
|
||||
"current_title": "Província de Gaza: Oportunidades mais sólidas e estratégicas de investimento no turismo no sul de Moçambique.",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "bfb80dff52964448b4bbd03995706020b9bccaa14d8abc1d944123e6b1240a6f"
|
||||
},
|
||||
"172230": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/provincia-de-nampula-perfil-de-investimento-no-setor-do-turismo",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 24,
|
||||
"document_sha256": "4f4570f7bbd5599cb925ed996c1734a23582362fe485736931a53b9266565d4e",
|
||||
"current_title": "Província de Nampula - Perfil de investimento no setor do turismo",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "6d42e0a347c938d201b8c8780fcdf0da83e6dab9fc031ea0ac73b14464f4f696"
|
||||
},
|
||||
"172223": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/cabo-delgado-pemba-a-capital-do-turismo-costeiro-do-norte-de-mocambique",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 25,
|
||||
"document_sha256": "514a9d88c097d234038fa6341f5a5b01e8938cb6a9535a807145c84c37010a4d",
|
||||
"current_title": "Cabo Delgado - Pemba, A capital do turismo costeiro do norte de Moçambique",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "a579796f41716bb43444295dce91b19d0346828e7b3cbfc994efbdc5058ec69b"
|
||||
},
|
||||
"172219": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/niassa-o-ultimo-grande-destino-virgem-do-turismo-africano",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 26,
|
||||
"document_sha256": "0d468ae0de9a76d2b140bc8cb5c555f29626fb6422287c917762bec3e615a70c",
|
||||
"current_title": "Niassa - O último grande destino virgem do turismo africano",
|
||||
"image_url": "/documentos/27/view",
|
||||
"response_sha256": "cffb6ea36fa8273112edaa31432f9c5093ea8e0303fee83c3cdb396e83aac816"
|
||||
},
|
||||
"171992": {
|
||||
"url": "https://teste-investidor.tempo.co.mz/infraestruturas",
|
||||
"status": 200,
|
||||
"identity_verified": true,
|
||||
"resource_id": 17,
|
||||
"document_sha256": "05eb492dd6cfca6a7f305b633658a16723c315168f4b2c75103f22d1fac84032",
|
||||
"current_title": "Infraestruturas",
|
||||
"image_url": "/documentos/16/view",
|
||||
"response_sha256": "0afd5c42dc0a187a22cb3f8ef8af1b10348d31899ce3b1befbc2c38b379d475b"
|
||||
}
|
||||
}
|
||||
},
|
||||
"qa_import": {
|
||||
"port": 55441,
|
||||
"actor": 239,
|
||||
"resources": [
|
||||
{
|
||||
"id": 43,
|
||||
"title": "15/30 costa do sol com dua na zona de mupulene muito lindo",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-62413-20170626",
|
||||
"sha256": "b471c1afd7235f359acdcc16a93088f77e1f988706c8d3c4d4f11c5d560aea47",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "b471c1afd7235f359acdcc16a93088f77e1f988706c8d3c4d4f11c5d560aea47",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 44,
|
||||
"title": "Banco Mundial classifica de proibitivos juros praticados em Moçambique",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-73945-20170731",
|
||||
"sha256": "2aa0ce0eb305bfdf93e390a978d3694d0fbc0129034077b68cebce418f878415",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "2aa0ce0eb305bfdf93e390a978d3694d0fbc0129034077b68cebce418f878415",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 45,
|
||||
"title": "Coloque o seu número para o grupo de whatsapp, venda de acessórios de computador",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-76474-20170808",
|
||||
"sha256": "8f769aec4715449d056c8c25294060b2ff290ec0893c28e9bdcc4e8092fc75f5",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "8f769aec4715449d056c8c25294060b2ff290ec0893c28e9bdcc4e8092fc75f5",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 46,
|
||||
"title": "Governo pretende construir 100 mil habitações em dois anos",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-75002-20170804",
|
||||
"sha256": "7b6fbd998be2b47b96e9a4724560c192c502709fa308d6590c0797ddcdb10544",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "7b6fbd998be2b47b96e9a4724560c192c502709fa308d6590c0797ddcdb10544",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 47,
|
||||
"title": "Rofino Licuco preso cinco meses depois de condenado por agressão",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-73246-20170728",
|
||||
"sha256": "430ee8674c71104fc126bed2911d676f150ac623857a1ada75662987fd9ac390",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "430ee8674c71104fc126bed2911d676f150ac623857a1ada75662987fd9ac390",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 48,
|
||||
"title": "Sete indivíduos mortos a tiro na Matola",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-77232-20170811",
|
||||
"sha256": "27544d3127803b7495bef9fd633147d0225d8c31816c4d59ed05a2edd59de070",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "27544d3127803b7495bef9fd633147d0225d8c31816c4d59ed05a2edd59de070",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 49,
|
||||
"title": "Daúde Razaque já não é treinador da Liga Desportiva de Maputo",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-62929-20170627",
|
||||
"sha256": "488017b062c6760659d9147b7f12aea4afd8d824121adb9462e2c7a2ae58437b",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "488017b062c6760659d9147b7f12aea4afd8d824121adb9462e2c7a2ae58437b",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 50,
|
||||
"title": "Dois alunos da 9ª classe caíram nas mãos da Polícia da República de Moçambique (...",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-63248-20170628",
|
||||
"sha256": "6c30e61197b8568332af6d9b19bbba7ad8665b3873bb031a8c8aedd41536b060",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "6c30e61197b8568332af6d9b19bbba7ad8665b3873bb031a8c8aedd41536b060",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 51,
|
||||
"title": "Alumínio “Verde”",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-173220-20260616",
|
||||
"sha256": "5b76c3d84dcf7f9a489686d90e3857ae6536705c918e841310bb42b354e80976",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "5b76c3d84dcf7f9a489686d90e3857ae6536705c918e841310bb42b354e80976",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 52,
|
||||
"title": "Checklist para Contratação e Direitos do Trabalho Doméstico",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-173482-20260915",
|
||||
"sha256": "6f888e133a779c719073246f89bcae150198e5897c0e7d9b7ba75f54c54a7830",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "6f888e133a779c719073246f89bcae150198e5897c0e7d9b7ba75f54c54a7830",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 53,
|
||||
"title": "Frelimo e Zanu-PF: Evocados laços de irmandade",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-62414-20170626",
|
||||
"sha256": "a040c0b7cb95dd7f053f4d13662dd58b2b06453b6fa61a2d531d0257a8db8c17",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "a040c0b7cb95dd7f053f4d13662dd58b2b06453b6fa61a2d531d0257a8db8c17",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 54,
|
||||
"title": "Governo dará todo O apoio à PGR no seguimento da auditoria",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-62418-20170626",
|
||||
"sha256": "e8ccafa68f0d1aa80f2e41d9e09f7b13e7c653a8472a731798cfd5e04ce2963e",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "e8ccafa68f0d1aa80f2e41d9e09f7b13e7c653a8472a731798cfd5e04ce2963e",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 55,
|
||||
"title": "Sete pessoas morrem e 150 são resgatados em acidente com eléctrico na Índia",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-62416-20170626",
|
||||
"sha256": "ac575ce340571192552c89b63f0bd62a84dbc47828e6930133af5ff70e249319",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "ac575ce340571192552c89b63f0bd62a84dbc47828e6930133af5ff70e249319",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 56,
|
||||
"title": "Daviz Simango não se incomoda com possível sucessão na Beira",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-62649-20170626",
|
||||
"sha256": "38a858713472759a2d20a308bdd9d8afa9cfb76dcaf8d11ca709e89261573e42",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "38a858713472759a2d20a308bdd9d8afa9cfb76dcaf8d11ca709e89261573e42",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 57,
|
||||
"title": "“Exigimos responsabilização dos agentes do crime”, Daviz Simango",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-62739-20170626",
|
||||
"sha256": "5870fc0fad9bb7af537b7b1468151cde138b1b886ce846c7be25647b630245dd",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "5870fc0fad9bb7af537b7b1468151cde138b1b886ce846c7be25647b630245dd",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 58,
|
||||
"title": "Governo e parceiros reforçam mecanismos de controlo de HIV/Sida",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-62854-20170627",
|
||||
"sha256": "44b1ba9d593bb625cf53de8909e78eb0b3129742c3ec9d58ec68c984e8930bce",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "44b1ba9d593bb625cf53de8909e78eb0b3129742c3ec9d58ec68c984e8930bce",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 59,
|
||||
"title": "Graça Machel diz que desnutrição é um problema de desenvolvimento",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-63128-20170627",
|
||||
"sha256": "74be8f29e5053f53ab1a39a2f63208145c30f23357ca6d78b637dd02bf9c1fb4",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "74be8f29e5053f53ab1a39a2f63208145c30f23357ca6d78b637dd02bf9c1fb4",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 60,
|
||||
"title": "In dependência",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-63181-20170627",
|
||||
"sha256": "3d0f9ac14d36156d8178e33f8616d6406f31ea4cd69c8308cbf4227586d7ccb3",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "3d0f9ac14d36156d8178e33f8616d6406f31ea4cd69c8308cbf4227586d7ccb3",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 61,
|
||||
"title": "Kroll não descobriu como foram gastos 2 biliões de dólares; Governo de Nyusi não apoiou a Auditoria; criação da Proindicus, EMATUM e MAM não foi para proteção da Zona Económica Exclusiva",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-63146-20170627",
|
||||
"sha256": "542353466cc6d82b651619c495f138b0576a4be41d21aad194ae2005050cb1bc",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "542353466cc6d82b651619c495f138b0576a4be41d21aad194ae2005050cb1bc",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"title": "O Instituto Nacional de Meteorologia prevê o seguinte estado do tempo para esta ...",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-62688-20170626",
|
||||
"sha256": "a210a789b06d425822f4124f74226a2b27246c086c5ec5f041f1b2237d7c2b65",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "a210a789b06d425822f4124f74226a2b27246c086c5ec5f041f1b2237d7c2b65",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 63,
|
||||
"title": "Energia e Recursos Naturais",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-173222-20260616",
|
||||
"sha256": "e64d7063d1ac4dda028c97e1ba64938d2a9e426a0b630c57ee5df52a41312cc7",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "e64d7063d1ac4dda028c97e1ba64938d2a9e426a0b630c57ee5df52a41312cc7",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 64,
|
||||
"title": "Direitos Especiais da Trabalhadora Doméstica no Regulamento",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-173484-20260915",
|
||||
"sha256": "297df455a9c6fd1f0ab7ca7bbe124ce483c86fe55be26baa695f03089b3ac049",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "297df455a9c6fd1f0ab7ca7bbe124ce483c86fe55be26baa695f03089b3ac049",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 65,
|
||||
"title": "Guia Prático para Empregadores sobre Contratação e Direitos no Trabalho Doméstico",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-173478-20260915",
|
||||
"sha256": "d4e320ed99dd9d53d205e47932f73651976d43081d5d39f2bfc2996901408644",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "d4e320ed99dd9d53d205e47932f73651976d43081d5d39f2bfc2996901408644",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 66,
|
||||
"title": "Modelo de Contrato de Trabalho Doméstico para Pequenas Famílias",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-173480-20260915",
|
||||
"sha256": "c3ee6401eda5b582c61969b5e3b74fdf26ef34ca481b1aca8c4c309d845147ae",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "c3ee6401eda5b582c61969b5e3b74fdf26ef34ca481b1aca8c4c309d845147ae",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 67,
|
||||
"title": "Procedimentos para Rescisão do Contrato de Trabalho Doméstico segundo o Regulamento de Moçambique",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-173476-20260915",
|
||||
"sha256": "72fbf28391db4804e0637b64e7f59e9fa597812a2681b4562dc3d6dfb4093a30",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "72fbf28391db4804e0637b64e7f59e9fa597812a2681b4562dc3d6dfb4093a30",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 68,
|
||||
"title": "Direitos e Deveres do Trabalhador Doméstico",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-173470-20260915",
|
||||
"sha256": "26feeae6b8d04972c7a64ee360583cccdcaeb4b0d8795fd7bc2fd3c38866cc19",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "26feeae6b8d04972c7a64ee360583cccdcaeb4b0d8795fd7bc2fd3c38866cc19",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 69,
|
||||
"title": "Sanções Disciplinares Aplicáveis a Trabalhadores Domésticos segundo o Regulamento de Moçambique",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-173474-20260915",
|
||||
"sha256": "9fa741a17b746bfa2f3ce7b6ce1d2bb34d3a35d3b58330b0554f9e0a6844f049",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "9fa741a17b746bfa2f3ce7b6ce1d2bb34d3a35d3b58330b0554f9e0a6844f049",
|
||||
"data_classification": "internal"
|
||||
},
|
||||
{
|
||||
"id": 70,
|
||||
"title": "Regulamento do Trabalho Doméstico",
|
||||
"published": false,
|
||||
"external_id": "legacy-tempo-post-173463-20260915",
|
||||
"sha256": "4f34b7bac4b3030c0614844d379f6e3cfad1b7f6ea3db575a58c33ae033b0e73",
|
||||
"malware_scan_status": "clean",
|
||||
"malware_scanned_sha256": "4f34b7bac4b3030c0614844d379f6e3cfad1b7f6ea3db575a58c33ae033b0e73",
|
||||
"data_classification": "internal"
|
||||
}
|
||||
],
|
||||
"imported_private": 28,
|
||||
"clean_actual_scans": 28,
|
||||
"idempotent_replay_reused": 28,
|
||||
"existing_resources_preserved": true,
|
||||
"roles_unchanged": true,
|
||||
"new_publications": 0,
|
||||
"active_writes": 0
|
||||
},
|
||||
"original_gaps": [
|
||||
{
|
||||
"legacy_id": 171994,
|
||||
"url": "https://investemoz.tempo.co.mz/mercado-financeiro",
|
||||
"status": 200,
|
||||
"title": "Mercado Financeiro",
|
||||
"body_characters": 0,
|
||||
"body_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"same_body_as_capture": true
|
||||
},
|
||||
{
|
||||
"legacy_id": 74364,
|
||||
"url": "https://investemoz.tempo.co.mz/74364-2",
|
||||
"status": 200,
|
||||
"title": "",
|
||||
"body_characters": 274,
|
||||
"body_sha256": "2a7f0a5b09e0136fec9eae357ac8a88a159ead23b25ed7bb735c75997b10ec5e",
|
||||
"same_body_as_capture": true
|
||||
}
|
||||
],
|
||||
"educational_cards": {
|
||||
"in_main": true,
|
||||
"observed_url": "https://teste-investidor.tempo.co.mz/explorar/turismo",
|
||||
"read_guide_links": 12,
|
||||
"visual_acceptance_by_tempo": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
{
|
||||
"qa_only": true,
|
||||
"originals": 28,
|
||||
"rows": [
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62413-20170626",
|
||||
"qa_resource_id": 43,
|
||||
"version": 2,
|
||||
"original_sha256": "b471c1afd7235f359acdcc16a93088f77e1f988706c8d3c4d4f11c5d560aea47",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 1,
|
||||
"history_revisions": 2
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-73945-20170731",
|
||||
"qa_resource_id": 44,
|
||||
"version": 1,
|
||||
"original_sha256": "2aa0ce0eb305bfdf93e390a978d3694d0fbc0129034077b68cebce418f878415",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 1,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-76474-20170808",
|
||||
"qa_resource_id": 45,
|
||||
"version": 1,
|
||||
"original_sha256": "8f769aec4715449d056c8c25294060b2ff290ec0893c28e9bdcc4e8092fc75f5",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 2,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62414-20170626",
|
||||
"qa_resource_id": 53,
|
||||
"version": 1,
|
||||
"original_sha256": "a040c0b7cb95dd7f053f4d13662dd58b2b06453b6fa61a2d531d0257a8db8c17",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 1,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62418-20170626",
|
||||
"qa_resource_id": 54,
|
||||
"version": 1,
|
||||
"original_sha256": "e8ccafa68f0d1aa80f2e41d9e09f7b13e7c653a8472a731798cfd5e04ce2963e",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 1,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-75002-20170804",
|
||||
"qa_resource_id": 46,
|
||||
"version": 1,
|
||||
"original_sha256": "7b6fbd998be2b47b96e9a4724560c192c502709fa308d6590c0797ddcdb10544",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 1,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-73246-20170728",
|
||||
"qa_resource_id": 47,
|
||||
"version": 1,
|
||||
"original_sha256": "430ee8674c71104fc126bed2911d676f150ac623857a1ada75662987fd9ac390",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 1,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-77232-20170811",
|
||||
"qa_resource_id": 48,
|
||||
"version": 1,
|
||||
"original_sha256": "27544d3127803b7495bef9fd633147d0225d8c31816c4d59ed05a2edd59de070",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 1,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62416-20170626",
|
||||
"qa_resource_id": 55,
|
||||
"version": 1,
|
||||
"original_sha256": "ac575ce340571192552c89b63f0bd62a84dbc47828e6930133af5ff70e249319",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 0,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62929-20170627",
|
||||
"qa_resource_id": 49,
|
||||
"version": 1,
|
||||
"original_sha256": "488017b062c6760659d9147b7f12aea4afd8d824121adb9462e2c7a2ae58437b",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 1,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62649-20170626",
|
||||
"qa_resource_id": 56,
|
||||
"version": 1,
|
||||
"original_sha256": "38a858713472759a2d20a308bdd9d8afa9cfb76dcaf8d11ca709e89261573e42",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 1,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62739-20170626",
|
||||
"qa_resource_id": 57,
|
||||
"version": 1,
|
||||
"original_sha256": "5870fc0fad9bb7af537b7b1468151cde138b1b886ce846c7be25647b630245dd",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 1,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62854-20170627",
|
||||
"qa_resource_id": 58,
|
||||
"version": 1,
|
||||
"original_sha256": "44b1ba9d593bb625cf53de8909e78eb0b3129742c3ec9d58ec68c984e8930bce",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 1,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-63128-20170627",
|
||||
"qa_resource_id": 59,
|
||||
"version": 1,
|
||||
"original_sha256": "74be8f29e5053f53ab1a39a2f63208145c30f23357ca6d78b637dd02bf9c1fb4",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 1,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-63181-20170627",
|
||||
"qa_resource_id": 60,
|
||||
"version": 1,
|
||||
"original_sha256": "3d0f9ac14d36156d8178e33f8616d6406f31ea4cd69c8308cbf4227586d7ccb3",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 1,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-63146-20170627",
|
||||
"qa_resource_id": 61,
|
||||
"version": 1,
|
||||
"original_sha256": "542353466cc6d82b651619c495f138b0576a4be41d21aad194ae2005050cb1bc",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 1,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-62688-20170626",
|
||||
"qa_resource_id": 62,
|
||||
"version": 1,
|
||||
"original_sha256": "a210a789b06d425822f4124f74226a2b27246c086c5ec5f041f1b2237d7c2b65",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 0,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-63248-20170628",
|
||||
"qa_resource_id": 50,
|
||||
"version": 1,
|
||||
"original_sha256": "6c30e61197b8568332af6d9b19bbba7ad8665b3873bb031a8c8aedd41536b060",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 0,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173220-20260616",
|
||||
"qa_resource_id": 51,
|
||||
"version": 1,
|
||||
"original_sha256": "5b76c3d84dcf7f9a489686d90e3857ae6536705c918e841310bb42b354e80976",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 0,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173222-20260616",
|
||||
"qa_resource_id": 63,
|
||||
"version": 1,
|
||||
"original_sha256": "e64d7063d1ac4dda028c97e1ba64938d2a9e426a0b630c57ee5df52a41312cc7",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 0,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173482-20260915",
|
||||
"qa_resource_id": 52,
|
||||
"version": 1,
|
||||
"original_sha256": "6f888e133a779c719073246f89bcae150198e5897c0e7d9b7ba75f54c54a7830",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 0,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173484-20260915",
|
||||
"qa_resource_id": 64,
|
||||
"version": 1,
|
||||
"original_sha256": "297df455a9c6fd1f0ab7ca7bbe124ce483c86fe55be26baa695f03089b3ac049",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 0,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173478-20260915",
|
||||
"qa_resource_id": 65,
|
||||
"version": 1,
|
||||
"original_sha256": "d4e320ed99dd9d53d205e47932f73651976d43081d5d39f2bfc2996901408644",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 0,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173480-20260915",
|
||||
"qa_resource_id": 66,
|
||||
"version": 1,
|
||||
"original_sha256": "c3ee6401eda5b582c61969b5e3b74fdf26ef34ca481b1aca8c4c309d845147ae",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 0,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173476-20260915",
|
||||
"qa_resource_id": 67,
|
||||
"version": 1,
|
||||
"original_sha256": "72fbf28391db4804e0637b64e7f59e9fa597812a2681b4562dc3d6dfb4093a30",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 0,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173470-20260915",
|
||||
"qa_resource_id": 68,
|
||||
"version": 1,
|
||||
"original_sha256": "26feeae6b8d04972c7a64ee360583cccdcaeb4b0d8795fd7bc2fd3c38866cc19",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 0,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173474-20260915",
|
||||
"qa_resource_id": 69,
|
||||
"version": 1,
|
||||
"original_sha256": "9fa741a17b746bfa2f3ce7b6ce1d2bb34d3a35d3b58330b0554f9e0a6844f049",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 0,
|
||||
"history_revisions": 1
|
||||
},
|
||||
{
|
||||
"external_id": "legacy-tempo-post-173463-20260915",
|
||||
"qa_resource_id": 70,
|
||||
"version": 1,
|
||||
"original_sha256": "4f34b7bac4b3030c0614844d379f6e3cfad1b7f6ea3db575a58c33ae033b0e73",
|
||||
"private": true,
|
||||
"image_available": false,
|
||||
"image_reference": "https://investemoz.tempo.co.mz/images/tempo-background.png",
|
||||
"body_image_references": 0,
|
||||
"history_revisions": 1
|
||||
}
|
||||
],
|
||||
"archive_entries": 57,
|
||||
"archive_sha256": "177c50d1cc3b1610e62123a2f8b532711a15f875aac2ee0e372636c2f747e8a2",
|
||||
"archive_bytes": 85614,
|
||||
"verified_original_hashes": 28,
|
||||
"included_image_originals": 0,
|
||||
"destination_imported": false,
|
||||
"destination_published": false,
|
||||
"database_writes": 0,
|
||||
"tests": {
|
||||
"passed": 6,
|
||||
"assertions": 1000,
|
||||
"database": "simulator_fiscal_delta_20261021_test",
|
||||
"port": 55441,
|
||||
"publication_transactions_rolled_back": true,
|
||||
"all_28_sanitized_original_text_preserved": true
|
||||
},
|
||||
"typescript": "passed",
|
||||
"qa_build": "passed",
|
||||
"public20_rechecked": false,
|
||||
"reimported": false,
|
||||
"main_integrated": false,
|
||||
"user_review_claimed": false,
|
||||
"ui_saved_resource": 43,
|
||||
"ui_saved_version": 2,
|
||||
"original_image_sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -9,6 +9,220 @@
|
||||
|
||||
@import './tempo-header.css';
|
||||
|
||||
/* Institutional shell adapted from Tempo; page content keeps its own theme. */
|
||||
.tempo-header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 40;
|
||||
background: #090909;
|
||||
color: #fff;
|
||||
}
|
||||
.tempo-header-inner {
|
||||
grid-template-rows: 84px;
|
||||
}
|
||||
.tempo-logo-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
width: 140px;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
.tempo-nav-trigger {
|
||||
display: flex;
|
||||
min-height: 48px;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.6rem 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: 600;
|
||||
color: #eee;
|
||||
transition:
|
||||
color 0.2s,
|
||||
background 0.2s;
|
||||
}
|
||||
.tempo-nav-trigger:hover,
|
||||
.tempo-nav-trigger[data-state='open'] {
|
||||
background: #ffffff0d;
|
||||
color: #fff;
|
||||
}
|
||||
.tempo-nav-trigger[data-current-group] {
|
||||
color: #fff;
|
||||
box-shadow: inset 0 -2px #ed1c24;
|
||||
}
|
||||
.tempo-nav-trigger[data-state='open'] > svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
@media (max-width: 640px) {
|
||||
.tempo-nav-trigger {
|
||||
gap: 0.25rem;
|
||||
padding-inline: 0.25rem;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.tempo-nav-trigger > svg {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
}
|
||||
}
|
||||
.tempo-utility-button {
|
||||
display: flex;
|
||||
min-height: 40px;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.4rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.75rem;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.tempo-utility-button:hover {
|
||||
background: #ffffff10;
|
||||
}
|
||||
.tempo-header-social {
|
||||
display: flex;
|
||||
width: 30px;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--social-color);
|
||||
transition:
|
||||
transform 0.2s,
|
||||
opacity 0.2s;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
.tempo-header-social:hover {
|
||||
transform: translateY(-2px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
@media (max-width: 360px) {
|
||||
.tempo-header-social {
|
||||
width: 26px;
|
||||
}
|
||||
}
|
||||
.tempo-login-button {
|
||||
display: inline-flex;
|
||||
min-height: 40px;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border: 1px solid #ffffff50;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.5rem 0.85rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
transition:
|
||||
background 0.2s,
|
||||
border-color 0.2s;
|
||||
}
|
||||
.tempo-login-button:hover {
|
||||
background: #ed1c24;
|
||||
border-color: #ed1c24;
|
||||
}
|
||||
.tempo-header-actions {
|
||||
order: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
.tempo-interest-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 40px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #ed1c24;
|
||||
border-radius: 0.4rem;
|
||||
background: #ed1c24;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.tempo-interest-button:hover {
|
||||
background: #c9151c;
|
||||
}
|
||||
.tempo-panel-summary {
|
||||
order: 4;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
padding-block: 0.5rem;
|
||||
border-top: 1px solid #ffffff1a;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.tempo-panel-summary a {
|
||||
color: #fff;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.tempo-panel-summary a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.tempo-panel-summary span {
|
||||
color: #cbd5e1;
|
||||
font-size: 0.6875rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.tempo-header-actions {
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
padding-block: 0.65rem;
|
||||
}
|
||||
}
|
||||
@media (min-width: 640px) {
|
||||
.tempo-header-inner {
|
||||
grid-template-rows: 90px;
|
||||
}
|
||||
.tempo-logo-link {
|
||||
width: 160px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1280px) {
|
||||
.tempo-header-inner {
|
||||
grid-template-rows: 96px;
|
||||
}
|
||||
.tempo-logo-link {
|
||||
width: 152px;
|
||||
}
|
||||
.tempo-header-actions {
|
||||
order: 6;
|
||||
margin-left: 0;
|
||||
}
|
||||
.tempo-header-inner .tempo-nav-trigger {
|
||||
padding-inline: 0.5rem;
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
.tempo-panel-summary {
|
||||
order: 0;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 0.1rem;
|
||||
width: auto;
|
||||
min-width: 102px;
|
||||
border-top: 0;
|
||||
border-left: 1px solid #ffffff1a;
|
||||
padding-left: 0.75rem;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1440px) {
|
||||
.tempo-header-inner {
|
||||
grid-template-rows: 102px;
|
||||
}
|
||||
.tempo-logo-link {
|
||||
width: 168px;
|
||||
}
|
||||
.tempo-header-inner .tempo-nav-trigger {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
.tempo-header :is(a, button):focus-visible {
|
||||
outline: 2px solid #fff;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
.tempo-dropdown {
|
||||
z-index: 60;
|
||||
border: 1px solid #e2e8f0;
|
||||
|
||||
@@ -26,6 +26,7 @@ export function AppSidebar() {
|
||||
educationEnabled,
|
||||
aiEnabled,
|
||||
opportunityManagement,
|
||||
contentManagementHref,
|
||||
profileAccess,
|
||||
} = usePage<{
|
||||
auth: Auth;
|
||||
@@ -34,6 +35,7 @@ export function AppSidebar() {
|
||||
aiEnabled?: boolean;
|
||||
profileAccess?: ProfileAccess | null;
|
||||
opportunityManagement?: OpportunityManagementAccess | null;
|
||||
contentManagementHref?: string | null;
|
||||
}>().props;
|
||||
const groups = portalNavigation({
|
||||
authenticated: !!auth.user,
|
||||
@@ -48,6 +50,9 @@ export function AppSidebar() {
|
||||
href: group.children?.find((item) => item.href)?.href ?? '/',
|
||||
icon: LayoutGrid,
|
||||
}));
|
||||
if (contentManagementHref) {
|
||||
items.unshift({ title: 'Gestão de conteúdos e oportunidades', href: contentManagementHref, icon: LayoutGrid });
|
||||
}
|
||||
if (auth.canManagePortal && opportunityManagement) {
|
||||
items.push({
|
||||
title: opportunityManagement.manage.label,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
export default function CatalogueNavigation({
|
||||
dimensionUrl,
|
||||
}: {
|
||||
@@ -21,7 +22,7 @@ export default function CatalogueNavigation({
|
||||
)
|
||||
return dimensionUrl(u.searchParams.get('dimension')!);
|
||||
if (context) u.searchParams.set('context', context);
|
||||
return u.pathname + u.search;
|
||||
return searchTerritoryHref(u.pathname + u.search, url);
|
||||
};
|
||||
const sections = {
|
||||
'Enquadramento e filtros': [
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
|
||||
export default function ContentManagementEntry() {
|
||||
const { contentManagementHref } = usePage<{ contentManagementHref?: string | null }>().props;
|
||||
return contentManagementHref ? <Link className="inline-block rounded-lg border px-4 py-3 font-semibold underline" href={contentManagementHref}>Gestão de conteúdos e oportunidades</Link> : null;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
export type EditorialArticle = {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
author: string | null;
|
||||
body: string;
|
||||
html?: string | null;
|
||||
image_url: string | null;
|
||||
original_url: string;
|
||||
source_url?: string | null;
|
||||
published_at?: string | null;
|
||||
modified_at?: string | null;
|
||||
legacy_id?: number | null;
|
||||
image_source_url?: string | null;
|
||||
image_notice?: string | null;
|
||||
image_references?: string[];
|
||||
metadata_version?: number;
|
||||
private_preview?: boolean;
|
||||
};
|
||||
export default function EditorialReading({
|
||||
article,
|
||||
}: {
|
||||
article: EditorialArticle;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{article.private_preview && (
|
||||
<p
|
||||
role="status"
|
||||
className="rounded border border-amber-500 bg-amber-50 p-3 text-slate-900"
|
||||
>
|
||||
Prévia privada — não publica o artigo nem altera os direitos
|
||||
do original.
|
||||
</p>
|
||||
)}
|
||||
<h1 className="text-3xl font-bold">{article.title}</h1>
|
||||
{article.description && <p>{article.description}</p>}
|
||||
{article.author && <p>Autor / entidade: {article.author}</p>}
|
||||
{article.legacy_id && (
|
||||
<p>
|
||||
Arquivo do portal antigo · artigo {article.legacy_id}.
|
||||
Conteúdo editorial preservado; não corresponde por si a
|
||||
oportunidade concreta ou regra executável.
|
||||
</p>
|
||||
)}
|
||||
{(article.published_at || article.modified_at) && (
|
||||
<p>
|
||||
Origem: publicado em{' '}
|
||||
{article.published_at ?? 'data não indicada'} · modificado
|
||||
em {article.modified_at ?? 'data não indicada'}. A edição no
|
||||
portal conserva estas datas de origem.
|
||||
</p>
|
||||
)}
|
||||
{article.image_url && (
|
||||
<img
|
||||
className="max-h-96 w-full object-contain"
|
||||
src={article.image_url}
|
||||
alt={`Imagem do artigo ${article.title}`}
|
||||
/>
|
||||
)}
|
||||
{article.image_notice && (
|
||||
<p role="note" className="rounded border p-3">
|
||||
{article.image_notice}
|
||||
</p>
|
||||
)}
|
||||
{article.html ? (
|
||||
<article
|
||||
className="prose dark:prose-invert max-w-none overflow-x-auto"
|
||||
dangerouslySetInnerHTML={{ __html: article.html }}
|
||||
/>
|
||||
) : (
|
||||
<article className="space-y-5">
|
||||
{article.body.split(/\n\s*\n/).map((p, i) => (
|
||||
<p className="whitespace-pre-wrap" key={i}>
|
||||
{p}
|
||||
</p>
|
||||
))}
|
||||
</article>
|
||||
)}
|
||||
{!!article.image_references?.length && (
|
||||
<section aria-label="Imagens referenciadas no corpo original">
|
||||
<h2>Imagens referenciadas no corpo original</h2>
|
||||
<p>
|
||||
Referências preservadas; não confirmam um ficheiro
|
||||
disponível na biblioteca.
|
||||
</p>
|
||||
<ul>
|
||||
{article.image_references.map((url, i) => (
|
||||
<li key={url}>
|
||||
<a
|
||||
className="underline"
|
||||
href={url}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Referência da imagem {i + 1}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
)}
|
||||
<nav aria-label="Origem do artigo" className="flex flex-wrap gap-4">
|
||||
{article.source_url && (
|
||||
<a
|
||||
className="underline"
|
||||
href={article.source_url}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Consultar página de origem
|
||||
</a>
|
||||
)}
|
||||
{article.image_source_url && (
|
||||
<a
|
||||
className="underline"
|
||||
href={article.image_source_url}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Referência da imagem de origem
|
||||
</a>
|
||||
)}
|
||||
<a href={article.original_url} className="underline">
|
||||
Consultar documento original
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,8 @@ export type InvestmentMapFeature = {
|
||||
};
|
||||
};
|
||||
export type InvestmentMapArea = {
|
||||
selected_territory?: boolean;
|
||||
location_type?: string;
|
||||
purpose?: string;
|
||||
geometry: GeoJsonObject;
|
||||
title: string;
|
||||
@@ -74,6 +76,13 @@ export default function InteractiveInvestmentMap({
|
||||
.addTo(instance);
|
||||
L.control.scale({ imperial: false }).addTo(instance);
|
||||
const bounds: [number, number][] = [];
|
||||
const hasSelectedBoundary = areas.some((area) => area.selected_territory);
|
||||
const administrativeLevel: Record<string, number> = {
|
||||
province: 1, district: 2, admin_post: 3, locality: 4,
|
||||
};
|
||||
const closestContextLevel = contextOnly
|
||||
? Math.max(0, ...areas.map((area) => administrativeLevel[area.location_type ?? ''] ?? 0))
|
||||
: 0;
|
||||
for (const area of areas) {
|
||||
const label = document.createElement('p');
|
||||
label.textContent = `${area.title} · ${area.purpose === 'administrative_boundary' ? 'Limite administrativo da edição indicada; não representa um local de investimento' : 'Polígono cadastrado, limites oficiais por verificar'} · Fonte: ${area.source_reference ?? 'Não indicada'} · Data: ${area.source_date ?? 'Não documentada'}`;
|
||||
@@ -92,7 +101,10 @@ export default function InteractiveInvestmentMap({
|
||||
.bindPopup(label)
|
||||
.addTo(instance);
|
||||
const extent = layer.getBounds();
|
||||
if (extent.isValid())
|
||||
const focusBoundary = hasSelectedBoundary
|
||||
? area.selected_territory
|
||||
: !closestContextLevel || administrativeLevel[area.location_type ?? ''] === closestContextLevel;
|
||||
if (extent.isValid() && focusBoundary)
|
||||
bounds.push(
|
||||
[extent.getSouth(), extent.getWest()],
|
||||
[extent.getNorth(), extent.getEast()],
|
||||
|
||||
@@ -1,24 +1,232 @@
|
||||
import {
|
||||
PreparationSummary,
|
||||
type Preparation,
|
||||
} from '@/components/private-investment-preparation';
|
||||
import { Link } from '@inertiajs/react';
|
||||
|
||||
type Snapshot = {context_revision:number;path?:{definition_version:number;steps:Array<{code:string;title:string;applicability:string;reason:string;state:string;context_review_required?:boolean}>}; zone?:{title:string;href:string;version:number|string;fields?:Record<string,string>;analysis?:{conditions?:Array<{id:string;label:string;knowledge:string;source_reference?:string;valid_from?:string;valid_until?:string}>;source?:{title:string;url:string;diploma?:string;article?:string;valid_from?:string;valid_until?:string}|null}}|null;fields?:Record<string,string>};
|
||||
export function InvestmentContextAssessment({raw,contextId}:{raw:unknown;contextId?:string|null}) {
|
||||
let snapshot:Snapshot|null=null;
|
||||
try {const value=JSON.parse(String(raw ?? ''));if(value.version===1) snapshot=value;} catch { /* Missing context remains explicit. */ }
|
||||
if(!snapshot) return null;
|
||||
const fields=snapshot.fields ?? {};const zone=snapshot.zone;
|
||||
const regime=zone?.fields?.investment_regime ?? fields.investment_regime;
|
||||
const names:Record<string,string>={zee:'ZEE',zfi:'ZFI',industrial_park:'Parque industrial'};
|
||||
return <section className="space-y-2 rounded border border-amber-400 p-3" aria-label="Aplicabilidade ao investimento">
|
||||
<h3 className="font-semibold">Aplicabilidade ao investimento · contexto v{snapshot.context_revision}</h3>
|
||||
<p>Origem do investidor: {{national:'nacional',foreign:'estrangeiro',mixed:'misto'}[fields.investor_origin] ?? 'informação em falta'}. Residência fiscal declarada: {fields.tax_residency_country || 'informação em falta'}. Estas condições são distintas; uma não determina a outra.</p>
|
||||
{zone ? <><p><strong>Potencialmente aplicável — análise específica de {names[regime ?? ''] ?? 'zona seleccionada'}.</strong> <Link className="underline" href={zone.href}>{zone.title} · versão {zone.version} · consultar ficha e fundamentos</Link>.</p>
|
||||
<p>A selecção identifica a referência consultada; não confirma o reconhecimento da entidade, actividade autorizada, licença ou acesso a benefícios. Consulte as condições documentadas na ficha e reúna a autorização ou evidência correspondente no dossier.</p>
|
||||
{zone.analysis?.source && <p>Fundamento cadastrado: <a className="underline" href={zone.analysis.source.url}>{zone.analysis.source.diploma || zone.analysis.source.title} {zone.analysis.source.article}</a>. Período cadastrado: {zone.analysis.source.valid_from || 'início por indicar'} a {zone.analysis.source.valid_until || 'sem termo indicado'}. A cobertura do investimento exige correspondência com estas condições.</p>}
|
||||
{zone.analysis?.conditions?.length ? <ul className="list-disc pl-5">{zone.analysis.conditions.map(condition=><li key={condition.id}><strong>{condition.label}</strong>: {condition.knowledge==='documented'?'condição documentada; cumprimento pelo investimento por confirmar':condition.knowledge==='disputed'?'condição com informação divergente':'informação em falta'}. {condition.source_reference && <>Fundamento: {condition.source_reference}.</>} {condition.valid_from && <>Desde {condition.valid_from}.</>} {condition.valid_until && <>Até {condition.valid_until}.</>}</li>)}</ul> : <p>Condições específicas: informação estruturada em falta nesta referência. A ficha e os documentos continuam consultáveis; não se presume cumprimento.</p>}
|
||||
<p role="status">Benefício fiscal deste enquadramento: indeterminado. Não existe neste consumidor uma regra executável ligada ao reconhecimento e condições da zona. No simulador, o IVA disponível corresponde apenas ao cenário geral sem benefício, sobre vendas declaradas compatíveis; não simula o regime especial. O cenário com benefício e o apuramento final estão indisponíveis.</p></>
|
||||
: <p>Enquadramento geral de preparação: sem ZEE, ZFI ou parque seleccionado. Isso não confirma a elegibilidade para uma operação fiscal. O cálculo mantém o âmbito e as exclusões da versão publicada.</p>}
|
||||
<p>Confirmado neste quadro significa apenas dado recuperado do dossier; declarações e referências não são decisões jurídicas. Requisitos específicos não documentados permanecem indeterminados.</p>
|
||||
{snapshot.path && <details><summary>Passos adaptados pelo percurso existente · versão {snapshot.path.definition_version}</summary><ul className="list-disc pl-5">{snapshot.path.steps.map(step=><li key={step.code}>{step.code} — {step.title}: {step.context_review_required?'informação alterada; rever fundamento':step.applicability==='not_needed_for_preparation'?'não aplicável à preparação declarada':step.applicability==='undetermined'?'informação em falta':['completed','approved'].includes(step.state)?'preparação confirmada no percurso; não é decisão fiscal':'potencialmente aplicável'}. {step.reason}</li>)}</ul></details>}
|
||||
{contextId && <Link className="underline" href={`/investimentos/${contextId}/contexto/editar`}>Alterar contexto e comparar impacto antes de guardar</Link>}
|
||||
</section>;
|
||||
type Snapshot = {
|
||||
preparation?: Preparation | null;
|
||||
context_revision: number;
|
||||
path?: {
|
||||
definition_version: number;
|
||||
steps: Array<{
|
||||
code: string;
|
||||
title: string;
|
||||
applicability: string;
|
||||
reason: string;
|
||||
state: string;
|
||||
context_review_required?: boolean;
|
||||
}>;
|
||||
};
|
||||
zone?: {
|
||||
title: string;
|
||||
href: string;
|
||||
version: number | string;
|
||||
fields?: Record<string, string>;
|
||||
analysis?: {
|
||||
conditions?: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
knowledge: string;
|
||||
source_reference?: string;
|
||||
valid_from?: string;
|
||||
valid_until?: string;
|
||||
}>;
|
||||
source?: {
|
||||
title: string;
|
||||
url: string;
|
||||
diploma?: string;
|
||||
article?: string;
|
||||
valid_from?: string;
|
||||
valid_until?: string;
|
||||
} | null;
|
||||
};
|
||||
} | null;
|
||||
fields?: Record<string, string>;
|
||||
};
|
||||
export function InvestmentContextAssessment({
|
||||
raw,
|
||||
contextId,
|
||||
}: {
|
||||
raw: unknown;
|
||||
contextId?: string | null;
|
||||
}) {
|
||||
let snapshot: Snapshot | null = null;
|
||||
try {
|
||||
const value = JSON.parse(String(raw ?? ''));
|
||||
if (value.version === 1) snapshot = value;
|
||||
} catch {
|
||||
/* Missing context remains explicit. */
|
||||
}
|
||||
if (!snapshot) return null;
|
||||
const fields = snapshot.fields ?? {};
|
||||
const zone = snapshot.zone;
|
||||
const regime = zone?.fields?.investment_regime ?? fields.investment_regime;
|
||||
const names: Record<string, string> = {
|
||||
zee: 'ZEE',
|
||||
zfi: 'ZFI',
|
||||
industrial_park: 'Parque industrial',
|
||||
};
|
||||
return (
|
||||
<section
|
||||
className="space-y-2 rounded border border-amber-400 p-3"
|
||||
aria-label="Aplicabilidade ao investimento"
|
||||
>
|
||||
<h3 className="font-semibold">
|
||||
Aplicabilidade ao investimento · contexto v
|
||||
{snapshot.context_revision}
|
||||
</h3>
|
||||
<p>
|
||||
Origem do investidor:{' '}
|
||||
{{
|
||||
national: 'nacional',
|
||||
foreign: 'estrangeiro',
|
||||
mixed: 'misto',
|
||||
}[fields.investor_origin] ?? 'informação em falta'}
|
||||
. Residência fiscal declarada:{' '}
|
||||
{fields.tax_residency_country || 'informação em falta'}. Estas
|
||||
condições são distintas; uma não determina a outra.
|
||||
</p>
|
||||
{zone ? (
|
||||
<>
|
||||
<p>
|
||||
<strong>
|
||||
Potencialmente aplicável — análise específica de{' '}
|
||||
{names[regime ?? ''] ?? 'zona seleccionada'}.
|
||||
</strong>{' '}
|
||||
<Link className="underline" href={zone.href}>
|
||||
{zone.title} · versão {zone.version} · consultar
|
||||
ficha e fundamentos
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
A selecção identifica a referência consultada; não
|
||||
confirma o reconhecimento da entidade, actividade
|
||||
autorizada, licença ou acesso a benefícios. Consulte as
|
||||
condições documentadas na ficha e reúna a autorização ou
|
||||
evidência correspondente no dossier.
|
||||
</p>
|
||||
{zone.analysis?.source && (
|
||||
<p>
|
||||
Fundamento cadastrado:{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={zone.analysis.source.url}
|
||||
>
|
||||
{zone.analysis.source.diploma ||
|
||||
zone.analysis.source.title}{' '}
|
||||
{zone.analysis.source.article}
|
||||
</a>
|
||||
. Período cadastrado:{' '}
|
||||
{zone.analysis.source.valid_from ||
|
||||
'início por indicar'}{' '}
|
||||
a{' '}
|
||||
{zone.analysis.source.valid_until ||
|
||||
'sem termo indicado'}
|
||||
. A cobertura do investimento exige correspondência
|
||||
com estas condições.
|
||||
</p>
|
||||
)}
|
||||
{zone.analysis?.conditions?.length ? (
|
||||
<ul className="list-disc pl-5">
|
||||
{zone.analysis.conditions.map((condition) => (
|
||||
<li key={condition.id}>
|
||||
<strong>{condition.label}</strong>:{' '}
|
||||
{condition.knowledge === 'documented'
|
||||
? 'condição documentada; cumprimento pelo investimento por confirmar'
|
||||
: condition.knowledge === 'disputed'
|
||||
? 'condição com informação divergente'
|
||||
: 'informação em falta'}
|
||||
.{' '}
|
||||
{condition.source_reference && (
|
||||
<>
|
||||
Fundamento:{' '}
|
||||
{condition.source_reference}.
|
||||
</>
|
||||
)}{' '}
|
||||
{condition.valid_from && (
|
||||
<>Desde {condition.valid_from}.</>
|
||||
)}{' '}
|
||||
{condition.valid_until && (
|
||||
<>Até {condition.valid_until}.</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>
|
||||
Condições específicas: informação estruturada em
|
||||
falta nesta referência. A ficha e os documentos
|
||||
continuam consultáveis; não se presume cumprimento.
|
||||
</p>
|
||||
)}
|
||||
<p role="status">
|
||||
Benefício fiscal deste enquadramento: indeterminado. Não
|
||||
existe neste consumidor uma regra executável ligada ao
|
||||
reconhecimento e condições da zona. No simulador, o IVA
|
||||
disponível corresponde apenas ao cenário geral sem
|
||||
benefício, sobre vendas declaradas compatíveis; não
|
||||
simula o regime especial. O cenário com benefício e o
|
||||
apuramento final estão indisponíveis.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
Enquadramento geral de preparação: sem ZEE, ZFI ou parque
|
||||
seleccionado. Isso não confirma a elegibilidade para uma
|
||||
operação fiscal. O cálculo mantém o âmbito e as exclusões da
|
||||
versão publicada.
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
Confirmado neste quadro significa apenas dado recuperado do
|
||||
dossier; declarações e referências não são decisões jurídicas.
|
||||
Requisitos específicos não documentados permanecem
|
||||
indeterminados.
|
||||
</p>
|
||||
{snapshot.path && (
|
||||
<details>
|
||||
<summary>
|
||||
Passos adaptados pelo percurso existente · versão{' '}
|
||||
{snapshot.path.definition_version}
|
||||
</summary>
|
||||
<ul className="list-disc pl-5">
|
||||
{snapshot.path.steps.map((step) => (
|
||||
<li key={step.code}>
|
||||
{step.code} — {step.title}:{' '}
|
||||
{step.context_review_required
|
||||
? 'informação alterada; rever fundamento'
|
||||
: step.applicability ===
|
||||
'not_needed_for_preparation'
|
||||
? 'não aplicável à preparação declarada'
|
||||
: step.applicability === 'undetermined'
|
||||
? 'informação em falta'
|
||||
: ['completed', 'approved'].includes(
|
||||
step.state,
|
||||
)
|
||||
? 'preparação confirmada no percurso; não é decisão fiscal'
|
||||
: 'potencialmente aplicável'}
|
||||
. {step.reason}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
)}
|
||||
{snapshot.preparation?.enabled && (
|
||||
<PreparationSummary
|
||||
data={snapshot.preparation}
|
||||
caseId={contextId ?? undefined}
|
||||
/>
|
||||
)}
|
||||
{contextId && (
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/investimentos/${contextId}/contexto/editar`}
|
||||
>
|
||||
Alterar contexto e comparar impacto antes de guardar
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -48,6 +48,16 @@ const labels: Record<string, string> = {
|
||||
authority_id: 'Entidade',
|
||||
publication_state: 'Critério herdado',
|
||||
};
|
||||
const specialisedEmpty: Record<string, string> = {
|
||||
corridor_id: 'Não existem corredores públicos cadastrados para esta localização.',
|
||||
cluster_id: 'Não existem clusters públicos cadastrados para esta localização.',
|
||||
zee_id: 'Não existem ZEE públicas cadastradas para esta localização.',
|
||||
zfi_id: 'Não existem ZFI públicas cadastradas para esta localização.',
|
||||
park_id: 'Não existem parques industriais públicos cadastrados para esta localização.',
|
||||
project_id: 'Não existem projectos âncora públicos cadastrados para esta localização.',
|
||||
subsector_id: 'Não existem actividades disponíveis no catálogo público.',
|
||||
chain_id: 'Não existem cadeias de valor públicas cadastradas para esta localização.',
|
||||
};
|
||||
export default function OpportunitySearchFilters({
|
||||
data,
|
||||
references,
|
||||
@@ -75,6 +85,9 @@ export default function OpportunitySearchFilters({
|
||||
<Field key={key} label={labels[key]}>
|
||||
<select
|
||||
className={input}
|
||||
aria-label={labels[key]}
|
||||
aria-describedby={specialisedEmpty[key] ? `filter-help-${key}` : undefined}
|
||||
disabled={Boolean(specialisedEmpty[key]) && choices.length === 0 && !data[key]}
|
||||
value={String(data[key] ?? '')}
|
||||
onChange={(e) =>
|
||||
change({
|
||||
@@ -86,12 +99,23 @@ export default function OpportunitySearchFilters({
|
||||
}
|
||||
>
|
||||
<option value="">Todos</option>
|
||||
{data[key] && !choices.some((o) => o.value === String(data[key])) && (
|
||||
<option value={String(data[key])}>Selecção indisponível neste contexto — remova o filtro</option>
|
||||
)}
|
||||
{choices.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{specialisedEmpty[key] && (
|
||||
<p id={`filter-help-${key}`} className="text-sm text-slate-600">
|
||||
{choices.length === 0
|
||||
? specialisedEmpty[key]
|
||||
: `${choices.length} opções públicas disponíveis, mesmo sem oportunidades associadas.`}
|
||||
{key === 'subsector_id' && choices.length > 0 && ' Classificação de actividades; não confirma a existência de investimentos nesta localização.'}
|
||||
</p>
|
||||
)}
|
||||
{hint && <p className="text-sm text-slate-600">{hint}</p>}
|
||||
</Field>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import PortalProductIdentity from '@/components/portal-product-identity';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import { Link, router, usePage } from '@inertiajs/react';
|
||||
import {
|
||||
ChevronDown,
|
||||
@@ -47,6 +48,7 @@ export function PortalGroupedMenu() {
|
||||
existingBusinessEnabled?: boolean;
|
||||
businessReviewUrl?: string | null;
|
||||
educationEnabled?: boolean;
|
||||
procurementEnabled?: boolean;
|
||||
aiEnabled?: boolean;
|
||||
legacyContentEnabled?: boolean;
|
||||
profileAccess?: ProfileAccess | null;
|
||||
@@ -65,6 +67,7 @@ export function PortalGroupedMenu() {
|
||||
businesses: props.existingBusinessEnabled,
|
||||
businessReviewUrl: props.businessReviewUrl,
|
||||
education: props.educationEnabled,
|
||||
procurement: props.procurementEnabled,
|
||||
ai: props.aiEnabled,
|
||||
legacyContent: props.legacyContentEnabled,
|
||||
opportunityManagement: props.opportunityManagement,
|
||||
@@ -110,7 +113,7 @@ export function PortalGroupedMenu() {
|
||||
className="tempo-menu-item"
|
||||
>
|
||||
<Link
|
||||
href={item.href!}
|
||||
href={searchTerritoryHref(item.href!, url)}
|
||||
aria-current={
|
||||
currentItem === item.id ? 'page' : undefined
|
||||
}
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { card, input } from '@/pages/investment/shared';
|
||||
|
||||
export type PreparationDocument = {
|
||||
document_id: string;
|
||||
file_name: string;
|
||||
status: string;
|
||||
malware_scan_status: string;
|
||||
};
|
||||
export type Preparation = {
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
mode: string;
|
||||
period_covered: boolean;
|
||||
scope_covered: boolean;
|
||||
reason: string[];
|
||||
missing_fields: string[];
|
||||
missing_labels?: string[];
|
||||
authority: string;
|
||||
next: string;
|
||||
calculation_notice: string;
|
||||
notice: string;
|
||||
source: { url: string; sha256: string };
|
||||
authority_source: { reference: string; url: string };
|
||||
questions: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
type?: string;
|
||||
options?: Record<string, string>;
|
||||
when?: { field: string; equals: string };
|
||||
source: string;
|
||||
help?: string;
|
||||
}>;
|
||||
requirements: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
state: string;
|
||||
required: boolean;
|
||||
document_id: string | null;
|
||||
document: PreparationDocument | null;
|
||||
stage: string;
|
||||
reason: string;
|
||||
source: { reference: string; url: string; period: string };
|
||||
}>;
|
||||
};
|
||||
const states: Record<string, string> = {
|
||||
not_applicable: 'Não aplicável neste contexto',
|
||||
indeterminate: 'Informação em falta para decidir',
|
||||
missing: 'Documento por associar',
|
||||
associated: 'Documento associado — conteúdo por conferir',
|
||||
};
|
||||
export const preparationMode = (mode: string) =>
|
||||
({
|
||||
authorization: 'Pedido de autorização',
|
||||
registration: 'Pedido de mero registo',
|
||||
indeterminate: 'Enquadramento por determinar',
|
||||
})[mode] ?? mode;
|
||||
|
||||
export function PreparationSummary({
|
||||
data,
|
||||
caseId,
|
||||
}: {
|
||||
data: Preparation | null | undefined;
|
||||
caseId?: string;
|
||||
}) {
|
||||
if (!data) return null;
|
||||
return (
|
||||
<section
|
||||
id="private-investment-preparation"
|
||||
className={card + ' space-y-3'}
|
||||
aria-label="Preparação adaptada do investimento privado"
|
||||
>
|
||||
<h2 className="text-xl font-bold">
|
||||
Preparar o pedido de investimento privado
|
||||
</h2>
|
||||
{!data.enabled ? (
|
||||
<p>
|
||||
Use os dados deste dossier para identificar o pedido e os
|
||||
documentos aplicáveis. A preparação é opcional e não atribui
|
||||
benefícios.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
<strong>{preparationMode(data.mode)}</strong> · recorte
|
||||
de 2026 ·{' '}
|
||||
{data.version.replace(
|
||||
'private-investment-preparation-',
|
||||
'',
|
||||
)}
|
||||
</p>
|
||||
<p>{data.next}</p>
|
||||
{data.missing_fields.length > 0 && (
|
||||
<p>
|
||||
Informação em falta:{' '}
|
||||
{(
|
||||
data.missing_labels ??
|
||||
data.missing_fields.map(
|
||||
(key) =>
|
||||
data.questions?.find(
|
||||
(q) => q.key === key,
|
||||
)?.label ??
|
||||
(
|
||||
{
|
||||
currency: 'Moeda do projecto',
|
||||
expected_start_date:
|
||||
'Data prevista de início',
|
||||
entity_stage:
|
||||
'Situação da entidade',
|
||||
} as Record<string, string>
|
||||
)[key] ??
|
||||
key,
|
||||
)
|
||||
).join('; ')}
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
{!data.period_covered && (
|
||||
<p>
|
||||
Este incremento cobre pedidos com início previsto em
|
||||
2026. Uma data em falta ou fora desse recorte não
|
||||
permite determinar o pedido por este lote; os
|
||||
restantes trabalhos do dossier continuam
|
||||
disponíveis.
|
||||
</p>
|
||||
)}
|
||||
{!data.scope_covered && (
|
||||
<p>
|
||||
Declare a actividade efectiva. Prospecção,
|
||||
pesquisa/produção extractiva e investimento público
|
||||
ou exclusivamente social/sem fins lucrativos seguem
|
||||
enquadramentos próprios, fora deste lote.
|
||||
</p>
|
||||
)}
|
||||
{data.reason.map((reason) => (
|
||||
<p key={reason}>{reason}</p>
|
||||
))}
|
||||
<p>
|
||||
Encaminhamento indicativo: {data.authority}.{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={data.authority_source.url}
|
||||
>
|
||||
{data.authority_source.reference}
|
||||
</a>
|
||||
</p>
|
||||
<ul className="space-y-3">
|
||||
{data.requirements
|
||||
.filter((r) => r.state !== 'not_applicable')
|
||||
.map((r) => (
|
||||
<li key={r.key} className="rounded border p-3">
|
||||
<strong>{r.label}</strong> —{' '}
|
||||
{states[r.state]}.{' '}
|
||||
{r.document?.file_name && (
|
||||
<span>
|
||||
Ficheiro: {r.document.file_name}
|
||||
.{' '}
|
||||
</span>
|
||||
)}
|
||||
<p>
|
||||
{r.reason}{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={r.source.url}
|
||||
>
|
||||
Ver fundamento: {r.source.reference}
|
||||
</a>{' '}
|
||||
· {r.source.period}.
|
||||
</p>
|
||||
{caseId && (
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/investimentos/${caseId}/contexto/editar#preparation-documents`}
|
||||
>
|
||||
Rever dados ou associar documento do
|
||||
dossier
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<details>
|
||||
<summary>
|
||||
Exigências que não se aplicam — histórico preservado
|
||||
</summary>
|
||||
{data.requirements
|
||||
.filter((r) => r.state === 'not_applicable')
|
||||
.map((r) => (
|
||||
<p key={r.key}>
|
||||
{r.label}: {r.reason}{' '}
|
||||
{r.document_id && (
|
||||
<>
|
||||
Associação anterior mantida
|
||||
{r.document?.file_name
|
||||
? `: ${r.document.file_name}`
|
||||
: ''}
|
||||
.
|
||||
</>
|
||||
)}{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={r.source.url}
|
||||
>
|
||||
{r.source.reference}
|
||||
</a>
|
||||
</p>
|
||||
))}
|
||||
</details>
|
||||
<p>{data.calculation_notice}</p>
|
||||
<p>{data.notice}</p>
|
||||
</>
|
||||
)}
|
||||
<a className="underline" href={data.source.url}>
|
||||
Consultar o exemplar oficial e os fundamentos
|
||||
</a>
|
||||
{caseId && (
|
||||
<p>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/investimentos/${caseId}/contexto/editar#preparation-questions`}
|
||||
>
|
||||
Completar apenas os dados em falta ou editar declarações
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreparationEditor({
|
||||
data,
|
||||
fields,
|
||||
changes,
|
||||
documents,
|
||||
links,
|
||||
onChange,
|
||||
onLink,
|
||||
disabled,
|
||||
}: {
|
||||
data: Preparation;
|
||||
fields: Record<string, string>;
|
||||
changes: Record<string, string>;
|
||||
documents: PreparationDocument[];
|
||||
links: Record<string, string | null>;
|
||||
onChange: (key: string, value: string) => void;
|
||||
onLink: (key: string, value: string | null) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
const values = { ...fields, ...changes };
|
||||
const active = values.investment_preparation === 'yes';
|
||||
const excluded = ['extractive', 'public_nonprofit'].includes(
|
||||
values.investment_activity_scope,
|
||||
);
|
||||
const applicable = (q: Preparation['questions'][number]) =>
|
||||
!q.when || values[q.when.field] === q.when.equals;
|
||||
// Keep a field in place while the user edits it; collapse only recovered answers.
|
||||
const known = (q: Preparation['questions'][number]) =>
|
||||
!!fields[q.key] && fields[q.key] !== 'unknown';
|
||||
const render = (q: Preparation['questions'][number]) => (
|
||||
<label className="grid gap-1" key={q.key} htmlFor={'prep-' + q.key}>
|
||||
{q.label}
|
||||
{q.options ? (
|
||||
<select
|
||||
id={'prep-' + q.key}
|
||||
className={input}
|
||||
disabled={disabled}
|
||||
value={values[q.key] ?? ''}
|
||||
onChange={(e) => onChange(q.key, e.target.value)}
|
||||
>
|
||||
<option value="">Por indicar</option>
|
||||
{Object.entries(q.options).map(([value, text]) => (
|
||||
<option key={value} value={value}>
|
||||
{text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
id={'prep-' + q.key}
|
||||
className={input}
|
||||
disabled={disabled}
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={values[q.key] ?? ''}
|
||||
onChange={(e) => onChange(q.key, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
<span className="text-sm">
|
||||
{q.help} Fundamento: {q.source}.
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
const questions = data.questions.filter(
|
||||
(q) =>
|
||||
q.key !== 'investment_preparation' &&
|
||||
applicable(q) &&
|
||||
(!excluded || q.key === 'investment_activity_scope'),
|
||||
);
|
||||
const needed = questions.filter(
|
||||
(q) =>
|
||||
!known(q) &&
|
||||
(!data.enabled ||
|
||||
data.mode !== 'authorization' ||
|
||||
data.missing_fields.includes(q.key)),
|
||||
);
|
||||
return (
|
||||
<section
|
||||
id="preparation-questions"
|
||||
className={card + ' space-y-4'}
|
||||
aria-label="Dados do pedido de investimento privado"
|
||||
>
|
||||
<h2 className="text-xl font-bold">
|
||||
Dados em falta para preparar o pedido
|
||||
</h2>
|
||||
<p>
|
||||
Valores conhecidos são reutilizados. As alterações só são
|
||||
gravadas depois da comparação de impacto. Nacionalidade e
|
||||
residência não determinam a origem do capital nem a modalidade
|
||||
da empresa.
|
||||
</p>
|
||||
{render(data.questions[0])}
|
||||
{active && (
|
||||
<>
|
||||
{needed.map(render)}
|
||||
<details>
|
||||
<summary>
|
||||
Dados já declarados — consultar ou alterar
|
||||
</summary>
|
||||
<div className="space-y-3">
|
||||
{questions.filter(known).map(render)}
|
||||
</div>
|
||||
</details>
|
||||
<p>
|
||||
Data, moeda e situação da entidade usam as declarações
|
||||
acima.{' '}
|
||||
{data.missing_fields.filter((k) =>
|
||||
[
|
||||
'currency',
|
||||
'expected_start_date',
|
||||
'entity_stage',
|
||||
].includes(k),
|
||||
).length > 0 &&
|
||||
'Complete esses campos em falta antes de comparar.'}
|
||||
</p>
|
||||
<div id="preparation-documents" className="space-y-3">
|
||||
<h3 className="font-bold">
|
||||
Associar documentos já existentes no dossier
|
||||
</h3>
|
||||
<p>
|
||||
Carregue documentos pelo Balcão Virtual do mesmo
|
||||
dossier. A associação não aprova o conteúdo nem
|
||||
substitui a análise do pedido.
|
||||
</p>
|
||||
{data.requirements
|
||||
.filter((r) => r.required)
|
||||
.map((r) => (
|
||||
<label className="grid gap-1" key={r.key}>
|
||||
{r.label}
|
||||
<select
|
||||
className={input}
|
||||
disabled={disabled}
|
||||
value={
|
||||
Object.hasOwn(links, r.key)
|
||||
? (links[r.key] ?? '')
|
||||
: (r.document_id ?? '')
|
||||
}
|
||||
onChange={(e) =>
|
||||
onLink(
|
||||
r.key,
|
||||
e.target.value || null,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">Por associar</option>
|
||||
{documents.map((doc) => (
|
||||
<option
|
||||
key={doc.document_id}
|
||||
value={doc.document_id}
|
||||
>
|
||||
{doc.file_name} ·{' '}
|
||||
{{
|
||||
quarantined:
|
||||
'Em quarentena',
|
||||
active: 'Disponível',
|
||||
deleted: 'Removido',
|
||||
}[doc.status] ??
|
||||
'Estado por conferir'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span>
|
||||
{r.reason}{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={r.source.url}
|
||||
>
|
||||
{r.source.reference}
|
||||
</a>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<p>
|
||||
Compare o impacto para actualizar o enquadramento, as perguntas
|
||||
e as exigências. Uma exigência que deixe de ser aplicável
|
||||
conserva o documento anterior no histórico.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreparationImpact({
|
||||
before,
|
||||
after,
|
||||
}: {
|
||||
before: Preparation;
|
||||
after: Preparation;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="space-y-2 rounded border border-amber-400 p-3"
|
||||
aria-label="Impacto no pedido de investimento"
|
||||
>
|
||||
<h3 className="font-bold">Impacto no pedido e nos documentos</h3>
|
||||
<p>
|
||||
{preparationMode(before.mode)} → {preparationMode(after.mode)}.
|
||||
</p>
|
||||
<p>Próximo passo: {after.next}.</p>
|
||||
<ul className="list-disc pl-5">
|
||||
{after.requirements
|
||||
.filter(
|
||||
(r) =>
|
||||
before.requirements.find((b) => b.key === r.key)
|
||||
?.state !== r.state,
|
||||
)
|
||||
.map((r) => (
|
||||
<li key={r.key}>
|
||||
{r.label}:{' '}
|
||||
{
|
||||
states[
|
||||
before.requirements.find(
|
||||
(b) => b.key === r.key,
|
||||
)?.state ?? 'indeterminate'
|
||||
]
|
||||
}{' '}
|
||||
→ {states[r.state]}.{' '}
|
||||
{r.document_id && 'Vínculo documental preservado.'}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>{after.calculation_notice}</p>
|
||||
<PreparationSummary data={after} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -71,6 +71,12 @@ export default function TerritoryGeometryPreview({
|
||||
? 'Limite administrativo disponível para este território.'
|
||||
: 'Sem limite administrativo acessível para este território neste perfil.'}
|
||||
</p>
|
||||
{!context.selected_geometry_available && context.areas.length > 0 && (
|
||||
<p role="note">
|
||||
O mapa mostra apenas limites dos níveis superiores como contexto.
|
||||
Estes limites não representam o território seleccionado.
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
{context.context_geometry_count} limites disponíveis na
|
||||
hierarquia selecionada. São contexto administrativo, não
|
||||
@@ -93,7 +99,7 @@ export default function TerritoryGeometryPreview({
|
||||
<ul>
|
||||
{context.areas.map((area, index) => (
|
||||
<li key={index}>
|
||||
{area.title} · Fonte:{' '}
|
||||
{area.title} · {area.selected_territory ? 'Território seleccionado' : 'Contexto de nível superior'} · Fonte:{' '}
|
||||
{area.source_reference ?? 'Não indicada'} ·
|
||||
Edição: {area.source_date ?? 'Não indicada'}
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import SearchFeedback from '@/components/search-feedback';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
import { createContext, useContext } from 'react';
|
||||
import { PortalGroupedMenu } from '@/components/portal-grouped-menu';
|
||||
@@ -148,7 +149,7 @@ export default function PortalNavigation({
|
||||
{parent && (
|
||||
<>
|
||||
<Link
|
||||
href={parent}
|
||||
href={searchTerritoryHref(parent, url)}
|
||||
className="underline"
|
||||
>
|
||||
{names[parent]}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { searchTerritoryHref } from './search-territory.ts';
|
||||
/** Only a query string is carried; the return destination is always local. */
|
||||
export function catalogueDetailHref(path: string, query: string): string {
|
||||
return query
|
||||
@@ -8,5 +9,5 @@ export function catalogueReturnHref(base: string, pageUrl: string): string {
|
||||
const query = new URL(pageUrl, 'https://portal.invalid').searchParams.get(
|
||||
'catalogue_query',
|
||||
);
|
||||
return base + (query ? `?${new URLSearchParams(query).toString()}` : '');
|
||||
return searchTerritoryHref(base + (query ? `?${new URLSearchParams(query).toString()}` : ''), pageUrl);
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ export function inheritSimulatorFields<T extends Record<string,unknown>>(draft:T
|
||||
if(/^\d{4}-\d{2}-\d{2}$/.test(start)) source.monthly_iva_start=start.slice(0,7);
|
||||
else if(/^\d{4}$/.test(String(draft.fiscal_period)) && draft.fiscal_periodicity==='monthly' && /^(0?[1-9]|1[0-2])$/.test(String(draft.fiscal_month))) source.monthly_iva_start=`${draft.fiscal_period}-${String(draft.fiscal_month).padStart(2,'0')}`;
|
||||
if(!settingsValid) delete source.monthly_iva_start;
|
||||
// Legacy drafts without a compatible source need no inheritance marker or autosave churn.
|
||||
if (!Object.keys(source).length && !draft.compatible_fields_snapshot) return draft;
|
||||
const values={...draft,monthly_iva_start:settings.start ?? ''};
|
||||
const patch=inheritCompatibleFields('simulator-compatible-fields',values,source,draft.compatible_fields_snapshot);
|
||||
const next:Record<string,unknown>={...draft,...patch.patch,compatible_fields_snapshot:patch.metadata};
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import type {
|
||||
Preparation,
|
||||
PreparationDocument,
|
||||
} from '@/components/private-investment-preparation';
|
||||
function csrfToken(): string {
|
||||
return decodeURIComponent(
|
||||
document.cookie
|
||||
@@ -56,6 +60,8 @@ export type DossierContext = {
|
||||
dependency_hash?: string;
|
||||
};
|
||||
export type ContextView = {
|
||||
private_investment?: Preparation | null;
|
||||
preparation_documents?: PreparationDocument[];
|
||||
simulator_context_revision?: number | null;
|
||||
case: { id: string; title: string; version: number };
|
||||
context: DossierContext;
|
||||
@@ -73,6 +79,7 @@ export type ContextView = {
|
||||
notice: string;
|
||||
};
|
||||
export type ContextPlan = {
|
||||
private_investment?: { before: Preparation; after: Preparation } | null;
|
||||
preview_hash: string;
|
||||
version: number;
|
||||
revision: number;
|
||||
@@ -119,6 +126,20 @@ export async function contextApi<T>(
|
||||
return data as T;
|
||||
}
|
||||
export const contextFieldNames: Record<string, string> = {
|
||||
investment_form: 'Tipo do investimento',
|
||||
investment_preparation: 'Preparação do pedido',
|
||||
investment_activity_scope: 'Natureza da actividade',
|
||||
implementation_mode: 'Modalidade da empresa',
|
||||
capital_origin: 'Origem dos capitais',
|
||||
site_tenure: 'Utilização das instalações',
|
||||
project_concession: 'PPP ou concessão empresarial',
|
||||
significant_implications: 'Implicações significativas',
|
||||
land_extension: 'Extensão de terra',
|
||||
forest_concession: 'Concessão florestal',
|
||||
forest_concession_hectares: 'Área florestal em hectares',
|
||||
total_investment: 'Valor total do investimento',
|
||||
area_required: 'Área de terra requerida',
|
||||
area_unit: 'Unidade da área',
|
||||
title: 'Título do investimento',
|
||||
territory_id: 'Localização documentada',
|
||||
location_source: 'Origem da localização',
|
||||
|
||||
@@ -8,6 +8,7 @@ export type PortalNavItem = {
|
||||
children?: PortalNavItem[];
|
||||
};
|
||||
export type PortalNavAccess = {
|
||||
procurement?: boolean;
|
||||
authenticated: boolean;
|
||||
manager: boolean;
|
||||
businesses?: boolean;
|
||||
@@ -461,6 +462,7 @@ export function portalNavigation(access: PortalNavAccess): PortalNavItem[] {
|
||||
),
|
||||
link('sectors', 'Sectores de investimento', '/sectores'),
|
||||
link('locations', 'Localizações', '/oportunidades?view=map'),
|
||||
...(access.procurement ? [link('procurement','Concursos e anúncios','/concursos')] : []),
|
||||
{
|
||||
id: 'related-catalogues',
|
||||
label: 'Catálogos relacionados',
|
||||
|
||||
@@ -279,15 +279,75 @@ function makeSections(
|
||||
|
||||
if (draft.context_applicability_snapshot) {
|
||||
try {
|
||||
const context = JSON.parse(String(draft.context_applicability_snapshot));
|
||||
const context = JSON.parse(
|
||||
String(draft.context_applicability_snapshot),
|
||||
);
|
||||
if (context.version === 1) {
|
||||
const section = sections.get('sources');
|
||||
const zone = context.zone;
|
||||
section?.rows.push({field:'Contexto do investimento',value:`Revisão ${context.context_revision}; ${zone ? `${zone.title}, versão ${zone.version}` : 'sem ZEE, ZFI ou parque seleccionado'}`,origin:'Referência do mesmo dossier no momento da gravação',classification:'Contexto versionado; não é reconhecimento jurídico'});
|
||||
section?.paragraphs.push(zone ? 'Benefício fiscal da zona indeterminado: falta regra executável ligada às condições e reconhecimento. O IVA apresentado é apenas cenário geral sem benefício; não representa o regime especial nem o apuramento final.' : 'Cenário geral de preparação; sem confirmação automática de elegibilidade fiscal.');
|
||||
if(zone?.href) section?.rows.push({field:'Ficha e fundamentos da zona',value:zone.href,origin:`Versão consultada ${zone.version}`,classification:'Referência consultável; condições por verificar'});
|
||||
section?.rows.push({
|
||||
field: 'Contexto do investimento',
|
||||
value: `Revisão ${context.context_revision}; ${zone ? `${zone.title}, versão ${zone.version}` : 'sem ZEE, ZFI ou parque seleccionado'}`,
|
||||
origin: 'Referência do mesmo dossier no momento da gravação',
|
||||
classification:
|
||||
'Contexto versionado; não é reconhecimento jurídico',
|
||||
});
|
||||
section?.paragraphs.push(
|
||||
zone
|
||||
? 'Benefício fiscal da zona indeterminado: falta regra executável ligada às condições e reconhecimento. O IVA apresentado é apenas cenário geral sem benefício; não representa o regime especial nem o apuramento final.'
|
||||
: 'Cenário geral de preparação; sem confirmação automática de elegibilidade fiscal.',
|
||||
);
|
||||
const preparation = context.preparation;
|
||||
if (preparation?.enabled) {
|
||||
const modes: Record<string, string> = {
|
||||
authorization: 'Pedido de autorização',
|
||||
registration: 'Pedido de mero registo',
|
||||
indeterminate: 'Enquadramento por determinar',
|
||||
};
|
||||
section?.rows.push({
|
||||
field: 'Preparação do investimento privado',
|
||||
value: modes[preparation.mode] ?? 'Por determinar',
|
||||
origin: `Contexto congelado v${context.context_revision}; ${preparation.version}`,
|
||||
classification:
|
||||
'Declarações e requisitos de preparação; sem aprovação jurídica',
|
||||
});
|
||||
section?.paragraphs.push(
|
||||
preparation.next,
|
||||
preparation.calculation_notice,
|
||||
preparation.notice,
|
||||
);
|
||||
for (const requirement of preparation.requirements ?? [])
|
||||
section?.rows.push({
|
||||
field: requirement.label,
|
||||
value:
|
||||
requirement.state === 'not_applicable'
|
||||
? 'Não aplicável; histórico preservado'
|
||||
: requirement.state === 'associated'
|
||||
? `Associado: ${requirement.document?.file_name ?? 'documento do dossier'}; conteúdo por conferir`
|
||||
: requirement.state === 'missing'
|
||||
? 'Documento por associar'
|
||||
: 'Informação em falta para decidir',
|
||||
origin: `${requirement.reason} ${requirement.source?.reference ?? ''}; ${requirement.source?.url ?? ''}`,
|
||||
classification:
|
||||
'Preparação informativa — simulação, sem submissão à Administração',
|
||||
});
|
||||
}
|
||||
if (zone?.href)
|
||||
section?.rows.push({
|
||||
field: 'Ficha e fundamentos da zona',
|
||||
value: zone.href,
|
||||
origin: `Versão consultada ${zone.version}`,
|
||||
classification:
|
||||
'Referência consultável; condições por verificar',
|
||||
});
|
||||
}
|
||||
} catch { sections.get('sources')?.paragraphs.push('Não foi possível recuperar o contexto guardado; o enquadramento territorial permanece indeterminado.'); }
|
||||
} catch {
|
||||
sections
|
||||
.get('sources')
|
||||
?.paragraphs.push(
|
||||
'Não foi possível recuperar o contexto guardado; o enquadramento territorial permanece indeterminado.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [sectionName, field, value, origin] of rows.slice(1)) {
|
||||
@@ -359,9 +419,23 @@ function makeSections(
|
||||
}
|
||||
|
||||
const finance = sections.get('finance');
|
||||
if (finance && ['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(currentCalculation?.snapshot?.scenario_kind)) && currentCalculation?.snapshot) {
|
||||
for (const [field, value] of fiscalQaRows(currentCalculation.snapshot)) {
|
||||
finance.rows.push({ field, value, origin: 'Snapshot fiscal preservado; sem recálculo', classification: 'Cálculo exacto; ver estado da regra no snapshot; não é saldo a pagar' });
|
||||
if (
|
||||
finance &&
|
||||
['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(
|
||||
String(currentCalculation?.snapshot?.scenario_kind),
|
||||
) &&
|
||||
currentCalculation?.snapshot
|
||||
) {
|
||||
for (const [field, value] of fiscalQaRows(
|
||||
currentCalculation.snapshot,
|
||||
)) {
|
||||
finance.rows.push({
|
||||
field,
|
||||
value,
|
||||
origin: 'Snapshot fiscal preservado; sem recálculo',
|
||||
classification:
|
||||
'Cálculo exacto; ver estado da regra no snapshot; não é saldo a pagar',
|
||||
});
|
||||
}
|
||||
}
|
||||
if (finance && currentCalculation) {
|
||||
@@ -436,13 +510,22 @@ function makeSections(
|
||||
},
|
||||
{
|
||||
field: 'Fórmula e valores usados',
|
||||
value:
|
||||
['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(snapshot?.scenario_kind))
|
||||
? `Base tributável declarada × taxa cadastrada ÷ 100; cálculo decimal exacto, sem arredondamento. Ver regra e proveniência acima.`
|
||||
: snapshot && typeof snapshot.formula_version === 'string'
|
||||
? `Fórmula ${snapshot.formula_version}; base ${displayValue(snapshot.base, 'não disponível')}; taxa ${displayValue(snapshot.rate_percent, 'não disponível')}%; redução ${displayValue(snapshot.reduction_percent, 'não disponível')}%; moeda ${displayValue(snapshot.currency, 'não indicada')}`
|
||||
: 'A fórmula não foi identificada no snapshot; ver explicação da execução',
|
||||
origin: ['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(snapshot?.scenario_kind)) ? (snapshot?.scenario_kind === 'fiscal_rule_published' ? 'Versão publicada na admissão; sem validação jurídica da operação' : 'Snapshot QA de regra em rascunho; sem activação ou validação jurídica da operação') : 'Snapshot guardado; sem aplicação de regra legal',
|
||||
value: [
|
||||
'fiscal_rule_qa_preview',
|
||||
'fiscal_rule_published',
|
||||
].includes(String(snapshot?.scenario_kind))
|
||||
? `Base tributável declarada × taxa cadastrada ÷ 100; cálculo decimal exacto, sem arredondamento. Ver regra e proveniência acima.`
|
||||
: snapshot && typeof snapshot.formula_version === 'string'
|
||||
? `Fórmula ${snapshot.formula_version}; base ${displayValue(snapshot.base, 'não disponível')}; taxa ${displayValue(snapshot.rate_percent, 'não disponível')}%; redução ${displayValue(snapshot.reduction_percent, 'não disponível')}%; moeda ${displayValue(snapshot.currency, 'não indicada')}`
|
||||
: 'A fórmula não foi identificada no snapshot; ver explicação da execução',
|
||||
origin: [
|
||||
'fiscal_rule_qa_preview',
|
||||
'fiscal_rule_published',
|
||||
].includes(String(snapshot?.scenario_kind))
|
||||
? snapshot?.scenario_kind === 'fiscal_rule_published'
|
||||
? 'Versão publicada na admissão; sem validação jurídica da operação'
|
||||
: 'Snapshot QA de regra em rascunho; sem activação ou validação jurídica da operação'
|
||||
: 'Snapshot guardado; sem aplicação de regra legal',
|
||||
classification: 'Reutilização do resultado existente',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
const levels = ['province_id', 'district_id', 'admin_post_id', 'locality_id'];
|
||||
const searchRoutes = ['/carteira', '/oportunidades', '/zonas', '/educacao'];
|
||||
|
||||
/** URL-scoped search context, never a saved investment or another browser tab. */
|
||||
export function searchTerritoryHref(href: string, pageUrl: string): string {
|
||||
const base = 'https://portal.invalid';
|
||||
const source = new URL(pageUrl, base);
|
||||
const target = new URL(href, base);
|
||||
const supported = (path: string) => searchRoutes.some((route) => path === route || path.startsWith(`${route}/`));
|
||||
if (target.origin !== base || !supported(source.pathname) || !supported(target.pathname)) return href;
|
||||
const inherited = source.searchParams.has('catalogue_query')
|
||||
? new URLSearchParams(source.searchParams.get('catalogue_query')!)
|
||||
: source.searchParams;
|
||||
// An explicit ancestor, including an explicit empty value, overrides its descendants.
|
||||
let changed = false;
|
||||
for (const field of levels) {
|
||||
if (target.searchParams.has(field)) {
|
||||
changed ||= target.searchParams.get(field) !== inherited.get(field);
|
||||
} else if (!changed && inherited.get(field)) {
|
||||
target.searchParams.set(field, inherited.get(field)!);
|
||||
}
|
||||
}
|
||||
return target.pathname + target.search + target.hash;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import ProfileNavigation from '@/components/profile-navigation';
|
||||
import ContentManagementEntry from '@/components/content-management-entry';
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import { dashboard } from '@/routes';
|
||||
import type { InvestmentContext } from '@/pages/investor-path/index';
|
||||
@@ -51,6 +52,7 @@ export default function Dashboard({
|
||||
<Head title="Painel" />
|
||||
<h1 className="text-2xl font-semibold">Painel do portal</h1>
|
||||
<ProfileNavigation />
|
||||
<ContentManagementEntry />
|
||||
{summary ? (
|
||||
<>
|
||||
{opportunityManagement && (
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createOperationKey } from '@/lib/operation-key';
|
||||
import ContentManagementEntry from '@/components/content-management-entry';
|
||||
import { Link, useForm, usePage } from '@inertiajs/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Frame, input, button, card } from '@/pages/investment/shared';
|
||||
@@ -84,6 +85,7 @@ export default function Panel({
|
||||
}, [contextTitle, form.data.title]);
|
||||
return (
|
||||
<Frame title="Balcão Virtual — acompanhamento">
|
||||
<ContentManagementEntry />
|
||||
<nav className="flex flex-wrap gap-4">
|
||||
<Link href="/balcao-virtual/servicos" className="underline">
|
||||
Serviços
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import EducationalGuides from '@/components/educational-guides';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import type { EducationalArticle } from '@/components/educational-article-card';
|
||||
import OpportunitySearchFilters from '@/components/opportunity-search-filters';
|
||||
import SearchState from '@/components/search-state';
|
||||
@@ -46,7 +47,8 @@ export default function Catalogue({
|
||||
corridor_id: string | null;
|
||||
}[];
|
||||
}) {
|
||||
const { auth } = usePage<{ auth: { user: unknown } }>().props;
|
||||
const page = usePage<{ auth: { user: unknown } }>();
|
||||
const { auth } = page.props;
|
||||
const form = useForm({
|
||||
association_type: String(filters.association_type ?? ''),
|
||||
association_target: String(filters.association_target ?? ''),
|
||||
@@ -100,6 +102,16 @@ export default function Catalogue({
|
||||
),
|
||||
});
|
||||
const view = String(filters.view ?? 'grid');
|
||||
const selectedStructures = [
|
||||
...(references.zones ?? []).filter((zone) =>
|
||||
['park_id', 'zee_id', 'zfi_id'].some((key) => filters[key] === zone.id),
|
||||
).map((zone) => ({ ...zone, url: `/zonas/${zone.id}` })),
|
||||
...(['clusters', 'corridors', 'projects'] as const).flatMap((group) =>
|
||||
references[group].filter((record) => filters[
|
||||
{ clusters: 'cluster_id', corridors: 'corridor_id', projects: 'project_id' }[group]
|
||||
] === record.id),
|
||||
),
|
||||
];
|
||||
const [territoryPath, setTerritoryPath] = useState(territorySelection);
|
||||
const [saved, setSaved] = useState(favourites);
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
@@ -360,6 +372,17 @@ export default function Catalogue({
|
||||
total={results.total}
|
||||
retry={() => runSearch()}
|
||||
/>
|
||||
{!searchLoading && !searchError && results.total === 0 && selectedStructures.length > 0 && (
|
||||
<section className={card} aria-label="Estrutura seleccionada">
|
||||
<p className="font-semibold">Sem oportunidades publicadas associadas com estes filtros.</p>
|
||||
<p>A estrutura continua disponível para consulta. A sua selecção não confirma benefícios nem disponibilidade para investimento.</p>
|
||||
{selectedStructures.map((record) => (
|
||||
record.url ? <Link key={record.id} className="block underline" href={searchTerritoryHref(record.url, page.url)}>
|
||||
Consultar ficha: {record.name}
|
||||
</Link> : <p key={record.id}>{record.name} — sem ficha pública detalhada disponível.</p>
|
||||
))}
|
||||
</section>
|
||||
)}
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
{!searchLoading &&
|
||||
!searchError &&
|
||||
@@ -696,7 +719,7 @@ export default function Catalogue({
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{territorial.map((d) => (
|
||||
<Link
|
||||
href={`/carteira/${d.id}`}
|
||||
href={searchTerritoryHref(`/carteira/${d.id}`, page.url)}
|
||||
key={d.id}
|
||||
className={card}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import EducationalGuides from '@/components/educational-guides';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import type { EducationalArticle } from '@/components/educational-article-card';
|
||||
import { ClassificationSummary } from '@/components/opportunity-classification';
|
||||
import { Link, router, usePage } from '@inertiajs/react';
|
||||
@@ -45,7 +46,7 @@ export default function Detail({
|
||||
<p>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/carteira/${dossierOrigin.id}`}
|
||||
href={searchTerritoryHref(`/carteira/${dossierOrigin.id}`, page.url)}
|
||||
>
|
||||
Voltar ao dossier: {dossierOrigin.title}
|
||||
</Link>
|
||||
@@ -253,7 +254,7 @@ export default function Detail({
|
||||
<span key={dossier.id}>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/carteira/${dossier.id}`}
|
||||
href={searchTerritoryHref(`/carteira/${dossier.id}`, page.url)}
|
||||
>
|
||||
Consultar dossier: {dossier.title}
|
||||
</Link>
|
||||
|
||||
@@ -19,6 +19,7 @@ export type Option = {
|
||||
export type Entity = {
|
||||
id: string;
|
||||
name: string;
|
||||
url?: string | null;
|
||||
dossiers?: { id: string; title: string; source: string }[];
|
||||
geography?: StructureGeography | null;
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@ import ParkDetailsEditor, {
|
||||
} from '@/components/investment-zone-operational-details';
|
||||
import { Link, router, useForm, usePage } from '@inertiajs/react';
|
||||
import { catalogueReturnHref } from '@/lib/catalogue-context';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import InvestmentTerritory from '@/components/investment-territory';
|
||||
import InvestmentMapPanel from '@/components/investment-map-panel';
|
||||
import {
|
||||
@@ -1772,7 +1773,7 @@ function Editor({
|
||||
<p key={o.id}>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/oportunidades/${o.slug}?zone_id=${z.id}`}
|
||||
href={searchTerritoryHref(`/oportunidades/${o.slug}?zone_id=${z.id}`, pageUrl)}
|
||||
>
|
||||
{o.title}
|
||||
</Link>
|
||||
@@ -1829,7 +1830,7 @@ function Editor({
|
||||
<p key={r.id}>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/carteira/${r.id}`}
|
||||
href={searchTerritoryHref(`/carteira/${r.id}`, pageUrl)}
|
||||
>
|
||||
{r.title}
|
||||
</Link>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import InvestmentZoneInstallation, {
|
||||
type ParkInstallationPlan,
|
||||
} from '@/components/investment-zone-installation';
|
||||
import { Link, router } from '@inertiajs/react';
|
||||
import { Link, router, usePage } from '@inertiajs/react';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import { useState } from 'react';
|
||||
import { catalogueDetailHref } from '@/lib/catalogue-context';
|
||||
import {
|
||||
@@ -90,9 +91,13 @@ export default function Zones({
|
||||
zoneExtensionsAvailable?: boolean;
|
||||
territorySelection?: TerritoryOption[];
|
||||
}) {
|
||||
const pageUrl = usePage().url;
|
||||
const [error, setError] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const exportParams = new URLSearchParams();
|
||||
['province_id', 'district_id', 'admin_post_id', 'locality_id'].forEach((field, index) => {
|
||||
if (territorySelection[index]) exportParams.set(field, territorySelection[index].id);
|
||||
});
|
||||
for (const key of [
|
||||
'q',
|
||||
'category',
|
||||
@@ -148,9 +153,9 @@ export default function Zones({
|
||||
>
|
||||
<Link href="/clusters">Por Cluster</Link>
|
||||
<Link href="/corredores">Por Corredor</Link>
|
||||
<Link href="/zonas?category=zee">Por ZEE</Link>
|
||||
<Link href="/zonas?category=zfi">Por ZFI</Link>
|
||||
<Link href="/zonas?category=industrial_park">
|
||||
<Link href={searchTerritoryHref('/zonas?category=zee', pageUrl)}>Por ZEE</Link>
|
||||
<Link href={searchTerritoryHref('/zonas?category=zfi', pageUrl)}>Por ZFI</Link>
|
||||
<Link href={searchTerritoryHref('/zonas?category=industrial_park', pageUrl)}>
|
||||
Parques industriais
|
||||
</Link>
|
||||
</nav>
|
||||
@@ -343,7 +348,7 @@ export default function Zones({
|
||||
{!management && (
|
||||
<Link
|
||||
className="ml-4 underline"
|
||||
href={`/oportunidades?${new URLSearchParams({ [zone.category === 'zee' ? 'zee_id' : zone.category === 'zfi' ? 'zfi_id' : 'park_id']: zone.id })}`}
|
||||
href={searchTerritoryHref(`/oportunidades?${new URLSearchParams({ [zone.category === 'zee' ? 'zee_id' : zone.category === 'zfi' ? 'zfi_id' : 'park_id']: zone.id })}`, pageUrl)}
|
||||
>
|
||||
Ver oportunidades associadas
|
||||
</Link>
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
PreparationEditor,
|
||||
PreparationImpact,
|
||||
} from '@/components/private-investment-preparation';
|
||||
import { InvestmentContextAssessment } from '@/components/investment-context-assessment';
|
||||
import ContextDocuments from '@/components/context-documents';
|
||||
import { Link } from '@inertiajs/react';
|
||||
@@ -36,6 +40,7 @@ export default function ContextEditor({
|
||||
}) {
|
||||
const navigation = useNavigationContext();
|
||||
const [view, setView] = useState(initial);
|
||||
const [preparation, setPreparation] = useState(initial.private_investment);
|
||||
const [options, setOptions] = useState(initialOptions);
|
||||
const [query, setQuery] = useState('');
|
||||
const [changes, setChanges] = useState<Partial<Record<Slot, string>>>(() =>
|
||||
@@ -49,6 +54,9 @@ export default function ContextEditor({
|
||||
const [declarations, setDeclarations] = useState<Record<string, string>>(
|
||||
{},
|
||||
);
|
||||
const [documentLinks, setDocumentLinks] = useState<
|
||||
Record<string, string | null>
|
||||
>({});
|
||||
const [replace, setReplace] = useState<string[]>([]);
|
||||
const [plan, setPlan] = useState<ContextPlan | null>(null);
|
||||
const [busy, setBusy] = useState(false);
|
||||
@@ -59,6 +67,32 @@ export default function ContextEditor({
|
||||
string,
|
||||
unknown
|
||||
> | null>(null);
|
||||
const displayValue = (
|
||||
field: string,
|
||||
value: string | null | undefined,
|
||||
): string => {
|
||||
if (value === null || value === undefined || value === '')
|
||||
return 'não informado';
|
||||
const question = view.private_investment?.questions.find(
|
||||
(q) => q.key === field,
|
||||
);
|
||||
return (
|
||||
question?.options?.[value] ??
|
||||
(
|
||||
{
|
||||
being_formed: 'Em constituição',
|
||||
existing: 'Existente',
|
||||
national: 'Nacional',
|
||||
foreign: 'Estrangeiro',
|
||||
mixed: 'Misto',
|
||||
yes: 'Sim',
|
||||
no: 'Não',
|
||||
unknown: 'Por determinar',
|
||||
} as Record<string, string>
|
||||
)[value] ??
|
||||
value
|
||||
);
|
||||
};
|
||||
const selected = (slot: Slot) =>
|
||||
Object.hasOwn(changes, slot)
|
||||
? (changes[slot] ?? '')
|
||||
@@ -77,6 +111,7 @@ export default function ContextEditor({
|
||||
selection: changes,
|
||||
declarations,
|
||||
replace_fields: replace,
|
||||
document_links: documentLinks,
|
||||
};
|
||||
try {
|
||||
const result = await contextApi<ContextPlan>(
|
||||
@@ -85,6 +120,9 @@ export default function ContextEditor({
|
||||
);
|
||||
setRequestBody(body);
|
||||
setPlan(result);
|
||||
setPreparation(
|
||||
result.private_investment?.after ?? view.private_investment,
|
||||
);
|
||||
} catch (e) {
|
||||
setError(
|
||||
e instanceof Error ? e.message : 'Não foi possível comparar.',
|
||||
@@ -111,9 +149,11 @@ export default function ContextEditor({
|
||||
`/investimentos/${view.case.id}/contexto`,
|
||||
);
|
||||
setView(next);
|
||||
setPreparation(next.private_investment);
|
||||
navigation.refresh();
|
||||
setChanges({});
|
||||
setDeclarations({});
|
||||
setDocumentLinks({});
|
||||
setReplace([]);
|
||||
setPlan(null);
|
||||
setOperation(createOperationKey());
|
||||
@@ -132,7 +172,10 @@ export default function ContextEditor({
|
||||
}
|
||||
return (
|
||||
<Frame title="Contexto do investimento">
|
||||
<InvestmentContextAssessment contextId={view.case.id} raw={view.prefill.simulator?.context_applicability_snapshot} />
|
||||
<InvestmentContextAssessment
|
||||
contextId={view.case.id}
|
||||
raw={view.prefill.simulator?.context_applicability_snapshot}
|
||||
/>
|
||||
<ContextDocuments source={view.context.selection.opportunity} />
|
||||
<div className="space-y-5 [overflow-wrap:anywhere]">
|
||||
<Link
|
||||
@@ -337,17 +380,107 @@ export default function ContextEditor({
|
||||
Estes campos pertencem ao processo. Alterá-los não muda
|
||||
o cadastro da oportunidade nem o perfil da organização.
|
||||
</p>
|
||||
<p>Origem do investidor, residência fiscal e constituição da entidade são declarações distintas. A localização não confirma benefícios. A prévia identifica o impacto antes de guardar.</p>
|
||||
<p>
|
||||
Origem do investidor, residência fiscal e constituição
|
||||
da entidade são declarações distintas. A localização não
|
||||
confirma benefícios. A prévia identifica o impacto antes
|
||||
de guardar.
|
||||
</p>
|
||||
{[
|
||||
['currency','Moeda do projecto',null],
|
||||
['expected_start_date','Data prevista de início',null],
|
||||
['investor_origin','Origem do investidor',[['national','Nacional'],['foreign','Estrangeiro'],['mixed','Misto']]],
|
||||
['person_type','Tipo de entidade',[['singular','Pessoa singular'],['collective','Pessoa colectiva'],['consortium','Consórcio']]],
|
||||
['entity_stage','Situação da entidade',[['existing','Existente'],['being_formed','Em constituição']]],
|
||||
['tax_residency_country','País de residência fiscal declarado (ISO de duas letras)',null],
|
||||
].map(([key,label,choices]) => <label className="grid gap-1" key={String(key)}>{String(label)}{Array.isArray(choices)
|
||||
? <select className={input} value={declarations[String(key)] ?? view.context.fields[String(key)] ?? ''} onChange={e=>{setDeclarations({...declarations,[String(key)]:e.target.value});setPlan(null);}}><option value="">Por indicar</option>{choices.map(([value,text])=><option key={value} value={value}>{text}</option>)}</select>
|
||||
: <input className={input} type={key==='expected_start_date'?'date':'text'} maxLength={key==='currency'?3:key==='tax_residency_country'?2:10} value={declarations[String(key)] ?? view.context.fields[String(key)] ?? ''} onChange={e=>{setDeclarations({...declarations,[String(key)]:e.target.value});setPlan(null);}} />}</label>)}
|
||||
['currency', 'Moeda do projecto', null],
|
||||
[
|
||||
'expected_start_date',
|
||||
'Data prevista de início',
|
||||
null,
|
||||
],
|
||||
[
|
||||
'investor_origin',
|
||||
'Origem do investidor',
|
||||
[
|
||||
['national', 'Nacional'],
|
||||
['foreign', 'Estrangeiro'],
|
||||
['mixed', 'Misto'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'person_type',
|
||||
'Tipo de entidade',
|
||||
[
|
||||
['singular', 'Pessoa singular'],
|
||||
['collective', 'Pessoa colectiva'],
|
||||
['consortium', 'Consórcio'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'entity_stage',
|
||||
'Situação da entidade',
|
||||
[
|
||||
['existing', 'Existente'],
|
||||
['being_formed', 'Em constituição'],
|
||||
],
|
||||
],
|
||||
[
|
||||
'tax_residency_country',
|
||||
'País de residência fiscal declarado (ISO de duas letras)',
|
||||
null,
|
||||
],
|
||||
].map(([key, label, choices]) => (
|
||||
<label className="grid gap-1" key={String(key)}>
|
||||
{String(label)}
|
||||
{Array.isArray(choices) ? (
|
||||
<select
|
||||
className={input}
|
||||
value={
|
||||
declarations[String(key)] ??
|
||||
view.context.fields[String(key)] ??
|
||||
''
|
||||
}
|
||||
onChange={(e) => {
|
||||
setDeclarations({
|
||||
...declarations,
|
||||
[String(key)]: e.target.value,
|
||||
});
|
||||
setPlan(null);
|
||||
}}
|
||||
>
|
||||
<option value="">Por indicar</option>
|
||||
{choices.map(([value, text]) => (
|
||||
<option key={value} value={value}>
|
||||
{text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
className={input}
|
||||
type={
|
||||
key === 'expected_start_date'
|
||||
? 'date'
|
||||
: 'text'
|
||||
}
|
||||
maxLength={
|
||||
key === 'currency'
|
||||
? 3
|
||||
: key === 'tax_residency_country'
|
||||
? 2
|
||||
: 10
|
||||
}
|
||||
value={
|
||||
declarations[String(key)] ??
|
||||
view.context.fields[String(key)] ??
|
||||
''
|
||||
}
|
||||
onChange={(e) => {
|
||||
setDeclarations({
|
||||
...declarations,
|
||||
[String(key)]: e.target.value,
|
||||
});
|
||||
setPlan(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
))}
|
||||
{['project_title', 'project_summary'].map((k) => (
|
||||
<label className="grid gap-1" key={k}>
|
||||
{contextFieldNames[k]}
|
||||
@@ -379,18 +512,7 @@ export default function ContextEditor({
|
||||
declarações do utilizador. Autorize somente os
|
||||
campos que pretende substituir pelo cadastro.
|
||||
</p>
|
||||
<p>Origem do investidor, residência fiscal e constituição da entidade são declarações distintas. A localização não confirma benefícios. A prévia identifica o impacto antes de guardar.</p>
|
||||
{[
|
||||
['currency','Moeda do projecto',null],
|
||||
['expected_start_date','Data prevista de início',null],
|
||||
['investor_origin','Origem do investidor',[['national','Nacional'],['foreign','Estrangeiro'],['mixed','Misto']]],
|
||||
['person_type','Tipo de entidade',[['singular','Pessoa singular'],['collective','Pessoa colectiva'],['consortium','Consórcio']]],
|
||||
['entity_stage','Situação da entidade',[['existing','Existente'],['being_formed','Em constituição']]],
|
||||
['tax_residency_country','País de residência fiscal declarado (ISO de duas letras)',null],
|
||||
].map(([key,label,choices]) => <label className="grid gap-1" key={String(key)}>{String(label)}{Array.isArray(choices)
|
||||
? <select className={input} value={declarations[String(key)] ?? view.context.fields[String(key)] ?? ''} onChange={e=>{setDeclarations({...declarations,[String(key)]:e.target.value});setPlan(null);}}><option value="">Por indicar</option>{choices.map(([value,text])=><option key={value} value={value}>{text}</option>)}</select>
|
||||
: <input className={input} type={key==='expected_start_date'?'date':'text'} maxLength={key==='currency'?3:key==='tax_residency_country'?2:10} value={declarations[String(key)] ?? view.context.fields[String(key)] ?? ''} onChange={e=>{setDeclarations({...declarations,[String(key)]:e.target.value});setPlan(null);}} />}</label>)}
|
||||
{['project_title', 'project_summary'].map((k) => (
|
||||
{['project_title', 'project_summary'].map((k) => (
|
||||
<label className="block" key={k}>
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -411,6 +533,30 @@ export default function ContextEditor({
|
||||
</label>
|
||||
))}
|
||||
</details>
|
||||
{view.private_investment && (
|
||||
<PreparationEditor
|
||||
data={preparation ?? view.private_investment}
|
||||
fields={view.context.fields}
|
||||
changes={declarations}
|
||||
documents={view.preparation_documents ?? []}
|
||||
links={documentLinks}
|
||||
disabled={busy || !view.can_write}
|
||||
onChange={(key, value) => {
|
||||
setDeclarations({
|
||||
...declarations,
|
||||
[key]: value,
|
||||
});
|
||||
setPlan(null);
|
||||
}}
|
||||
onLink={(key, value) => {
|
||||
setDocumentLinks({
|
||||
...documentLinks,
|
||||
[key]: value,
|
||||
});
|
||||
setPlan(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
className={button}
|
||||
type="button"
|
||||
@@ -467,10 +613,15 @@ export default function ContextEditor({
|
||||
x.field
|
||||
] ?? x.field}
|
||||
:{' '}
|
||||
{x.before ??
|
||||
'não informado'}{' '}
|
||||
{displayValue(
|
||||
x.field,
|
||||
x.before,
|
||||
)}{' '}
|
||||
→{' '}
|
||||
{x.after ?? 'não informado'}
|
||||
{displayValue(
|
||||
x.field,
|
||||
x.after,
|
||||
)}
|
||||
</p>
|
||||
))}
|
||||
{Object.values(plan.selection).map(
|
||||
@@ -530,6 +681,9 @@ export default function ContextEditor({
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{plan.private_investment && (
|
||||
<PreparationImpact {...plan.private_investment} />
|
||||
)}
|
||||
<p>{plan.notice}</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<button
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
PreparationSummary,
|
||||
type Preparation,
|
||||
} from '@/components/private-investment-preparation';
|
||||
import ContextDocuments from '@/components/context-documents';
|
||||
import type { DossierContext } from '@/lib/investor-context';
|
||||
import FintechInvestment, {
|
||||
@@ -27,6 +31,7 @@ type Step = {
|
||||
source_paragraph: number;
|
||||
};
|
||||
type Path = {
|
||||
private_investment?: Preparation | null;
|
||||
fintech: FintechView | null;
|
||||
investment_context: DossierContext | null;
|
||||
zone_contexts?: {
|
||||
@@ -180,6 +185,28 @@ function Stage({ step, path }: { step: Step; path: Path }) {
|
||||
independente.
|
||||
</label>
|
||||
)}
|
||||
{path.private_investment?.enabled && (
|
||||
<div className="space-y-2">
|
||||
{path.private_investment.requirements
|
||||
.filter((r) => r.stage === step.code)
|
||||
.map((r) => (
|
||||
<p className="text-sm" key={r.key}>
|
||||
<strong>{r.label}:</strong>{' '}
|
||||
{r.state === 'not_applicable'
|
||||
? 'não exigido neste contexto; histórico preservado'
|
||||
: r.state === 'associated'
|
||||
? 'documento associado; conteúdo por conferir'
|
||||
: r.state === 'missing'
|
||||
? 'documento por associar'
|
||||
: 'aplicabilidade por determinar'}
|
||||
. {r.reason}{' '}
|
||||
<a className="underline" href={r.source.url}>
|
||||
{r.source.reference}
|
||||
</a>
|
||||
</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{step.reason && (
|
||||
<p className="text-sm">Fundamento: {step.reason}</p>
|
||||
)}
|
||||
@@ -415,6 +442,10 @@ export default function InvestmentPath(path: Path) {
|
||||
<ContextDocuments
|
||||
source={path.investment_context?.selection.opportunity}
|
||||
/>
|
||||
<PreparationSummary
|
||||
data={path.private_investment}
|
||||
caseId={path.case.id}
|
||||
/>
|
||||
{path.investment_context && (
|
||||
<section className="space-y-2 rounded border border-emerald-300 bg-emerald-50 p-4">
|
||||
<h2 className="font-semibold">
|
||||
|
||||
@@ -6,7 +6,7 @@ export default function ManagementAccess({
|
||||
}: {
|
||||
registrationEnabled: boolean;
|
||||
}) {
|
||||
const { auth } = usePage<{ auth: Auth }>().props;
|
||||
const { auth, contentManagementHref } = usePage<{ auth: Auth; contentManagementHref?: string | null }>().props;
|
||||
const user = auth.user;
|
||||
const action =
|
||||
'inline-flex rounded-lg bg-slate-900 px-5 py-3 font-medium text-white';
|
||||
@@ -29,7 +29,7 @@ export default function ManagementAccess({
|
||||
<h2 className="text-xl font-semibold">
|
||||
{!user
|
||||
? 'Preparar a sua conta'
|
||||
: auth.canManagePortal
|
||||
: contentManagementHref || auth.canManagePortal
|
||||
? 'Acesso disponível'
|
||||
: !user.email_verified_at
|
||||
? 'Verificar o seu email'
|
||||
@@ -53,6 +53,11 @@ export default function ManagementAccess({
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : contentManagementHref ? (
|
||||
<>
|
||||
<p>Abra a entrada de gestão para cadastrar oportunidades e gerir documentos e legislação, conforme as funções já atribuídas à sua conta.</p>
|
||||
<Link className={action} href={contentManagementHref}>Abrir gestão de conteúdos e oportunidades</Link>
|
||||
</>
|
||||
) : auth.canManagePortal ? (
|
||||
<>
|
||||
<p>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { Frame, card, button } from '@/pages/investment/shared';
|
||||
import { states } from './shared';
|
||||
|
||||
type Access = {
|
||||
procurement?: {index:string;create:string};
|
||||
opportunities?: {index: string; create: string | null; profileWorkflow: boolean};
|
||||
documents?: {index: string; edit: boolean};
|
||||
};
|
||||
export default function ContentHub({access, counts}: {access: Access; counts: Record<string, Record<string, number>>}) {
|
||||
return <Frame title="Gestão de conteúdos e oportunidades">
|
||||
<p>Use os cadastros e a biblioteca existentes. Guardar um rascunho, analisar um ficheiro e publicar são operações distintas. Cada acção respeita a sua autorização actual.</p>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{access.procurement && <section className={card}><h2 className="text-xl font-semibold">Concursos e anúncios</h2><p>Cadastre o procedimento e os lotes, guarde e recupere a preparação, reveja a prévia e publique os actos autorizados. Esclarecimentos, prorrogações, resultados e cancelamentos conservam versões e histórico. Divulgação de oportunidade e abertura de concurso são operações distintas.</p><Link className={button} href={access.procurement.create}>Novo concurso/anúncio</Link><Link className="block underline" href={access.procurement.index}>Gerir concursos e anúncios</Link><Link className="block underline" href="/concursos">Catálogo público</Link></section>}
|
||||
{access.opportunities && <section className={card}>
|
||||
<h2 className="text-xl font-semibold">Oportunidades</h2>
|
||||
<p>Escolha a modalidade, o sector e a localização; preencha, guarde o rascunho e siga as acções de submissão, revisão e publicação disponíveis no seu perfil.</p>
|
||||
<p>Sector económico e modalidade são escolhas distintas. Pode guardar preparação incompleta; o editor identifica os campos e variantes ainda sem suporte de submissão.</p>
|
||||
{access.opportunities.create && <Link className={button} href={access.opportunities.create}>Nova oportunidade</Link>}
|
||||
<Link className="block underline" href={access.opportunities.index}>Gerir oportunidades e histórico</Link>
|
||||
<ul>{Object.entries(counts.opportunities ?? {}).map(([state, total]) => <li key={state}><Link className="underline" href={`${access.opportunities!.index}?status=${state}`}>{states[state] ?? state}: {total}</Link></li>)}</ul>
|
||||
<p className="text-sm">As etapas e a independência já definidas para oportunidades mantêm-se. Esta entrada não atribui autorização para rever ou publicar.</p>
|
||||
{!access.opportunities.profileWorkflow && <p>O seu acesso actual abre o cadastro legado de rascunhos. A submissão, revisão e publicação de novas oportunidades utilizam os perfis operacionais já existentes; a administração de conteúdos não substitui essa designação. Esta entrada não apresenta o rascunho legado como percurso completo de publicação.</p>}
|
||||
</section>}
|
||||
{access.documents && <section className={card}>
|
||||
<h2 className="text-xl font-semibold">Documentos e legislação</h2>
|
||||
<p>Carregue o original ou reutilize um ficheiro existente; acompanhe a análise, complete os metadados e associações e defina o acesso. Depois publique explicitamente, se autorizado.</p>
|
||||
<div className="flex flex-wrap gap-3"><Link className={button} href={`${access.documents.index}?intent=document#receber`}>Novo documento</Link>
|
||||
{access.documents.edit && <Link className={button} href={`${access.documents.index}?intent=legislation#receber`}>Nova legislação</Link>}</div>
|
||||
<Link className="block underline" href={`${access.documents.index}#conteudos`}>Gerir conteúdos, versões e histórico</Link>
|
||||
{!access.documents.edit && <p>O seu acesso permite carregar e analisar ficheiros próprios; não inclui cadastro editorial nem publicação.</p>}
|
||||
<p>Publicados: {counts.documents?.published ?? 0} · Não publicados: {counts.documents?.unpublished ?? 0}. A lista distingue rascunhos de conteúdos retirados através do histórico.</p>
|
||||
<p className="text-sm">Publicar legislação permite consultá-la. Não a activa como regra executável no simulador.</p>
|
||||
</section>}
|
||||
</div>
|
||||
<section className={card}><h2 className="font-semibold">Como concluir sem apoio técnico</h2>
|
||||
<ol className="list-decimal space-y-2 pl-5"><li>Abra a acção pretendida e use os campos do editor existente.</li><li>Guarde a preparação. As mensagens indicam o estado e o que precisa de corrigir.</li><li>Reabra pela gestão, pesquise o título e consulte o histórico.</li><li>Use as acções autorizadas de publicação ou retirada. Os originais e as revisões são preservados.</li></ol>
|
||||
</section>
|
||||
</Frame>;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ClassificationReview } from '@/components/opportunity-classification';
|
||||
import { createOperationKey } from '@/lib/operation-key';
|
||||
import { valueText } from '@/lib/value-text';
|
||||
import { modalityLabels } from '@/components/opportunity-search-filters';
|
||||
import OpportunityProfileActions from '@/components/opportunity-profile-actions';
|
||||
import OpportunityContactForm from '@/components/opportunity-contact-form';
|
||||
import OpportunityCanonicalFields, {
|
||||
@@ -330,7 +331,7 @@ export default function OpportunityDraft({
|
||||
{c.organization_name ??
|
||||
c.organization_id ??
|
||||
'Âmbito individual'}{' '}
|
||||
— {c.type}
|
||||
— {modalityLabels[c.type] ?? c.type}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import ProfileNavigation from '@/components/profile-navigation';
|
||||
import ContentManagementEntry from '@/components/content-management-entry';
|
||||
import { Head, Link, usePage } from '@inertiajs/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { cloneElement, isValidElement, useId } from 'react';
|
||||
@@ -60,6 +61,7 @@ export function Management({
|
||||
return (
|
||||
<div className="space-y-6 p-4 md:p-8">
|
||||
<Head title={title} />
|
||||
<ContentManagementEntry />
|
||||
{auth?.canManagePortal && basePath !== '/minhas-oportunidades' ? (
|
||||
<nav
|
||||
className="flex flex-wrap gap-4 text-sm underline"
|
||||
|
||||
@@ -1,14 +1,35 @@
|
||||
import { Link, useForm } from '@inertiajs/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Management, Field, button, control } from '../management/shared';
|
||||
type Content = { body: string; image_resource_id: number | null };
|
||||
import EditorialReading, {
|
||||
type EditorialArticle,
|
||||
} from '@/components/editorial-reading';
|
||||
type Content = {
|
||||
body: string;
|
||||
image_resource_id: number | null;
|
||||
image_mode?: string;
|
||||
};
|
||||
export default function ArticleEditor({
|
||||
resource,
|
||||
content,
|
||||
source,
|
||||
images,
|
||||
revisions,
|
||||
}: {
|
||||
resource: { id: number; title: string; metadata_version: number };
|
||||
resource: {
|
||||
id: number;
|
||||
title: string;
|
||||
metadata_version: number;
|
||||
published: boolean;
|
||||
};
|
||||
content: Content | null;
|
||||
source?: {
|
||||
source_url?: string;
|
||||
image_source_url?: string;
|
||||
published_at?: string;
|
||||
modified_at?: string;
|
||||
legacy_id?: number;
|
||||
} | null;
|
||||
images: { id: number; title: string }[];
|
||||
revisions: {
|
||||
revision_no: number;
|
||||
@@ -16,28 +37,107 @@ export default function ArticleEditor({
|
||||
created_at: string;
|
||||
}[];
|
||||
}) {
|
||||
const form = useForm({
|
||||
const initial = () => ({
|
||||
body: content?.body ?? '',
|
||||
image_resource_id: content?.image_resource_id
|
||||
? String(content.image_resource_id)
|
||||
: '',
|
||||
image_mode:
|
||||
content?.image_mode ??
|
||||
(content?.image_resource_id ? 'selected' : 'none'),
|
||||
metadata_version: resource.metadata_version,
|
||||
});
|
||||
const form = useForm(initial());
|
||||
const [preview, setPreview] = useState<EditorialArticle | null>(null);
|
||||
const [error, setError] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
useEffect(() => {
|
||||
form.setData(initial());
|
||||
setPreview(null);
|
||||
setError('');
|
||||
}, [resource.id, resource.metadata_version]);
|
||||
useEffect(() => {
|
||||
setPreview(null);
|
||||
}, [form.data.body, form.data.image_mode, form.data.image_resource_id]);
|
||||
const showPreview = async () => {
|
||||
setBusy(true);
|
||||
setError('');
|
||||
try {
|
||||
const token = document.cookie
|
||||
.split('; ')
|
||||
.find((v) => v.startsWith('XSRF-TOKEN='))
|
||||
?.slice(11);
|
||||
const response = await fetch(
|
||||
`/management/portal-services/resources/${resource.id}/article/preview`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-XSRF-TOKEN': decodeURIComponent(token ?? ''),
|
||||
},
|
||||
body: JSON.stringify(form.data),
|
||||
},
|
||||
);
|
||||
const data = await response
|
||||
.json()
|
||||
.catch(() => ({
|
||||
message:
|
||||
'Não foi possível abrir a prévia. Recupere o editor e confirme a sessão e o MFA existentes.',
|
||||
}));
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
Object.values(data.errors ?? {})
|
||||
.flat()
|
||||
.join(' ') ||
|
||||
data.message ||
|
||||
'Não foi possível preparar a prévia.',
|
||||
);
|
||||
setPreview(data.article);
|
||||
} catch (e) {
|
||||
setError(
|
||||
e instanceof Error
|
||||
? e.message
|
||||
: 'Não foi possível preparar a prévia.',
|
||||
);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Management title={`Editar artigo — ${resource.title}`}>
|
||||
<p>
|
||||
O original permanece intacto na biblioteca. Guardar o corpo
|
||||
retira a publicação até à sua ação expressa no acervo.
|
||||
O original, a origem e as versões permanecem na biblioteca.
|
||||
Guardar cria uma revisão editorial e retira a publicação até à
|
||||
sua acção expressa no acervo.
|
||||
</p>
|
||||
<p role="status">
|
||||
Revisão guardada {resource.metadata_version} ·{' '}
|
||||
{resource.published ? 'Publicado' : 'Rascunho privado'}
|
||||
</p>
|
||||
{source && (
|
||||
<p>
|
||||
Original {source.legacy_id} · publicado em{' '}
|
||||
{source.published_at} · modificado em {source.modified_at}.
|
||||
A imagem de origem mantém a sua referência mesmo quando não
|
||||
existe exemplar disponível.
|
||||
</p>
|
||||
)}
|
||||
<form
|
||||
className="max-w-3xl space-y-5"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
setError('');
|
||||
form.put(
|
||||
`/management/portal-services/resources/${resource.id}/article`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
{!!Object.keys(form.errors).length && (
|
||||
<div role="alert">
|
||||
{Object.values(form.errors).join(' ')}
|
||||
</div>
|
||||
)}
|
||||
<Field label="Corpo do artigo" error={form.errors.body}>
|
||||
<textarea
|
||||
className={control}
|
||||
@@ -46,36 +146,93 @@ export default function ArticleEditor({
|
||||
onChange={(e) => form.setData('body', e.target.value)}
|
||||
/>
|
||||
<p>
|
||||
Texto simples. Separe os parágrafos com uma linha em
|
||||
branco.
|
||||
Texto simples, com uma linha em branco entre parágrafos.
|
||||
Se mantiver o texto original, a leitura conserva também
|
||||
a sua formatação; as correcções ficam numa edição
|
||||
separada.
|
||||
</p>
|
||||
</Field>
|
||||
<Field
|
||||
label="Imagem da biblioteca"
|
||||
error={form.errors.image_resource_id}
|
||||
label="Tratamento da imagem"
|
||||
error={form.errors.image_mode}
|
||||
>
|
||||
<select
|
||||
className={control}
|
||||
value={form.data.image_resource_id}
|
||||
value={form.data.image_mode}
|
||||
onChange={(e) =>
|
||||
form.setData('image_resource_id', e.target.value)
|
||||
form.setData('image_mode', e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="">Sem imagem</option>
|
||||
{images.map((i) => (
|
||||
<option key={i.id} value={i.id}>
|
||||
{i.title}
|
||||
</option>
|
||||
))}
|
||||
<option value="original">
|
||||
Manter referência e imagem de origem disponível
|
||||
</option>
|
||||
<option value="selected">
|
||||
Escolher imagem pública da biblioteca
|
||||
</option>
|
||||
<option value="none">
|
||||
Não mostrar imagem nesta edição
|
||||
</option>
|
||||
</select>
|
||||
</Field>
|
||||
<button className={button} disabled={form.processing}>
|
||||
Guardar artigo como rascunho
|
||||
</button>{' '}
|
||||
<Link className="underline" href="/management/portal-services">
|
||||
Voltar ao acervo
|
||||
</Link>
|
||||
{form.data.image_mode === 'selected' && (
|
||||
<Field
|
||||
label="Imagem da biblioteca"
|
||||
error={form.errors.image_resource_id}
|
||||
>
|
||||
<select
|
||||
className={control}
|
||||
value={form.data.image_resource_id}
|
||||
onChange={(e) =>
|
||||
form.setData(
|
||||
'image_resource_id',
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">Seleccione uma imagem</option>
|
||||
{images.map((i) => (
|
||||
<option key={i.id} value={i.id}>
|
||||
{i.title}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<button className={button} disabled={form.processing}>
|
||||
Guardar artigo como rascunho
|
||||
</button>
|
||||
<button
|
||||
className={button}
|
||||
type="button"
|
||||
disabled={busy}
|
||||
onClick={() => void showPreview()}
|
||||
>
|
||||
Pré-visualizar alterações
|
||||
</button>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/management/portal-services/resources/${resource.id}/article/preview`}
|
||||
>
|
||||
Ler revisão guardada
|
||||
</Link>
|
||||
<Link
|
||||
className="underline"
|
||||
href="/management/portal-services"
|
||||
>
|
||||
Voltar ao acervo para metadados e publicação
|
||||
</Link>
|
||||
</div>
|
||||
</form>
|
||||
{error && <p role="alert">{error}</p>}
|
||||
{preview && (
|
||||
<section
|
||||
aria-label="Prévia das alterações"
|
||||
className="my-6 max-w-3xl rounded border p-5"
|
||||
>
|
||||
<EditorialReading article={preview} />
|
||||
</section>
|
||||
)}
|
||||
<h2>Histórico</h2>
|
||||
<ul>
|
||||
{revisions.map((r) => (
|
||||
@@ -86,8 +243,10 @@ export default function ArticleEditor({
|
||||
))}
|
||||
</ul>
|
||||
<p>
|
||||
Título, descrição, autor e fonte são editados em Editar
|
||||
metadados no acervo.
|
||||
Título, descrição, autor, fonte, direitos e acesso são
|
||||
corrigidos em Editar metadados no acervo. Carregue e analise
|
||||
imagens na biblioteca antes de as seleccionar. Publicar continua
|
||||
a ser uma acção separada.
|
||||
</p>
|
||||
</Management>
|
||||
);
|
||||
|
||||
@@ -1,42 +1,27 @@
|
||||
import { Head, Link } from '@inertiajs/react';
|
||||
import { PortalMenu } from '@/layouts/portal-navigation';
|
||||
export default function Article({
|
||||
article,
|
||||
}: {
|
||||
article: {
|
||||
id: number;
|
||||
title: string;
|
||||
description: string | null;
|
||||
author: string | null;
|
||||
body: string;
|
||||
image_url: string | null;
|
||||
original_url: string;
|
||||
};
|
||||
}) {
|
||||
import EditorialReading, {
|
||||
type EditorialArticle,
|
||||
} from '@/components/editorial-reading';
|
||||
export default function Article({ article }: { article: EditorialArticle }) {
|
||||
return (
|
||||
<>
|
||||
<Head title={article.title} />
|
||||
<PortalMenu />
|
||||
<main className="mx-auto max-w-3xl space-y-6 p-6">
|
||||
<h1 className="text-3xl font-bold">{article.title}</h1>
|
||||
{article.description && <p>{article.description}</p>}
|
||||
{article.author && <p>Autor / entidade: {article.author}</p>}
|
||||
{article.image_url && (
|
||||
<img src={article.image_url} alt="Imagem do artigo" />
|
||||
)}
|
||||
<article className="space-y-5">
|
||||
{article.body.split(/\n\s*\n/).map((p, i) => (
|
||||
<p className="whitespace-pre-wrap" key={i}>
|
||||
{p}
|
||||
</p>
|
||||
))}
|
||||
</article>
|
||||
<Link href={article.original_url} className="underline">
|
||||
Consultar documento original
|
||||
</Link>
|
||||
<EditorialReading article={article} />
|
||||
<p>
|
||||
<Link href="/documentos" className="underline">
|
||||
Voltar à biblioteca
|
||||
<Link
|
||||
href={
|
||||
article.private_preview
|
||||
? `/management/portal-services/resources/${article.id}/article`
|
||||
: '/documentos'
|
||||
}
|
||||
className="underline"
|
||||
>
|
||||
{article.private_preview
|
||||
? 'Voltar ao editor'
|
||||
: 'Voltar à biblioteca'}
|
||||
</Link>
|
||||
</p>
|
||||
</main>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { router, useForm } from '@inertiajs/react';
|
||||
import { useState } from 'react';
|
||||
import { router, useForm, usePage } from '@inertiajs/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Management, Field, control, button } from '../management/shared';
|
||||
|
||||
type DocumentFile = {
|
||||
@@ -164,7 +164,11 @@ export default function Manage({
|
||||
legacyContentEnabled = false,
|
||||
documentAccessOnly = false,
|
||||
editorialScopeOnly = false,
|
||||
managementFilters = {},
|
||||
associationOptions = {},
|
||||
}: {
|
||||
managementFilters?: {q?: string; state?: string; intent?: string};
|
||||
associationOptions?: Record<string, {value: string | number; label: string}[]>;
|
||||
legacyContentEnabled?: boolean;
|
||||
documentAccessOnly?: boolean;
|
||||
editorialScopeOnly?: boolean;
|
||||
@@ -206,12 +210,26 @@ export default function Manage({
|
||||
}) {
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [actionError, setActionError] = useState('');
|
||||
const [query, setQuery] = useState(managementFilters.q ?? '');
|
||||
const [stateFilter, setStateFilter] = useState(managementFilters.state ?? '');
|
||||
const { flash } = usePage<{flash?: {uploaded_document_id?: string}}>().props;
|
||||
const upload = useForm<{
|
||||
file: File | null;
|
||||
original_source: string;
|
||||
issuer: string;
|
||||
}>({ file: null, original_source: '', issuer: '' });
|
||||
const form = useForm<ResourceForm>(emptyResource());
|
||||
intent: string;
|
||||
}>({ file: null, original_source: '', issuer: '', intent: managementFilters.intent ?? 'document' });
|
||||
const form = useForm<ResourceForm>({...emptyResource(), ...(managementFilters.intent === 'legislation' ? {document_type: 'legislation', legislation: true} : {})});
|
||||
const chooseDocument = (id: string) => {
|
||||
const document = documents.find((item) => item.id === id);
|
||||
form.setData((current) => ({...current, document_id: id,
|
||||
issuer: current.issuer || document?.issuer || '',
|
||||
title: current.title || document?.file_name || '',
|
||||
}));
|
||||
};
|
||||
useEffect(() => {
|
||||
if (flash?.uploaded_document_id && !editingId && !form.data.document_id) chooseDocument(flash.uploaded_document_id);
|
||||
}, [flash?.uploaded_document_id]);
|
||||
const setAssociation = (
|
||||
index: number,
|
||||
key: keyof Association,
|
||||
@@ -361,6 +379,7 @@ export default function Manage({
|
||||
))}
|
||||
</section>
|
||||
<form
|
||||
id="receber"
|
||||
className="space-y-4 rounded border p-5"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -420,6 +439,7 @@ export default function Manage({
|
||||
</form>
|
||||
|
||||
{!documentAccessOnly && <>
|
||||
{managementFilters.intent === 'legislation' && <p className="rounded border p-4">Nova legislação: reutilize o original, complete diploma/tipo, entidade emitente, número, datas e âmbito. Registe alterações e revogações nas associações. Publicar para consulta não activa cálculos no simulador.</p>}
|
||||
<form
|
||||
className="space-y-4 rounded border p-5"
|
||||
onSubmit={(e) => {
|
||||
@@ -458,9 +478,7 @@ export default function Manage({
|
||||
required
|
||||
className={control}
|
||||
value={form.data.document_id}
|
||||
onChange={(e) =>
|
||||
form.setData('document_id', e.target.value)
|
||||
}
|
||||
onChange={(e) => chooseDocument(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
Seleccionar ficheiro ou registo existente
|
||||
@@ -925,6 +943,8 @@ export default function Manage({
|
||||
'document',
|
||||
'investment_type',
|
||||
'legal_source',
|
||||
'sector',
|
||||
'opportunity',
|
||||
'requirement',
|
||||
]
|
||||
.filter(
|
||||
@@ -934,8 +954,8 @@ export default function Manage({
|
||||
requirementAssociationsEnabled,
|
||||
)
|
||||
.map((v) => (
|
||||
<option key={v}>
|
||||
{v}
|
||||
<option key={v} value={v}>
|
||||
{({theme:'Tema', module:'Módulo',activity:'Actividade',stage:'Etapa',procedure:'Procedimento',entity:'Entidade',territory:'Território',document:'Documento / diploma',investment_type:'Modalidade de investimento',legal_source:'Fonte legal',requirement:'Requisito',sector:'Sector',opportunity:'Oportunidade'} as Record<string,string>)[v] ?? v}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
@@ -974,6 +994,12 @@ export default function Manage({
|
||||
),
|
||||
)}
|
||||
</select>
|
||||
) : associationOptions[association.target_type] ? (
|
||||
<select className={control} required value={association.target_key} onChange={(e) => setAssociation(index, 'target_key', e.target.value)}>
|
||||
<option value="">Seleccione um registo existente</option>
|
||||
{association.target_key && !associationOptions[association.target_type].some((o) => String(o.value) === association.target_key) && <option value={association.target_key}>Referência histórica guardada</option>}
|
||||
{associationOptions[association.target_type].map((o) => <option key={o.value} value={o.value}>{o.label}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
required
|
||||
@@ -1221,13 +1247,20 @@ export default function Manage({
|
||||
</section>
|
||||
)}
|
||||
|
||||
<h2 className="text-xl font-bold">
|
||||
<h2 id="conteudos" className="text-xl font-bold">
|
||||
Registos, pendências e duplicados por rever
|
||||
</h2>
|
||||
<form className="flex flex-wrap gap-3 rounded border p-4" aria-label="Pesquisar conteúdos" onSubmit={(e) => {e.preventDefault(); router.get('/management/portal-services', {q:query,state:stateFilter}, {preserveState:true,preserveScroll:true});}}>
|
||||
<Field label="Pesquisar título, ficheiro, número ou entidade"><input className={control} value={query} onChange={(e) => setQuery(e.target.value)} /></Field>
|
||||
<Field label="Estado do conteúdo"><select className={control} value={stateFilter} onChange={(e) => setStateFilter(e.target.value)}><option value="">Todos</option><option value="draft">Rascunhos</option><option value="published">Publicados</option><option value="withdrawn">Retirados</option></select></Field>
|
||||
<button className={button}>Pesquisar conteúdos</button>
|
||||
</form>
|
||||
<p>Mostram-se até 200 registos por pesquisa. Refine o título ou o estado para localizar conteúdos anteriores.</p>
|
||||
{!resources.length && <p role="status">Sem conteúdos correspondentes no seu âmbito de acesso.</p>}
|
||||
{resources.map((r) => (
|
||||
<article key={r.id} className="space-y-3 rounded border p-4">
|
||||
<h3 className="font-bold">
|
||||
{r.title} · {r.published ? 'Publicado' : 'Rascunho'}
|
||||
{r.title} · {r.published ? 'Publicado' : r.revisions.some((v) => ['unpublished', 'withdrawn'].includes(v.change_type)) ? 'Retirado' : 'Rascunho'}
|
||||
</h3>
|
||||
<p>
|
||||
Ficheiro: {r.file_name} · {r.file_status} ·{' '}
|
||||
@@ -1352,6 +1385,7 @@ export default function Manage({
|
||||
>
|
||||
Editar artigo
|
||||
</a>
|
||||
<a className={button} href={`/management/portal-services/resources/${r.id}/article/preview`}>Ler / pré-visualizar artigo</a>
|
||||
<button
|
||||
type="button"
|
||||
className={button}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Link, useForm } from '@inertiajs/react';
|
||||
import { Link, useForm, usePage } from '@inertiajs/react';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import { catalogueDetailHref } from '@/lib/catalogue-context';
|
||||
import { Management } from '../management/shared';
|
||||
import type { Paginated } from '../management/shared';
|
||||
@@ -35,6 +36,7 @@ export default function Index({
|
||||
locality_id: filters.locality_id ?? '',
|
||||
});
|
||||
const base = management ? '/management/portfolio' : '/carteira';
|
||||
const { url } = usePage();
|
||||
const Frame = management ? Management : Layout;
|
||||
return (
|
||||
<Frame title="Corredores, clusters e projectos âncora">
|
||||
@@ -151,7 +153,7 @@ export default function Index({
|
||||
(r.corridor_id || r.cluster_id || r.project_id) && (
|
||||
<Link
|
||||
className="ml-4 underline"
|
||||
href={`/oportunidades?${new URLSearchParams(r.corridor_id ? { corridor_id: r.corridor_id } : r.cluster_id ? { cluster_id: r.cluster_id } : { project_id: r.project_id! })}`}
|
||||
href={searchTerritoryHref(`/oportunidades?${new URLSearchParams(r.corridor_id ? { corridor_id: r.corridor_id } : r.cluster_id ? { cluster_id: r.cluster_id } : { project_id: r.project_id! })}`, url)}
|
||||
>
|
||||
Ver oportunidades associadas
|
||||
</Link>
|
||||
@@ -174,10 +176,10 @@ export default function Index({
|
||||
aria-label="Alternativas de exploração"
|
||||
className="flex flex-wrap gap-4"
|
||||
>
|
||||
<Link className="underline" href="/oportunidades">
|
||||
<Link className="underline" href={searchTerritoryHref('/oportunidades', url)}>
|
||||
Ver oportunidades publicadas
|
||||
</Link>
|
||||
<Link className="underline" href="/zonas">
|
||||
<Link className="underline" href={searchTerritoryHref('/zonas', url)}>
|
||||
Consultar ZEE, ZFI e parques
|
||||
</Link>
|
||||
<Link className="underline" href="/Simulador">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Link, useForm, router, usePage } from '@inertiajs/react';
|
||||
import { useState } from 'react';
|
||||
import { catalogueReturnHref } from '@/lib/catalogue-context';
|
||||
import { searchTerritoryHref } from '@/lib/search-territory';
|
||||
import InvestmentMapPanel from '@/components/investment-map-panel';
|
||||
import StructureGeographyPanel, {
|
||||
type StructureGeography,
|
||||
@@ -190,7 +191,7 @@ function Editor({
|
||||
</h2>
|
||||
{zones.map((z) => (
|
||||
<p key={z.id}>
|
||||
<Link className="underline" href={`/zonas/${z.id}`}>
|
||||
<Link className="underline" href={searchTerritoryHref(`/zonas/${z.id}`, pageUrl)}>
|
||||
{z.name}
|
||||
</Link>{' '}
|
||||
· {z.association_type} · Fonte: {z.source_reference}
|
||||
@@ -297,7 +298,7 @@ function Editor({
|
||||
<article key={opportunity.id}>
|
||||
<Link
|
||||
className="font-semibold underline"
|
||||
href={`/oportunidades/${opportunity.slug}?from_dossier=${record.id}`}
|
||||
href={searchTerritoryHref(`/oportunidades/${opportunity.slug}?from_dossier=${record.id}`, pageUrl)}
|
||||
>
|
||||
{opportunity.title}
|
||||
</Link>
|
||||
@@ -485,7 +486,7 @@ function Editor({
|
||||
{kind(r)}:{' '}
|
||||
<Link
|
||||
className="underline"
|
||||
href={`${base}/${r.id}${management ? '' : `?from=${record.id}`}`}
|
||||
href={searchTerritoryHref(`${base}/${r.id}${management ? '' : `?from=${record.id}`}`, pageUrl)}
|
||||
>
|
||||
{r.title}
|
||||
</Link>
|
||||
|
||||
@@ -0,0 +1,832 @@
|
||||
import { Link, router, useForm } from '@inertiajs/react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Frame, card, button } from '@/pages/investment/shared';
|
||||
import {
|
||||
Field,
|
||||
control,
|
||||
labels,
|
||||
acts,
|
||||
NoticeContent,
|
||||
type Draft,
|
||||
type Lot,
|
||||
type Attachment,
|
||||
type Payload,
|
||||
} from './shared';
|
||||
type Choice = {
|
||||
id: string | number;
|
||||
name?: string;
|
||||
title?: string;
|
||||
location_type?: string;
|
||||
published?: boolean;
|
||||
malware_scan_status?: string;
|
||||
};
|
||||
type References = {
|
||||
regimes: Record<string, string>;
|
||||
acts: Record<string, string>;
|
||||
organizations: Choice[];
|
||||
sectors: Choice[];
|
||||
territories: Choice[];
|
||||
opportunities: Choice[];
|
||||
projects: Choice[];
|
||||
documents: Choice[];
|
||||
};
|
||||
type RecordData = {
|
||||
id: string;
|
||||
version: number;
|
||||
draft: Draft;
|
||||
published: boolean;
|
||||
};
|
||||
type Plan = {
|
||||
id: string;
|
||||
version: number;
|
||||
act: string;
|
||||
payload: Payload;
|
||||
hash: string;
|
||||
};
|
||||
const dates = [
|
||||
'origin_published_at',
|
||||
'clarifications_at',
|
||||
'deadline',
|
||||
'opening_at',
|
||||
];
|
||||
const groups: [string, string[]][] = [
|
||||
[
|
||||
'Identificação e objecto',
|
||||
['title', 'reference', 'modality', 'object', 'quantities'],
|
||||
],
|
||||
[
|
||||
'Regime e financiamento',
|
||||
[
|
||||
'financier',
|
||||
'instrument',
|
||||
'edition',
|
||||
'rule_source',
|
||||
'financing_agreement',
|
||||
],
|
||||
],
|
||||
['Participação', ['eligibility', 'requirements', 'proposal_validity']],
|
||||
[
|
||||
'Calendário e entrega',
|
||||
[
|
||||
'origin_published_at',
|
||||
'clarifications_at',
|
||||
'deadline',
|
||||
'opening_at',
|
||||
'delivery_channel',
|
||||
'delivery_address',
|
||||
'delivery_url',
|
||||
],
|
||||
],
|
||||
['Divulgação e contacto', ['origin_url', 'dissemination']],
|
||||
];
|
||||
export default function Edit({
|
||||
record,
|
||||
references,
|
||||
history,
|
||||
}: {
|
||||
record: RecordData | null;
|
||||
references: References;
|
||||
history: {
|
||||
version: number;
|
||||
act: string;
|
||||
reason: string;
|
||||
actor_id: number;
|
||||
published_at: string | null;
|
||||
created_at: string;
|
||||
}[];
|
||||
}) {
|
||||
const empty: Draft = {
|
||||
timezone: 'Africa/Maputo',
|
||||
lots: [],
|
||||
attachments: [],
|
||||
contact_authorized: false,
|
||||
};
|
||||
const form = useForm({
|
||||
version: record?.version ?? 0,
|
||||
reason: '',
|
||||
draft: record?.draft ?? empty,
|
||||
});
|
||||
const [act, setAct] = useState(
|
||||
record?.published ? 'clarification' : 'initial',
|
||||
);
|
||||
const [preview, setPreview] = useState<Plan | null>(null);
|
||||
const [message, setMessage] = useState('');
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [saved, setSaved] = useState(true);
|
||||
useEffect(() => {
|
||||
form.setData({
|
||||
version: record?.version ?? 0,
|
||||
reason: '',
|
||||
draft: record?.draft ?? empty,
|
||||
});
|
||||
setPreview(null);
|
||||
setSaved(true);
|
||||
}, [record?.id, record?.version]); // A server version is the recovery boundary.
|
||||
const change = (key: string, value: Draft[string]) => {
|
||||
form.setData('draft', { ...form.data.draft, [key]: value });
|
||||
setPreview(null);
|
||||
setSaved(false);
|
||||
};
|
||||
const value = (key: string) => String(form.data.draft[key] ?? '');
|
||||
const select = (key: string, label: string, choices: Choice[]) => (
|
||||
<Field label={label}>
|
||||
<select
|
||||
className={control}
|
||||
value={value(key)}
|
||||
onChange={(e) =>
|
||||
change(
|
||||
key,
|
||||
e.target.value
|
||||
? key === 'sector_id' || key === 'opportunity_id'
|
||||
? Number(e.target.value)
|
||||
: e.target.value
|
||||
: null,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">Por indicar</option>
|
||||
{choices.map((o) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
{o.name ?? o.title}
|
||||
{o.location_type ? ` · ${o.location_type}` : ''}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
);
|
||||
const input = (key: string, label?: string) => (
|
||||
<Field key={key} label={label ?? labels[key] ?? key}>
|
||||
{dates.includes(key) ? (
|
||||
<input
|
||||
type="datetime-local"
|
||||
className={control}
|
||||
value={value(key)}
|
||||
onChange={(e) => change(key, e.target.value)}
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
className={control}
|
||||
rows={key === 'object' ? 4 : 2}
|
||||
value={value(key)}
|
||||
onChange={(e) => change(key, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
</Field>
|
||||
);
|
||||
const lots = (form.data.draft.lots ?? []) as Lot[];
|
||||
const attachments = (form.data.draft.attachments ?? []) as Attachment[];
|
||||
const makePreview = async () => {
|
||||
setBusy(true);
|
||||
setMessage('');
|
||||
try {
|
||||
const token = document.cookie
|
||||
.split('; ')
|
||||
.find((v) => v.startsWith('XSRF-TOKEN='))
|
||||
?.slice(11);
|
||||
const response = await fetch(
|
||||
`/management/concursos/${record!.id}/previa`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'X-XSRF-TOKEN': decodeURIComponent(token ?? ''),
|
||||
},
|
||||
body: JSON.stringify({ act }),
|
||||
},
|
||||
);
|
||||
const body = await response.json();
|
||||
if (!response.ok)
|
||||
throw new Error(
|
||||
Object.values(body.errors ?? {})
|
||||
.flat()
|
||||
.join(' ') ||
|
||||
body.message ||
|
||||
'Não foi possível preparar a prévia.',
|
||||
);
|
||||
setPreview(body);
|
||||
} catch (error) {
|
||||
setMessage(
|
||||
error instanceof Error ? error.message : 'Erro de ligação.',
|
||||
);
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Frame
|
||||
title={record ? 'Editar concurso/anúncio' : 'Novo concurso/anúncio'}
|
||||
>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link className="underline" href="/management/concursos">
|
||||
Recuperar concursos
|
||||
</Link>
|
||||
<Link className="underline" href="/gestao-conteudos">
|
||||
Entrada única de gestão
|
||||
</Link>
|
||||
<a
|
||||
className="underline"
|
||||
href="/management/portal-services?intent=document#receber"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Carregar / gerir documentos e scan na biblioteca (outra
|
||||
página)
|
||||
</a>
|
||||
<button
|
||||
className="underline"
|
||||
type="button"
|
||||
onClick={() => router.reload({ only: ['references'] })}
|
||||
>
|
||||
Actualizar opções da biblioteca
|
||||
</button>
|
||||
</div>
|
||||
<p>
|
||||
Os campos destinam-se à divulgação após revisão da prévia,
|
||||
excepto as notas internas, os anexos não públicos e o contacto
|
||||
sem autorização. O vínculo a um cadastro privado não publica os
|
||||
seus dados.
|
||||
</p>
|
||||
<p>
|
||||
O rascunho pode ficar incompleto. Alterações guardadas não
|
||||
substituem o acto público; reveja a prévia e publique
|
||||
explicitamente. Publicar concurso não publica a oportunidade nem
|
||||
recebe propostas.
|
||||
</p>
|
||||
<p role="status">
|
||||
{record
|
||||
? `Versão guardada ${record.version}. ${saved ? 'Preparação recuperada do servidor.' : 'Há alterações por guardar.'}`
|
||||
: 'Preparação ainda não guardada.'}
|
||||
</p>
|
||||
{!!Object.keys(form.errors).length && (
|
||||
<div role="alert">
|
||||
{Object.values(form.errors).map((error, i) => (
|
||||
<p key={i}>{error}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<form
|
||||
className="space-y-5"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
setPreview(null);
|
||||
record
|
||||
? form.put(`/management/concursos/${record.id}`)
|
||||
: form.post('/management/concursos');
|
||||
}}
|
||||
>
|
||||
<section className={`${card} grid gap-3 md:grid-cols-2`}>
|
||||
{select(
|
||||
'organization_id',
|
||||
'Entidade contratante existente',
|
||||
references.organizations,
|
||||
)}
|
||||
{select(
|
||||
'opportunity_id',
|
||||
'Oportunidade relacionada existente',
|
||||
references.opportunities,
|
||||
)}
|
||||
{select(
|
||||
'project_id',
|
||||
'Projecto existente',
|
||||
references.projects,
|
||||
)}
|
||||
{select('sector_id', 'Sector', references.sectors)}
|
||||
{select(
|
||||
'territory_id',
|
||||
'Localização existente',
|
||||
references.territories,
|
||||
)}
|
||||
<p>
|
||||
Os vínculos reutilizam os cadastros; não criam nova
|
||||
entidade, oportunidade, projecto ou limite. Referências
|
||||
privadas não são transformadas em conteúdo público.
|
||||
</p>
|
||||
</section>
|
||||
<section className={card}>
|
||||
<h2 className="text-xl font-semibold">
|
||||
Regime do procedimento
|
||||
</h2>
|
||||
<Field label="Regime">
|
||||
<select
|
||||
className={control}
|
||||
value={value('regime')}
|
||||
onChange={(e) => change('regime', e.target.value)}
|
||||
>
|
||||
<option value="">Por indicar</option>
|
||||
{Object.entries(references.regimes).map(
|
||||
([v, l]) => (
|
||||
<option key={v} value={v}>
|
||||
{l}
|
||||
</option>
|
||||
),
|
||||
)}
|
||||
</select>
|
||||
</Field>
|
||||
{value('regime') === 'state' ? (
|
||||
<>
|
||||
<p>
|
||||
Contratação do Estado: identifique a modalidade,
|
||||
o instrumento e a edição aplicáveis.{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href="https://www.ufsa.gov.mz/divnovoreg.php"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
UFSA — Decreto n.º 79/2022
|
||||
</a>
|
||||
. A referência não atribui automaticamente
|
||||
requisitos ou prazos a todos os procedimentos.
|
||||
</p>
|
||||
{input(
|
||||
'responsible_unit',
|
||||
'UGEA / unidade responsável quando aplicável',
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
{value('regime') === 'external' ? (
|
||||
<>
|
||||
<a
|
||||
className="underline"
|
||||
href="https://www.worldbank.org/ext/en/what-we-do/project-procurement/framework"
|
||||
>
|
||||
Quadro e documentos normalizados do
|
||||
Banco Mundial
|
||||
</a>{' '}
|
||||
são referências; indique a edição e o
|
||||
acordo efectivamente aplicáveis.
|
||||
</>
|
||||
) : (
|
||||
'Use as regras documentadas da entidade e deste procedimento.'
|
||||
)}{' '}
|
||||
UGEA, Cadastro Único, garantias e abertura
|
||||
pública não são impostos a este regime.
|
||||
</p>
|
||||
{input(
|
||||
'responsible_unit',
|
||||
'Unidade responsável (quando aplicável)',
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Field label="Obras, bens ou serviços">
|
||||
<select
|
||||
className={control}
|
||||
value={value('object_kind')}
|
||||
onChange={(e) =>
|
||||
change('object_kind', e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="">Por indicar</option>
|
||||
{Object.entries({
|
||||
works: 'Obras',
|
||||
goods: 'Bens',
|
||||
services: 'Serviços',
|
||||
mixed: 'Misto',
|
||||
other: 'Outro',
|
||||
}).map(([v, l]) => (
|
||||
<option key={v} value={v}>
|
||||
{l}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
</section>
|
||||
{groups.map(([title, fields]) => (
|
||||
<section className={card} key={title}>
|
||||
<h2 className="mb-3 text-xl font-semibold">{title}</h2>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{fields.map((key) =>
|
||||
input(
|
||||
key,
|
||||
key === 'title'
|
||||
? 'Título do concurso/anúncio'
|
||||
: undefined,
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
<section className={`${card} space-y-3`}>
|
||||
<h2 className="text-xl font-semibold">
|
||||
Garantias e prazo mínimo documentado
|
||||
</h2>
|
||||
<Field label="Garantias exigidas neste procedimento">
|
||||
<select
|
||||
className={control}
|
||||
value={value('guarantee_status')}
|
||||
onChange={(e) =>
|
||||
change('guarantee_status', e.target.value)
|
||||
}
|
||||
>
|
||||
<option value="unknown">
|
||||
Por determinar nos documentos
|
||||
</option>
|
||||
<option value="required">
|
||||
Exigidas nos documentos
|
||||
</option>
|
||||
<option value="not_required">Não exigidas</option>
|
||||
</select>
|
||||
</Field>
|
||||
{value('guarantee_status') === 'required' &&
|
||||
input('guarantees')}
|
||||
<Field label="Fuso horário IANA do procedimento">
|
||||
<input
|
||||
className={control}
|
||||
value={value('timezone')}
|
||||
onChange={(e) => change('timezone', e.target.value)}
|
||||
placeholder="Africa/Maputo"
|
||||
/>
|
||||
</Field>
|
||||
<p>
|
||||
As datas e horas introduzidas pertencem a este fuso.
|
||||
Publicação no InvesteMoz é registada separadamente pelo
|
||||
servidor; não reinicia o prazo.
|
||||
</p>
|
||||
<Field label="Prazo mínimo documentado em dias (opcional)">
|
||||
<input
|
||||
className={control}
|
||||
type="number"
|
||||
min="0"
|
||||
value={value('minimum_days')}
|
||||
onChange={(e) =>
|
||||
change(
|
||||
'minimum_days',
|
||||
e.target.value === ''
|
||||
? null
|
||||
: Number(e.target.value),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
{form.data.draft.minimum_days !== null &&
|
||||
form.data.draft.minimum_days !== undefined && (
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{input('minimum_source')}
|
||||
{input('minimum_edition')}
|
||||
{input('minimum_modality')}
|
||||
<Field label="Contagem documentada">
|
||||
<select
|
||||
className={control}
|
||||
value={value('minimum_day_basis')}
|
||||
onChange={(e) =>
|
||||
change(
|
||||
'minimum_day_basis',
|
||||
e.target.value,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">Por indicar</option>
|
||||
<option value="calendar">
|
||||
Dias corridos
|
||||
</option>
|
||||
<option value="business">
|
||||
Dias úteis — calendário documentado
|
||||
</option>
|
||||
</select>
|
||||
</Field>
|
||||
{value('minimum_day_basis') === 'business' && (
|
||||
<>
|
||||
{input(
|
||||
'minimum_calendar_source',
|
||||
'Fonte do calendário aplicável e abrangência de feriados',
|
||||
)}
|
||||
{input(
|
||||
'minimum_working_days',
|
||||
'Dias de trabalho documentados: 1=segunda a 7=domingo, separados por vírgula',
|
||||
)}
|
||||
{input(
|
||||
'minimum_holidays',
|
||||
'Feriados / exclusões documentados (AAAA-MM-DD, separados por espaço ou vírgula)',
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<p>
|
||||
Sem prazo parametrizado não é presumido um mínimo geral.
|
||||
A contagem em dias úteis usa exclusivamente os dias de
|
||||
trabalho e exclusões do calendário documentado neste
|
||||
registo; não adopta um calendário nacional por
|
||||
suposição.
|
||||
</p>
|
||||
</section>
|
||||
<section className={`${card} space-y-3`}>
|
||||
<h2 className="text-xl font-semibold">Lotes</h2>
|
||||
{lots.map((lot, i) => (
|
||||
<fieldset
|
||||
className="grid gap-3 rounded border p-3 md:grid-cols-3"
|
||||
key={i}
|
||||
>
|
||||
<legend>Lote {i + 1}</legend>
|
||||
{(
|
||||
['code', 'description', 'quantities'] as const
|
||||
).map((key) => (
|
||||
<Field
|
||||
key={key}
|
||||
label={
|
||||
{
|
||||
code: 'Referência do lote',
|
||||
description: 'Objecto do lote',
|
||||
quantities: 'Quantidades do lote',
|
||||
}[key]
|
||||
}
|
||||
>
|
||||
<textarea
|
||||
className={control}
|
||||
value={lot[key]}
|
||||
onChange={(e) =>
|
||||
change(
|
||||
'lots',
|
||||
lots.map((r, n) =>
|
||||
n === i
|
||||
? {
|
||||
...r,
|
||||
[key]: e.target
|
||||
.value,
|
||||
}
|
||||
: r,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className="underline"
|
||||
onClick={() =>
|
||||
change(
|
||||
'lots',
|
||||
lots.filter((_, n) => n !== i),
|
||||
)
|
||||
}
|
||||
>
|
||||
Retirar lote do rascunho
|
||||
</button>
|
||||
</fieldset>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={button}
|
||||
onClick={() =>
|
||||
change('lots', [
|
||||
...lots,
|
||||
{ code: '', description: '', quantities: '' },
|
||||
])
|
||||
}
|
||||
>
|
||||
Acrescentar lote
|
||||
</button>
|
||||
</section>
|
||||
<section className={`${card} space-y-3`}>
|
||||
<h2 className="text-xl font-semibold">
|
||||
Documentos e anexos
|
||||
</h2>
|
||||
<p>
|
||||
Carregue e classifique os originais na biblioteca
|
||||
existente pelo link acima. Reutilize-os aqui: anúncio,
|
||||
caderno, termos de referência, formulários, anexos ou
|
||||
comprovativos. Publicar exige que cada anexo marcado
|
||||
público já esteja autorizado, publicado e com scan limpo
|
||||
na biblioteca. Os restantes ficam privados no cadastro.
|
||||
</p>
|
||||
{attachments.map((item, i) => (
|
||||
<fieldset
|
||||
key={i}
|
||||
className="space-y-2 rounded border p-3"
|
||||
>
|
||||
<legend>Anexo {i + 1}</legend>
|
||||
<Field label={`Documento do anexo ${i + 1}`}>
|
||||
<select
|
||||
className={control}
|
||||
value={item.resource_id || ''}
|
||||
onChange={(e) =>
|
||||
change(
|
||||
'attachments',
|
||||
attachments.map((r, n) =>
|
||||
n === i
|
||||
? {
|
||||
...r,
|
||||
resource_id: Number(
|
||||
e.target.value,
|
||||
),
|
||||
}
|
||||
: r,
|
||||
),
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">
|
||||
Seleccione da biblioteca
|
||||
</option>
|
||||
{references.documents.map((d) => (
|
||||
<option key={d.id} value={d.id}>
|
||||
{d.title} · {d.malware_scan_status}{' '}
|
||||
·{' '}
|
||||
{d.published
|
||||
? 'publicado'
|
||||
: 'privado / rascunho'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label={`Finalidade do anexo ${i + 1}`}>
|
||||
<input
|
||||
className={control}
|
||||
value={item.purpose}
|
||||
onChange={(e) =>
|
||||
change(
|
||||
'attachments',
|
||||
attachments.map((r, n) =>
|
||||
n === i
|
||||
? {
|
||||
...r,
|
||||
purpose:
|
||||
e.target.value,
|
||||
}
|
||||
: r,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={item.public}
|
||||
onChange={(e) =>
|
||||
change(
|
||||
'attachments',
|
||||
attachments.map((r, n) =>
|
||||
n === i
|
||||
? {
|
||||
...r,
|
||||
public: e.target
|
||||
.checked,
|
||||
}
|
||||
: r,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>{' '}
|
||||
Incluir este anexo na divulgação pública
|
||||
</label>
|
||||
<button
|
||||
type="button"
|
||||
className="ml-3 underline"
|
||||
onClick={() =>
|
||||
change(
|
||||
'attachments',
|
||||
attachments.filter((_, n) => n !== i),
|
||||
)
|
||||
}
|
||||
>
|
||||
Retirar anexo do rascunho
|
||||
</button>
|
||||
</fieldset>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={button}
|
||||
onClick={() =>
|
||||
change('attachments', [
|
||||
...attachments,
|
||||
{ resource_id: 0, purpose: '', public: false },
|
||||
])
|
||||
}
|
||||
>
|
||||
Reutilizar documento
|
||||
</button>
|
||||
</section>
|
||||
<section className={`${card} space-y-3`}>
|
||||
<h2 className="text-xl font-semibold">
|
||||
Contacto e acto a divulgar
|
||||
</h2>
|
||||
{input('public_contact')}
|
||||
<label className="block">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(
|
||||
form.data.draft.contact_authorized,
|
||||
)}
|
||||
onChange={(e) =>
|
||||
change('contact_authorized', e.target.checked)
|
||||
}
|
||||
/>{' '}
|
||||
Contacto autorizado para divulgação pública
|
||||
</label>
|
||||
{input('result')}
|
||||
{input(
|
||||
'public_reason',
|
||||
'Motivo público do acto — aparece na prévia e no histórico público',
|
||||
)}
|
||||
{input(
|
||||
'internal_notes',
|
||||
'Notas internas — nunca incluídas na divulgação',
|
||||
)}
|
||||
<Field label="Motivo interno da gravação (20–1000 caracteres)">
|
||||
<textarea
|
||||
className={control}
|
||||
value={form.data.reason}
|
||||
onChange={(e) =>
|
||||
form.setData('reason', e.target.value)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<button className={button} disabled={form.processing}>
|
||||
Guardar rascunho
|
||||
</button>
|
||||
</section>
|
||||
</form>
|
||||
{record && (
|
||||
<section className={`${card} my-5 space-y-3`}>
|
||||
<h2 className="text-xl font-semibold">
|
||||
Pré-visualizar e publicar
|
||||
</h2>
|
||||
<Field label="Acto a publicar">
|
||||
<select
|
||||
className={control}
|
||||
value={act}
|
||||
onChange={(e) => {
|
||||
setAct(e.target.value);
|
||||
setPreview(null);
|
||||
}}
|
||||
>
|
||||
{Object.entries(references.acts).map(([v, l]) => (
|
||||
<option key={v} value={v}>
|
||||
{l}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<button
|
||||
className={button}
|
||||
disabled={!saved || busy}
|
||||
onClick={makePreview}
|
||||
>
|
||||
Preparar prévia pública
|
||||
</button>
|
||||
<p>
|
||||
Guarde antes da prévia. Uma prorrogação identifica o
|
||||
prazo anterior e o novo; o fim do prazo não é
|
||||
adjudicação.
|
||||
</p>
|
||||
{message && <p role="alert">{message}</p>}
|
||||
{preview && (
|
||||
<div className="space-y-3 rounded border p-4">
|
||||
<h3 className="text-xl font-semibold">
|
||||
Prévia do conteúdo público — {acts[preview.act]}
|
||||
</h3>
|
||||
<p>
|
||||
Motivo público:{' '}
|
||||
{String(preview.payload.public_reason)}
|
||||
</p>
|
||||
<NoticeContent p={preview.payload} />
|
||||
<button
|
||||
className={button}
|
||||
onClick={() =>
|
||||
router.post(
|
||||
`/management/concursos/${record.id}/publicar`,
|
||||
{
|
||||
act: preview.act,
|
||||
hash: preview.hash,
|
||||
reason: String(
|
||||
preview.payload.public_reason,
|
||||
),
|
||||
},
|
||||
{ onSuccess: () => setPreview(null) },
|
||||
)
|
||||
}
|
||||
>
|
||||
Publicar este acto
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{record.published && (
|
||||
<Link
|
||||
className="block underline"
|
||||
href={`/concursos/${record.id}`}
|
||||
>
|
||||
Consultar anúncio público
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
<section className={card}>
|
||||
<h2 className="text-xl font-semibold">
|
||||
Histórico de preparação e publicação
|
||||
</h2>
|
||||
{history.map((v) => (
|
||||
<p key={v.version}>
|
||||
Versão {v.version} ·{' '}
|
||||
{v.act === 'draft' ? 'Rascunho' : acts[v.act]} ·
|
||||
responsável {v.actor_id} ·{' '}
|
||||
{v.published_at ?? v.created_at} · {v.reason}
|
||||
</p>
|
||||
))}
|
||||
</section>
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
import { Link, useForm } from '@inertiajs/react';
|
||||
import { Frame, card, button } from '@/pages/investment/shared';
|
||||
import { Field, control, states, type Publication } from './shared';
|
||||
type Choice = { id: string; name: string };
|
||||
export default function Index({
|
||||
items,
|
||||
filters,
|
||||
options,
|
||||
regimes,
|
||||
management,
|
||||
}: {
|
||||
items: {
|
||||
data: { id: string; publication: Publication; state: string }[];
|
||||
current_page: number;
|
||||
links: { url: string | null; label: string; active: boolean }[];
|
||||
};
|
||||
filters: Record<string, string>;
|
||||
options: Record<string, Choice[]>;
|
||||
regimes: Record<string, string>;
|
||||
management: boolean;
|
||||
}) {
|
||||
const form = useForm({
|
||||
q: filters.q ?? '',
|
||||
entity: filters.entity ?? '',
|
||||
sector: filters.sector ?? '',
|
||||
territory: filters.territory ?? '',
|
||||
modality: filters.modality ?? '',
|
||||
financier: filters.financier ?? '',
|
||||
state: filters.state ?? '',
|
||||
deadline_before: filters.deadline_before ?? '',
|
||||
regime: filters.regime ?? '',
|
||||
});
|
||||
const query = new URLSearchParams(
|
||||
Object.entries({ ...filters, page: String(items.current_page) }).filter(
|
||||
([, v]) => v != null && v !== '',
|
||||
),
|
||||
).toString();
|
||||
return (
|
||||
<Frame title="Concursos e anúncios">
|
||||
<p>
|
||||
Procedimentos e actos divulgados pelas entidades responsáveis.
|
||||
Uma oportunidade divulgada não é, por si, um concurso aberto.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link className="underline" href="/concursos/acompanhar">
|
||||
Os concursos que acompanho
|
||||
</Link>
|
||||
{management && (
|
||||
<Link className={button} href="/management/concursos">
|
||||
Gerir concursos e anúncios
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
className={`${card} grid gap-3 md:grid-cols-3`}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
form.get('/concursos', { preserveState: true });
|
||||
}}
|
||||
>
|
||||
<Field label="Palavras-chave">
|
||||
<input
|
||||
className={control}
|
||||
value={form.data.q}
|
||||
onChange={(e) => form.setData('q', e.target.value)}
|
||||
/>
|
||||
</Field>
|
||||
{(
|
||||
[
|
||||
'entity',
|
||||
'sector',
|
||||
'territory',
|
||||
'modality',
|
||||
'financier',
|
||||
] as const
|
||||
).map((key) => (
|
||||
<Field
|
||||
key={key}
|
||||
label={
|
||||
{
|
||||
entity: 'Entidade',
|
||||
sector: 'Sector',
|
||||
territory: 'Localização',
|
||||
modality: 'Modalidade',
|
||||
financier: 'Financiador',
|
||||
}[key]
|
||||
}
|
||||
>
|
||||
<select
|
||||
className={control}
|
||||
value={form.data[key]}
|
||||
onChange={(e) => form.setData(key, e.target.value)}
|
||||
>
|
||||
<option value="">Todos</option>
|
||||
{(options[key] ?? []).map((o) => (
|
||||
<option key={o.id} value={o.id}>
|
||||
{o.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
))}
|
||||
<Field label="Regime">
|
||||
<select
|
||||
className={control}
|
||||
value={form.data.regime}
|
||||
onChange={(e) => form.setData('regime', e.target.value)}
|
||||
>
|
||||
<option value="">Todos</option>
|
||||
{Object.entries(regimes).map(([v, l]) => (
|
||||
<option key={v} value={v}>
|
||||
{l}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Estado">
|
||||
<select
|
||||
className={control}
|
||||
value={form.data.state}
|
||||
onChange={(e) => form.setData('state', e.target.value)}
|
||||
>
|
||||
<option value="">Todos</option>
|
||||
{Object.entries(states).map(([v, l]) => (
|
||||
<option key={v} value={v}>
|
||||
{l}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field label="Prazo até (data local do procedimento)">
|
||||
<input
|
||||
className={control}
|
||||
type="date"
|
||||
value={form.data.deadline_before}
|
||||
onChange={(e) =>
|
||||
form.setData('deadline_before', e.target.value)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<button className={button}>Pesquisar</button>
|
||||
</form>
|
||||
{!items.data.length && (
|
||||
<p role="status">Nenhum anúncio corresponde aos filtros.</p>
|
||||
)}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{items.data.map((item) => (
|
||||
<article key={item.id} className={card}>
|
||||
<h2 className="text-xl font-semibold">
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/concursos/${item.id}${query ? '?' + query : ''}`}
|
||||
>
|
||||
{String(item.publication.payload.title)}
|
||||
</Link>
|
||||
</h2>
|
||||
<p>
|
||||
{String(item.publication.payload.organization_name)}{' '}
|
||||
· {states[item.state]}
|
||||
</p>
|
||||
<p>
|
||||
Entrega:{' '}
|
||||
{String(item.publication.payload.deadline).replace(
|
||||
'T',
|
||||
' ',
|
||||
)}{' '}
|
||||
· {String(item.publication.payload.timezone)}
|
||||
</p>
|
||||
<p>{String(item.publication.payload.object)}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
<nav aria-label="Paginação">
|
||||
{items.links.map((link, i) =>
|
||||
link.url ? (
|
||||
<Link
|
||||
key={i}
|
||||
className={`mx-2 underline ${link.active ? 'font-bold' : ''}`}
|
||||
href={link.url}
|
||||
>
|
||||
{link.label
|
||||
.replace('«', '«')
|
||||
.replace('»', '»')}
|
||||
</Link>
|
||||
) : (
|
||||
<span key={i} className="mx-2">
|
||||
{link.label
|
||||
.replace('«', '«')
|
||||
.replace('»', '»')}
|
||||
</span>
|
||||
),
|
||||
)}
|
||||
</nav>
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { Frame, card, button } from '@/pages/investment/shared';
|
||||
import { states, type Publication } from './shared';
|
||||
export default function Manage({
|
||||
items,
|
||||
watched = false,
|
||||
}: {
|
||||
items: {
|
||||
id: string;
|
||||
title?: string;
|
||||
version?: number;
|
||||
published?: boolean;
|
||||
publication?: Publication;
|
||||
state?: string;
|
||||
changed?: boolean;
|
||||
}[];
|
||||
watched?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Frame
|
||||
title={
|
||||
watched
|
||||
? 'Concursos que acompanho'
|
||||
: 'Gestão de concursos e anúncios'
|
||||
}
|
||||
>
|
||||
<Link className="underline" href="/concursos">
|
||||
Catálogo público
|
||||
</Link>
|
||||
{!watched && (
|
||||
<>
|
||||
<Link className={button} href="/management/concursos/novo">
|
||||
Novo concurso/anúncio
|
||||
</Link>
|
||||
<Link className="ml-3 underline" href="/gestao-conteudos">
|
||||
Entrada única de gestão
|
||||
</Link>
|
||||
<p>
|
||||
Guardar → recuperar → pré-visualizar → publicar →
|
||||
actualizar/prorrogar → publicar resultado ou
|
||||
cancelamento. Carregue documentos na biblioteca
|
||||
existente, acompanhe o scan e autorize separadamente os
|
||||
anexos públicos. O rascunho pode ficar incompleto.
|
||||
</p>
|
||||
<p>
|
||||
Para actualizar, reabra o concurso, guarde as
|
||||
alterações, escolha o acto e confira a prévia. A data de
|
||||
publicação no portal não substitui a data de origem.
|
||||
Acompanhar não apresenta proposta.
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{!items.length && <p>Sem registos.</p>}
|
||||
{items.map((item) => (
|
||||
<article key={item.id} className={`${card} my-3`}>
|
||||
<h2 className="font-semibold">
|
||||
{item.title ?? String(item.publication?.payload.title)}
|
||||
</h2>
|
||||
{watched ? (
|
||||
<>
|
||||
<p>
|
||||
{states[item.state ?? '']} ·{' '}
|
||||
{item.changed
|
||||
? 'Há novos actos desde que começou a acompanhar'
|
||||
: 'Sem novos actos desde o registo de acompanhamento'}
|
||||
</p>
|
||||
<p>
|
||||
O acompanhamento é consultado nesta área; não
|
||||
afirma envio de alertas por email.
|
||||
</p>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/concursos/${item.id}`}
|
||||
>
|
||||
Consultar concurso
|
||||
</Link>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
Versão {item.version} ·{' '}
|
||||
{item.published
|
||||
? 'Com acto publicado; alterações em preparação são separadas'
|
||||
: 'Rascunho privado'}
|
||||
</p>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/management/concursos/${item.id}`}
|
||||
>
|
||||
Recuperar e editar
|
||||
</Link>
|
||||
{item.published && (
|
||||
<Link
|
||||
className="ml-3 underline"
|
||||
href={`/concursos/${item.id}`}
|
||||
>
|
||||
Consultar publicação
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
export type Lot = { code: string; description: string; quantities: string };
|
||||
export type Attachment = {
|
||||
resource_id: number;
|
||||
public: boolean;
|
||||
purpose: string;
|
||||
title?: string;
|
||||
available?: boolean;
|
||||
};
|
||||
export type Draft = {
|
||||
[key: string]: string | number | boolean | null | Lot[] | Attachment[];
|
||||
};
|
||||
export type Payload = Draft & {
|
||||
opportunity?: { title: string; url: string } | null;
|
||||
};
|
||||
export type Publication = {
|
||||
id: string;
|
||||
version: number;
|
||||
act: string;
|
||||
payload: Payload;
|
||||
published_at: string;
|
||||
reason: string;
|
||||
};
|
||||
export const states: Record<string, string> = {
|
||||
open: 'Aberto',
|
||||
extended: 'Prorrogado',
|
||||
deadline_ended: 'Prazo de propostas terminado',
|
||||
cancelled: 'Cancelado',
|
||||
results: 'Resultados publicados',
|
||||
};
|
||||
export const acts: Record<string, string> = {
|
||||
initial: 'Anúncio inicial',
|
||||
clarification: 'Esclarecimento',
|
||||
correction: 'Rectificação',
|
||||
extension: 'Prorrogação',
|
||||
award: 'Resultado / adjudicação',
|
||||
cancellation: 'Cancelamento',
|
||||
other: 'Outro acto',
|
||||
};
|
||||
export const labels: Record<string, string> = {
|
||||
reference: 'Referência',
|
||||
object: 'Objecto',
|
||||
responsible_unit: 'Unidade responsável / UGEA quando aplicável',
|
||||
modality: 'Modalidade',
|
||||
object_kind: 'Natureza do objecto',
|
||||
quantities: 'Quantidades essenciais',
|
||||
financier: 'Financiador',
|
||||
instrument: 'Instrumento aplicável',
|
||||
edition: 'Edição aplicável',
|
||||
rule_source: 'Fonte das regras',
|
||||
financing_agreement: 'Acordo de financiamento',
|
||||
eligibility: 'Elegibilidade',
|
||||
requirements: 'Requisitos de participação',
|
||||
proposal_validity: 'Validade da proposta',
|
||||
guarantee_status: 'Aplicabilidade de garantias',
|
||||
guarantees: 'Garantias documentadas',
|
||||
delivery_channel: 'Canal de entrega',
|
||||
delivery_address: 'Endereço / instruções para apresentar proposta',
|
||||
delivery_url: 'Plataforma de entrega',
|
||||
public_contact: 'Contacto público autorizado',
|
||||
dissemination: 'Meios de divulgação exigidos / adoptados e comprovativos',
|
||||
origin_url: 'Anúncio de origem',
|
||||
origin_published_at: 'Publicação de origem — referência do prazo',
|
||||
clarifications_at: 'Limite para esclarecimentos',
|
||||
deadline: 'Entrega de propostas',
|
||||
opening_at: 'Abertura (quando aplicável)',
|
||||
timezone: 'Fuso horário do procedimento',
|
||||
minimum_days: 'Prazo mínimo documentado (dias)',
|
||||
minimum_source: 'Fonte / citação do prazo mínimo',
|
||||
minimum_edition: 'Edição da regra de prazo',
|
||||
minimum_modality: 'Modalidade da regra de prazo',
|
||||
minimum_day_basis: 'Base de contagem do prazo',
|
||||
minimum_calendar_source: 'Fonte do calendário de dias úteis',
|
||||
minimum_working_days: 'Dias de trabalho (1=segunda, 7=domingo)',
|
||||
minimum_holidays: 'Feriados e exclusões documentados',
|
||||
result: 'Acto ou resultado documentado',
|
||||
};
|
||||
export const control =
|
||||
'w-full rounded border border-slate-400 bg-background p-2 text-foreground';
|
||||
export function Field({
|
||||
label,
|
||||
children,
|
||||
}: {
|
||||
label: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<label className="block space-y-1">
|
||||
<span className="font-medium">{label}</span>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
export function NoticeContent({ p }: { p: Payload }) {
|
||||
const links = ['rule_source', 'origin_url', 'delivery_url'];
|
||||
const names: Record<string, string> = {
|
||||
works: 'Obras',
|
||||
goods: 'Bens',
|
||||
services: 'Serviços',
|
||||
mixed: 'Misto',
|
||||
other: 'Outro',
|
||||
required: 'Exigidas',
|
||||
not_required: 'Não exigidas',
|
||||
unknown: 'Por determinar',
|
||||
calendar: 'Dias corridos',
|
||||
business: 'Dias úteis',
|
||||
};
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<h2 className="text-2xl font-semibold">{String(p.title ?? '')}</h2>
|
||||
<p>
|
||||
<strong>Entidade:</strong>{' '}
|
||||
{String(p.organization_name ?? 'Por indicar')}
|
||||
</p>
|
||||
<p>
|
||||
Regime:{' '}
|
||||
{(
|
||||
{
|
||||
state: 'Contratação pública do Estado',
|
||||
external: 'Financiamento externo',
|
||||
private: 'Concurso privado',
|
||||
other: 'Outro regime documentado',
|
||||
} as Record<string, string>
|
||||
)[String(p.regime)] ?? 'Por indicar'}
|
||||
</p>
|
||||
<section>
|
||||
<h3 className="text-lg font-semibold">
|
||||
Objecto e como participar
|
||||
</h3>
|
||||
<p className="whitespace-pre-wrap">{String(p.object ?? '')}</p>
|
||||
<p className="my-2 rounded border p-3">
|
||||
<strong>Entrega:</strong>{' '}
|
||||
{String(p.deadline ?? 'Por indicar').replace('T', ' ')} ·{' '}
|
||||
{String(p.timezone ?? '')}
|
||||
<br />
|
||||
{String(p.delivery_channel ?? '')} ·{' '}
|
||||
{String(p.delivery_address ?? '')}
|
||||
</p>
|
||||
<p>
|
||||
Sector: {String(p.sector_name ?? 'Por indicar')} ·
|
||||
Localização: {String(p.territory_name ?? 'Por indicar')}
|
||||
</p>
|
||||
{p.project_name && (
|
||||
<p>
|
||||
Projecto relacionado: {String(p.project_name)}. Esta
|
||||
relação não confirma execução ou contratação.
|
||||
</p>
|
||||
)}
|
||||
{p.opportunity && (
|
||||
<a className="underline" href={p.opportunity.url}>
|
||||
Oportunidade relacionada: {p.opportunity.title}
|
||||
</a>
|
||||
)}
|
||||
</section>
|
||||
{p.deadline_previous && (
|
||||
<p className="rounded border p-3">
|
||||
<strong>Prorrogação:</strong> prazo anterior{' '}
|
||||
{String(p.deadline_previous).replace('T', ' ')} (
|
||||
{String(p.timezone_previous)}) → novo prazo{' '}
|
||||
{String(p.deadline).replace('T', ' ')} ({String(p.timezone)}
|
||||
).
|
||||
</p>
|
||||
)}
|
||||
<dl className="grid gap-3 md:grid-cols-2">
|
||||
{Object.entries(labels)
|
||||
.filter(
|
||||
([key]) =>
|
||||
p[key] !== null &&
|
||||
p[key] !== undefined &&
|
||||
p[key] !== '',
|
||||
)
|
||||
.map(([key, label]) => (
|
||||
<div
|
||||
key={key}
|
||||
className="rounded border p-3 break-words"
|
||||
>
|
||||
<dt className="font-semibold">{label}</dt>
|
||||
<dd className="whitespace-pre-wrap">
|
||||
{links.includes(key) ? (
|
||||
<a
|
||||
className="underline"
|
||||
href={String(p[key])}
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{String(p[key])}
|
||||
</a>
|
||||
) : (
|
||||
(
|
||||
names[String(p[key])] ?? String(p[key])
|
||||
).replace(/^(\d{4}-\d{2}-\d{2})T/, '$1 ')
|
||||
)}
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
<section>
|
||||
<h3 className="text-lg font-semibold">Lotes</h3>
|
||||
{((p.lots ?? []) as Lot[]).length ? (
|
||||
<ul>
|
||||
{(p.lots as Lot[]).map((lot, i) => (
|
||||
<li key={i}>
|
||||
{lot.code} — {lot.description} ·{' '}
|
||||
{lot.quantities}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>Sem lotes indicados.</p>
|
||||
)}
|
||||
</section>
|
||||
<section>
|
||||
<h3 className="text-lg font-semibold">
|
||||
Documentos do concurso
|
||||
</h3>
|
||||
<ul>
|
||||
{((p.attachments ?? []) as Attachment[]).map((item, i) => (
|
||||
<li key={i}>
|
||||
{item.available === false ? (
|
||||
<span>
|
||||
{item.title} — documento actualmente
|
||||
indisponível
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
className="underline"
|
||||
href={`/documentos/${item.resource_id}/download`}
|
||||
>
|
||||
{item.title ??
|
||||
`Documento ${item.resource_id}`}{' '}
|
||||
— {item.purpose}
|
||||
</a>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{!((p.attachments as Attachment[]) ?? []).length && (
|
||||
<p>
|
||||
Sem anexos públicos neste acto. Consulte o canal e o
|
||||
anúncio de origem.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
<p className="rounded border p-3">
|
||||
O InvesteMoz divulga este acto e não recebe propostas nesta
|
||||
secção. Apresente a proposta no canal indicado nos documentos.
|
||||
Esta divulgação não substitui os meios oficiais exigidos,
|
||||
não comprova envio à UFSA ou ao financiador e não equivale à
|
||||
execução do investimento.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { Head, Link, router, usePage } from '@inertiajs/react';
|
||||
import { Frame, card, button } from '@/pages/investment/shared';
|
||||
import { NoticeContent, states, acts, type Publication } from './shared';
|
||||
export default function Show({
|
||||
id,
|
||||
publication,
|
||||
state,
|
||||
history,
|
||||
watching,
|
||||
backUrl,
|
||||
printMode,
|
||||
}: {
|
||||
id: string;
|
||||
publication: Publication;
|
||||
state: string;
|
||||
history: Publication[];
|
||||
watching: boolean;
|
||||
backUrl: string;
|
||||
printMode: boolean;
|
||||
}) {
|
||||
const { auth } = usePage<{ auth: { user: unknown | null } }>().props;
|
||||
return (
|
||||
<Frame title="Concurso e anúncio">
|
||||
<Head title={String(publication.payload.title)} />
|
||||
<style>
|
||||
{
|
||||
'@media print { nav, header, aside, footer, [data-slot="sidebar"], .no-print {display:none!important} main {margin:0!important; width:100%!important} details {display:block} }'
|
||||
}
|
||||
</style>
|
||||
<div className="no-print flex flex-wrap gap-4">
|
||||
<Link className="underline" href={backUrl}>
|
||||
Voltar aos resultados
|
||||
</Link>
|
||||
<Link className="underline" href={`/concursos/${id}/imprimir`}>
|
||||
Relatório imprimível
|
||||
</Link>
|
||||
{printMode && (
|
||||
<button className={button} onClick={() => window.print()}>
|
||||
Imprimir anúncio
|
||||
</button>
|
||||
)}
|
||||
{auth?.user ? (
|
||||
<button
|
||||
className={button}
|
||||
onClick={() =>
|
||||
watching
|
||||
? router.delete(`/concursos/${id}/acompanhar`)
|
||||
: router.put(`/concursos/${id}/acompanhar`)
|
||||
}
|
||||
>
|
||||
{watching
|
||||
? 'Deixar de acompanhar'
|
||||
: 'Acompanhar concurso'}
|
||||
</button>
|
||||
) : (
|
||||
<Link className="underline" href="/login">
|
||||
Entrar para acompanhar — conta simples
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<p className="rounded border p-3">
|
||||
<strong>{states[state]}</strong> · {acts[publication.act]} ·
|
||||
Versão {publication.version}
|
||||
</p>
|
||||
<p>
|
||||
Publicação deste acto no InvesteMoz: {publication.published_at}{' '}
|
||||
(UTC). Esta data não reinicia o prazo de origem.
|
||||
</p>
|
||||
<article className={card}>
|
||||
<NoticeContent p={publication.payload} />
|
||||
</article>
|
||||
<section className={card}>
|
||||
<h2 className="text-xl font-semibold">
|
||||
Actos publicados e histórico
|
||||
</h2>
|
||||
{history.map((v) => (
|
||||
<details key={v.id} open={printMode}>
|
||||
<summary>
|
||||
{acts[v.act]} · versão {v.version} ·{' '}
|
||||
{v.published_at} (UTC)
|
||||
</summary>
|
||||
<p className="my-2">Motivo público: {v.reason}</p>
|
||||
<NoticeContent p={v.payload} />
|
||||
</details>
|
||||
))}
|
||||
</section>
|
||||
</Frame>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
use App\Http\Controllers\ProcurementController as Procurement;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
Route::get('/concursos', [Procurement::class, 'index'])->name('procurement.index');
|
||||
Route::get('/concursos/acompanhar', [Procurement::class, 'watched'])->middleware('auth');
|
||||
Route::get('/concursos/{id}', [Procurement::class, 'show'])->whereUuid('id')->name('procurement.show');
|
||||
Route::get('/concursos/{id}/imprimir', [Procurement::class, 'print'])->whereUuid('id');
|
||||
Route::put('/concursos/{id}/acompanhar', [Procurement::class, 'watch'])->whereUuid('id')->middleware(['auth', 'throttle:30,1']);
|
||||
Route::delete('/concursos/{id}/acompanhar', [Procurement::class, 'unwatch'])->whereUuid('id')->middleware(['auth', 'throttle:30,1']);
|
||||
Route::middleware(['auth', 'verified'])->prefix('management/concursos')->group(function () {
|
||||
Route::get('/', [Procurement::class, 'manage']);
|
||||
Route::get('/novo', [Procurement::class, 'edit']);
|
||||
Route::post('/', [Procurement::class, 'save']);
|
||||
Route::get('/{id}', [Procurement::class, 'edit'])->whereUuid('id');
|
||||
Route::put('/{id}', [Procurement::class, 'save'])->whereUuid('id');
|
||||
Route::post('/{id}/previa', [Procurement::class, 'preview'])->whereUuid('id');
|
||||
Route::post('/{id}/publicar', [Procurement::class, 'publish'])->whereUuid('id');
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
require __DIR__.'/procurement.php';
|
||||
|
||||
require __DIR__.'/existing-business.php';
|
||||
require __DIR__.'/territorial-registry.php';
|
||||
require __DIR__.'/partnerships.php';
|
||||
@@ -62,12 +64,15 @@ Route::get('/explorar/{sectorSlug}', [LegacyContentController::class, 'landing']
|
||||
Route::get('/{legacySlug}', [LegacyContentController::class, 'article'])->whereIn('legacySlug', array_keys(config('legacy_content.articles', [])));
|
||||
Route::post('/apoio', [PortalServicesController::class, 'submit'])->middleware('throttle:10,1')->name('services.submit');
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::get('/gestao-conteudos', fn (\Illuminate\Http\Request $request) => Inertia::render('management/content-hub', app(\App\Services\ContentManagement::class)->view($request->user())))->name('content-management');
|
||||
Route::get('/balcao-virtual', [PortalServicesController::class, 'page'])->defaults('page', 'desk')->name('services.desk');
|
||||
Route::post('/parcerias', [PortalServicesController::class, 'partnership'])->middleware('throttle:10,1')->name('services.partnership');
|
||||
});
|
||||
Route::middleware(['auth', 'verified', \App\Http\Middleware\EnsureScopedEditorial::class])->group(function () {
|
||||
Route::get('/management/portal-services/resources/{id}/article', [\App\Http\Controllers\EditorialArticleController::class, 'edit'])->whereNumber('id')->name('editorial.edit');
|
||||
Route::put('/management/portal-services/resources/{id}/article', [\App\Http\Controllers\EditorialArticleController::class, 'save'])->whereNumber('id')->name('editorial.save');
|
||||
Route::get('/management/portal-services/resources/{id}/article/preview', [\App\Http\Controllers\EditorialArticleController::class, 'preview'])->whereNumber('id')->name('editorial.preview');
|
||||
Route::post('/management/portal-services/resources/{id}/article/preview', [\App\Http\Controllers\EditorialArticleController::class, 'previewContent'])->whereNumber('id')->name('editorial.preview-content');
|
||||
Route::get('/management/editorial-installation', [\App\Http\Controllers\EditorialArticleController::class, 'installation'])->name('editorial.installation');
|
||||
Route::get('/management/editorial-installation/mfa', [\App\Http\Controllers\EditorialArticleController::class, 'showMfa'])->name('editorial.mfa');
|
||||
Route::post('/management/editorial-installation/mfa', [\App\Http\Controllers\EditorialArticleController::class, 'confirmMfa'])->middleware('throttle:5,1');
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Services\InvestmentCatalogue;
|
||||
use App\Services\InvestmentZones;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CatalogueSelectorReferencesTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$db = DB::selectOne('select current_database() as name, inet_server_port() as port');
|
||||
$this->assertSame('simulator_fiscal_delta_20261021_test', $db->name);
|
||||
$this->assertSame(55441, (int) $db->port);
|
||||
$this->withoutVite();
|
||||
}
|
||||
|
||||
public function test_public_parks_remain_selectable_with_zero_results_without_exposing_private_or_other_territories(): void
|
||||
{
|
||||
$province = '059b02ca-7095-577a-8705-4d4c46896747';
|
||||
$template = app(InvestmentZones::class)->visible()->firstOrFail();
|
||||
$ids = [];
|
||||
foreach (['public', 'private', 'other'] as $case) {
|
||||
$zone = $template->replicate();
|
||||
$zone->id = (string) Str::uuid();
|
||||
$zone->code = 'SINT-SELECTOR-'.Str::random(12);
|
||||
$zone->name = 'SINTÉTICO QA '.$case;
|
||||
$zone->category = 'industrial_park';
|
||||
$zone->primary_territory_id = $case === 'other' ? '9e42dac8-fd9b-523f-bf59-71a7d3aa0fb8' : $province;
|
||||
$zone->publication_status = $case === 'private' ? 'draft' : 'published';
|
||||
$zone->source_evidence = [['source_type' => 'government', 'url' => 'https://example.test/qa-only', 'supports' => 'Synthetic QA fixture, not a legal source']];
|
||||
$zone->save();
|
||||
$ids[$case] = $zone->id;
|
||||
}
|
||||
$scope = ['province_id' => $province, 'park_id' => $ids['public'], 'q' => 'no-matching-opportunity-'.Str::uuid()];
|
||||
$this->assertSame(0, app(InvestmentCatalogue::class)->query($scope)->count());
|
||||
$response = $this->getJson('/api/v1/investment/references?'.http_build_query($scope))->assertOk();
|
||||
$available = array_column($response->json('zones'), 'id');
|
||||
$this->assertContains($ids['public'], $available);
|
||||
$this->assertNotContains($ids['private'], $available);
|
||||
$this->assertNotContains($ids['other'], $available);
|
||||
$this->assertContains($ids['other'], array_column($this->getJson('/api/v1/investment/references')->assertOk()->json('zones'), 'id'));
|
||||
$this->get('/zonas/'.$ids['public'])->assertOk();
|
||||
$this->get('/zonas/'.$ids['private'])->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_activity_catalogue_does_not_disappear_when_opportunity_search_has_no_matches(): void
|
||||
{
|
||||
$catalogue = app(InvestmentCatalogue::class);
|
||||
$global = collect($catalogue->references()['classification']['targets']['activity'])->pluck('id')->all();
|
||||
$this->assertNotEmpty($global);
|
||||
foreach (['059b02ca-7095-577a-8705-4d4c46896747', '9e42dac8-fd9b-523f-bf59-71a7d3aa0fb8'] as $province) {
|
||||
$scope = ['province_id' => $province, 'q' => 'no-matching-opportunity-'.Str::uuid(), 'sector' => 'tourism'];
|
||||
$this->assertSame(0, $catalogue->query($scope)->count());
|
||||
$this->assertSame($global, collect($catalogue->references(false, $scope)['classification']['targets']['activity'])->pluck('id')->all());
|
||||
}
|
||||
}
|
||||
|
||||
public function test_public_master_structures_and_chain_are_available_without_opportunities_or_published_dossiers(): void
|
||||
{
|
||||
$province = '059b02ca-7095-577a-8705-4d4c46896747';
|
||||
$clusters = [];
|
||||
foreach (['public', 'internal'] as $visibility) {
|
||||
$id = (string) Str::uuid();
|
||||
DB::table('economic_clusters')->insert(['id' => $id, 'code' => 'SINT-'.Str::random(12), 'name' => 'SINTÉTICO QA cluster',
|
||||
'sector' => 'QA', 'status' => 'active', 'data_classification' => $visibility, 'primary_territory_id' => $province]);
|
||||
$clusters[$visibility] = $id;
|
||||
}
|
||||
$corridor = (string) Str::uuid();
|
||||
DB::table('development_corridors')->insert(['id' => $corridor, 'code' => 'SINT-'.Str::random(12), 'name' => 'SINTÉTICO QA corredor',
|
||||
'status' => 'active', 'data_classification' => 'public', 'primary_territory_id' => $province]);
|
||||
$project = \App\Models\Project::factory()->create(['data_classification' => 'public', 'primary_territory_id' => $province]);
|
||||
$chain = (string) Str::uuid();
|
||||
DB::table('value_chains')->insert(['id' => $chain, 'cluster_id' => $clusters['public'], 'code' => 'SINT-'.Str::random(12),
|
||||
'name' => 'SINTÉTICO QA cadeia', 'stage' => 'production', 'data_classification' => 'public']);
|
||||
$refs = app(InvestmentCatalogue::class)->references(false, ['province_id' => $province, 'q' => 'no-matching-opportunity']);
|
||||
$this->assertContains($clusters['public'], $refs['clusters']->pluck('id')->all());
|
||||
$this->assertNotContains($clusters['internal'], $refs['clusters']->pluck('id')->all());
|
||||
$this->assertContains($corridor, $refs['corridors']->pluck('id')->all());
|
||||
$this->assertContains($project->id, $refs['projects']->pluck('id')->all());
|
||||
$this->assertContains($chain, collect($refs['classification']['targets']['chain'])->pluck('id')->all());
|
||||
$other = app(InvestmentCatalogue::class)->references(false, ['province_id' => '9e42dac8-fd9b-523f-bf59-71a7d3aa0fb8']);
|
||||
$this->assertNotContains($clusters['public'], $other['clusters']->pluck('id')->all());
|
||||
$this->assertNotContains($chain, collect($other['classification']['targets']['chain'])->pluck('id')->all());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\ContentManagement;
|
||||
use App\Services\PortalProfileAdministration;
|
||||
use App\Services\OpportunityManagement;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use App\Services\Malware\MalwareScanner;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ContentManagementEntryTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$db = DB::selectOne('select current_database() as name, inet_server_port() as port');
|
||||
$this->assertSame('simulator_fiscal_delta_20261021_test', $db->name);
|
||||
$this->assertSame(55441, (int) $db->port);
|
||||
$this->withoutVite();
|
||||
}
|
||||
|
||||
public function test_entry_does_not_grant_access_to_an_investor(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
config(['portal.managers' => [], 'portal.document_managers' => []]);
|
||||
$this->assertSame([], app(ContentManagement::class)->access($user));
|
||||
$this->actingAs($user)->get('/gestao-conteudos')->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_existing_manager_gets_reused_destinations_and_document_intent_and_search(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
config(['portal.managers' => [$user->email]]);
|
||||
$this->actingAs($user)->get('/gestao-conteudos')->assertOk()->assertInertia(fn (Assert $page) => $page->component('management/content-hub')
|
||||
->where('access.documents.edit', true)->where('access.opportunities.create', '/management/opportunities/create'));
|
||||
$this->get('/management/portal-services?intent=legislation&q=not-existing-qa-title')->assertOk()->assertInertia(fn (Assert $page) => $page
|
||||
->where('managementFilters.intent', 'legislation')->has('associationOptions.sector')->has('associationOptions.stage')->has('resources', 0));
|
||||
$this->get('/management/portal-services?state=published')->assertOk();
|
||||
$this->get('/management/portal-services?state=withdrawn')->assertOk();
|
||||
}
|
||||
|
||||
public function test_team_promoter_uses_operational_editor_without_general_administration(): void
|
||||
{
|
||||
config(['portal.profile_access_enabled' => true, 'portal.opportunity_specialized_drafts_enabled' => true, 'portal.managers' => []]);
|
||||
$actor = User::factory()->create();
|
||||
$actor->forceFill(['two_factor_secret' => Crypt::encryptString('JBSWY3DPEHPK3PXP'), 'two_factor_confirmed_at' => now()])->save();
|
||||
$org = (string) Str::uuid();
|
||||
DB::table('organizations')->insert(['id' => $org, 'name' => 'Synthetic QA content team', 'organization_type' => 'private']);
|
||||
DB::table('memberships')->insert(['id' => (string) Str::uuid(), 'user_id' => $actor->id, 'organization_id' => $org, 'role' => 'officer']);
|
||||
app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), User::factory()->create(), [
|
||||
'user_id' => $actor->id, 'role' => 'promoter', 'organization_id' => $org,
|
||||
'opportunity_types' => OpportunityManagement::CATEGORIES, 'responsibilities' => [], 'source' => 'synthetic-content-entry-test',
|
||||
]);
|
||||
$before = DB::table('portal_role_assignments')->count();
|
||||
$this->actingAs($actor)->withSession(['portal.mfa_actor' => $actor->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $actor->portal_access_revision]);
|
||||
$this->get('/gestao-conteudos')->assertOk()->assertInertia(fn (Assert $page) => $page
|
||||
->where('access.opportunities.create', '/minhas-oportunidades/create')->where('access.opportunities.profileWorkflow', true)->missing('access.documents'));
|
||||
$this->get('/minhas-oportunidades/create')->assertOk();
|
||||
$this->get('/management/portal-services')->assertForbidden();
|
||||
$this->assertSame($before, DB::table('portal_role_assignments')->count());
|
||||
}
|
||||
|
||||
public function test_new_legislation_is_recovered_edited_published_and_withdrawn_with_history(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
config(['queue.default' => 'sync', 'services.malware.scanner' => 'clamd', 'services.malware.host' => '127.0.0.1', 'services.malware.port' => 3310]);
|
||||
$this->assertTrue(app(MalwareScanner::class)->health()['available'], 'Real ClamD required for this QA demonstration');
|
||||
$user = User::factory()->create();
|
||||
config(['portal.managers' => [$user->email]]);
|
||||
$this->actingAs($user);
|
||||
$name = 'synthetic-entry-'.Str::uuid().'.txt';
|
||||
$this->post('/management/portal-services/upload', ['intent' => 'legislation', 'file' => UploadedFile::fake()->createWithContent($name, 'Synthetic QA legislation example, no legal value.')])
|
||||
->assertSessionHasNoErrors()->assertSessionHas('uploaded_document_id')->assertRedirect('/management/portal-services?intent=legislation');
|
||||
$document = DB::table('documents')->where('file_name', $name)->first();
|
||||
$this->assertSame('clean', $document->malware_scan_status);
|
||||
$payload = ['document_id' => $document->id, 'title' => $name, 'category' => 'Synthetic QA', 'topic' => 'Synthetic QA',
|
||||
'document_type' => 'legislation', 'issuer' => 'Synthetic QA only', 'source_url' => 'https://example.invalid/synthetic-qa', 'language' => 'pt', 'version_label' => 'QA-1',
|
||||
'territorial_scope' => 'national', 'source_confirmation' => 'identified', 'legal_status' => 'proposed',
|
||||
'rights_status' => 'authorized', 'data_classification' => 'public', 'source_reference' => 'Synthetic QA example, no official provenance or legal value.',
|
||||
'legislation' => true, 'essential' => false, 'independent_review' => false, 'associations' => []];
|
||||
$this->post('/management/portal-services/resources', $payload)->assertRedirect()->assertSessionHasNoErrors();
|
||||
$resource = DB::table('portal_resources')->where('document_id', $document->id)->first();
|
||||
$this->assertNotNull($resource);
|
||||
$this->assertSame($name, $resource->title);
|
||||
$this->get('/management/portal-services?q='.$name)->assertOk()->assertInertia(fn (Assert $page) => $page->has('resources', 1)->where('resources.0.title', $name));
|
||||
$this->put('/management/portal-services/resources/'.$resource->id, [...$payload, 'title' => $name.' edited', 'metadata_version' => $resource->metadata_version])->assertSessionHasNoErrors();
|
||||
$this->post('/management/portal-services/resources/'.$resource->id, ['published' => true])->assertSessionHasNoErrors();
|
||||
$this->get('/documentos/'.$resource->id.'/download')->assertOk()->assertDownload($name);
|
||||
$this->post('/management/portal-services/resources/'.$resource->id, ['published' => false])->assertSessionHasNoErrors();
|
||||
$this->get('/management/portal-services?state=withdrawn&q='.$name)->assertOk()->assertInertia(fn (Assert $page) => $page->has('resources', 1));
|
||||
$this->get('/management/portal-services?state=draft&q='.$name)->assertOk()->assertInertia(fn (Assert $page) => $page->has('resources', 0));
|
||||
$this->get('/documentos/'.$resource->id.'/download')->assertNotFound();
|
||||
$this->assertDatabaseHas('documents', ['id' => $document->id, 'sha256' => $document->sha256]);
|
||||
$this->assertSame(4, DB::table('portal_resource_revisions')->where('resource_id', $resource->id)->count());
|
||||
}
|
||||
|
||||
public function test_existing_named_editorial_designation_survives_removal_of_technical_reservation(): void
|
||||
{
|
||||
$actor = User::factory()->create();
|
||||
$other = User::factory()->create();
|
||||
$actor->forceFill(['two_factor_secret' => Crypt::encryptString('JBSWY3DPEHPK3PXP'), 'two_factor_confirmed_at' => now()])->save();
|
||||
config(['portal.homologation_only' => false, 'portal.profile_access_enabled' => true,
|
||||
'portal.managers' => [], 'homologation_editorial.accounts' => [$actor->email]]);
|
||||
DB::table('portal_superuser_pair')->updateOrInsert(['id' => 1], ['first_user_id' => $actor->id, 'second_user_id' => $other->id, 'approved_by' => $other->id]);
|
||||
$this->actingAs($actor)->withSession(['portal.mfa_actor' => $actor->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $actor->portal_access_revision]);
|
||||
$this->get('/gestao-conteudos')->assertOk()->assertInertia(fn (Assert $page) => $page->where('access.documents.edit', true));
|
||||
$this->get('/management/portal-services?intent=legislation')->assertOk()->assertInertia(fn (Assert $page) => $page
|
||||
->where('editorialScopeOnly', true)->where('documentAccessOnly', false));
|
||||
config(['homologation_editorial.accounts' => []]);
|
||||
$this->get('/management/portal-services')->assertForbidden();
|
||||
config(['homologation_editorial.accounts' => [$actor->email]]);
|
||||
$this->withSession(['portal.mfa_at' => time() - 901]);
|
||||
$this->assertFalse(app(\App\Services\DocumentManagementAccess::class)->editorial($actor));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\EditorialReading;
|
||||
use App\Services\LegacyContent;
|
||||
use App\Services\Malware\MalwareScanner;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\TestCase;
|
||||
|
||||
class LegacyEditorialWorkflowTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
private User $editor;
|
||||
|
||||
private $resources;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$db = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||
$this->assertSame('simulator_fiscal_delta_20261021_test', $db->name);
|
||||
$this->assertSame(55441, (int) $db->port);
|
||||
$this->withoutVite();
|
||||
$identities = json_decode(file_get_contents(config_path('legacy_editorial_resources.json')), true);
|
||||
$this->resources = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->whereIn('d.external_id', array_column($identities, 'external_id'))->orderBy('r.id')->get(['r.*']);
|
||||
$this->assertCount(28, $this->resources);
|
||||
$this->editor = User::factory()->create();
|
||||
config(['portal.managers' => [$this->editor->email]]);
|
||||
$this->actingAs($this->editor);
|
||||
}
|
||||
|
||||
private function payload(object $r, string $body): array
|
||||
{
|
||||
return ['metadata_version' => (int) $r->metadata_version, 'body' => $body, 'image_mode' => 'original', 'image_resource_id' => null];
|
||||
}
|
||||
|
||||
public function test_all_28_open_from_existing_originals_and_private_previews_without_reimport_or_save(): void
|
||||
{
|
||||
$before = DB::table('portal_resource_revisions')->count();
|
||||
$service = app(EditorialReading::class);
|
||||
foreach ($this->resources as $r) {
|
||||
$original = $service->original($r);
|
||||
$this->assertNotEmpty($original['content_html']);
|
||||
$text = fn (string $html) => preg_replace('/\s+/u', '', html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
||||
$this->assertSame($text($original['content_html']), $text(app(LegacyContent::class)->sanitize($original['content_html'], true)), 'Reader must preserve the original text, including inline spans and table cells.');
|
||||
$this->assertStringStartsWith('https://investemoz.tempo.co.mz/', $service->reading($r, null, true)['image_source_url']);
|
||||
$this->get('/management/portal-services/resources/'.$r->id.'/article')->assertOk()->assertHeader('Cache-Control', 'no-store, private')->assertInertia(fn (Assert $p) => $p->where('source.legacy_id', $original['legacy_id'])->where('content.body', $service->content($r, $original)['body']));
|
||||
$this->get('/management/portal-services/resources/'.$r->id.'/article/preview')->assertOk()->assertHeader('X-Robots-Tag', 'noindex, nofollow')->assertInertia(fn (Assert $p) => $p->where('article.private_preview', true)->where('article.source_url', $original['source_url'])->where('article.legacy_id', $original['legacy_id']));
|
||||
$this->get('/artigos/'.$r->id)->assertNotFound();
|
||||
}
|
||||
$this->assertSame($before, DB::table('portal_resource_revisions')->count());
|
||||
}
|
||||
|
||||
public function test_review_changes_save_recover_versions_and_stale_previews_preserve_original(): void
|
||||
{
|
||||
$r = $this->resources->first(fn ($item) => ! $item->editorial_content);
|
||||
$original = app(EditorialReading::class)->original($r);
|
||||
$sha = DB::table('documents')->where('id', $r->document_id)->value('sha256');
|
||||
$payload = $this->payload($r, 'Correcção editorial sintética QA; preservar o original e não publicar.');
|
||||
$this->postJson('/management/portal-services/resources/'.$r->id.'/article/preview', $payload)->assertOk()->assertJsonPath('article.body', $payload['body']);
|
||||
$this->assertNull(DB::table('portal_resources')->where('id', $r->id)->value('editorial_content'));
|
||||
$this->put('/management/portal-services/resources/'.$r->id.'/article', $payload)->assertSessionHasNoErrors()->assertRedirect();
|
||||
$this->get('/management/portal-services/resources/'.$r->id.'/article')->assertInertia(fn (Assert $p) => $p->where('resource.metadata_version', (int) $r->metadata_version + 1)->where('content.body', $payload['body']));
|
||||
$this->postJson('/management/portal-services/resources/'.$r->id.'/article/preview', $payload)->assertConflict();
|
||||
$this->put('/management/portal-services/resources/'.$r->id.'/article', $payload)->assertConflict();
|
||||
$payload['metadata_version']++;
|
||||
$payload['body'] .= ' Segunda revisão.';
|
||||
$this->put('/management/portal-services/resources/'.$r->id.'/article', $payload)->assertSessionHasNoErrors()->assertRedirect();
|
||||
$this->get('/management/portal-services/resources/'.$r->id.'/article/preview')->assertInertia(fn (Assert $p) => $p->where('article.body', $payload['body'])->where('article.source_url', $original['source_url']));
|
||||
$this->assertSame($sha, DB::table('documents')->where('id', $r->document_id)->value('sha256'));
|
||||
$this->assertSame($original, app(EditorialReading::class)->original($r));
|
||||
$this->assertFalse((bool) DB::table('portal_resources')->where('id', $r->id)->value('published'));
|
||||
$this->assertGreaterThanOrEqual(2, DB::table('portal_resource_revisions')->where('resource_id', $r->id)->count());
|
||||
}
|
||||
|
||||
public function test_publication_remains_explicit_and_reader_works_before_first_editorial_save(): void
|
||||
{
|
||||
$r = $this->resources->first(fn ($item) => ! $item->editorial_content);
|
||||
$original = app(EditorialReading::class)->original($r);
|
||||
$this->post('/management/portal-services/resources/'.$r->id, ['published' => true])->assertSessionHasErrors();
|
||||
$document = DB::table('documents')->find($r->document_id);
|
||||
$metadata = ['document_id' => $r->document_id, 'title' => $r->title, 'category' => $r->category, 'topic' => $r->topic, 'issuer' => $document->issuer,
|
||||
'document_type' => $r->document_type, 'language' => 'pt', 'description' => 'Arquivo editorial QA. Sem oportunidade concreta ou regra activa.',
|
||||
'source_url' => $r->source_url, 'source_reference' => $r->source_reference, 'source_confirmation' => 'identified', 'legal_status' => 'historical', 'rights_status' => 'authorized', 'data_classification' => 'public', 'metadata_version' => $r->metadata_version, 'associations' => [],
|
||||
'territorial_scope' => 'unknown', 'independent_review' => false, 'legislation' => false, 'essential' => false];
|
||||
$this->put('/management/portal-services/resources/'.$r->id, $metadata)->assertSessionHasNoErrors()->assertRedirect();
|
||||
$this->get('/artigos/'.$r->id)->assertNotFound();
|
||||
$this->post('/management/portal-services/resources/'.$r->id, ['published' => true])->assertSessionHasNoErrors()->assertRedirect();
|
||||
Auth::logout();
|
||||
$this->app['auth']->forgetGuards();
|
||||
$this->get('/artigos/'.$r->id)->assertOk()->assertInertia(fn (Assert $p) => $p->where('article.legacy_id', $original['legacy_id'])->where('article.private_preview', false));
|
||||
$this->get('/documentos/'.$r->id)->assertOk()->assertInertia(fn (Assert $p) => $p->where('articleUrl', '/artigos/'.$r->id));
|
||||
$this->actingAs($this->editor);
|
||||
$current = DB::table('portal_resources')->find($r->id);
|
||||
$this->put('/management/portal-services/resources/'.$r->id.'/article', $this->payload($current, 'Edição sintética depois da publicação; revisão expressa necessária.'))->assertRedirect();
|
||||
$this->get('/artigos/'.$r->id)->assertNotFound();
|
||||
}
|
||||
|
||||
public function test_named_editorial_scope_reuses_28_exact_original_identities_and_mfa_without_global_grant(): void
|
||||
{
|
||||
$actor = $this->editor;
|
||||
$other = User::factory()->create();
|
||||
$actor->forceFill(['two_factor_secret' => Crypt::encryptString('JBSWY3DPEHPK3PXP'), 'two_factor_confirmed_at' => now()])->save();
|
||||
config(['portal.profile_access_enabled' => true, 'portal.managers' => [], 'homologation_editorial.accounts' => [$actor->email]]);
|
||||
DB::table('portal_superuser_pair')->updateOrInsert(['id' => 1], ['first_user_id' => $actor->id, 'second_user_id' => $other->id, 'approved_by' => $other->id]);
|
||||
$this->withSession(['portal.mfa_actor' => $actor->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $actor->portal_access_revision]);
|
||||
$this->get('/management/portal-services/resources/'.$this->resources->first()->id.'/article/preview')->assertOk();
|
||||
$this->assertFalse($actor->can('manage-portal'));
|
||||
DB::table('documents')->where('id', $this->resources->first()->document_id)->update(['external_id' => 'destination-upload-qa-identity', 'source_system' => 'portal-services']);
|
||||
$this->get('/management/portal-services/resources/'.$this->resources->first()->id.'/article/preview')->assertOk();
|
||||
$this->get('/management/portal-services/resources/38/article/preview')->assertForbidden();
|
||||
$this->withSession(['portal.mfa_at' => time() - 901])->get('/management/portal-services/resources/'.$this->resources->first()->id.'/article/preview')->assertForbidden();
|
||||
$this->actingAs($other)->get('/management/portal-services/resources/'.$this->resources->first()->id.'/article/preview')->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_structured_original_is_sanitized_and_unavailable_images_are_explicit(): void
|
||||
{
|
||||
$html = app(LegacyContent::class)->sanitize('<div onclick="evil()"><table><tr><td>Conteúdo</td></tr></table><script>evil()</script><a href="javascript:evil()">Ligação</a></div>', true);
|
||||
$this->assertStringContainsString('<table>', $html);
|
||||
$this->assertStringContainsString('Conteúdo', $html);
|
||||
$this->assertStringNotContainsString('evil', $html);
|
||||
$this->assertStringNotContainsString('onclick', $html);
|
||||
$r = $this->resources->first(fn ($item) => ! $item->editorial_content);
|
||||
$data = app(EditorialReading::class)->reading($r, null, true);
|
||||
if (! $data['image_url']) {
|
||||
$this->assertNotEmpty($data['image_notice']);
|
||||
}
|
||||
$payload = $this->payload($r, app(EditorialReading::class)->content($r, app(EditorialReading::class)->original($r))['body']);
|
||||
$payload['image_mode'] = 'selected';
|
||||
$payload['image_resource_id'] = 999999;
|
||||
$this->postJson('/management/portal-services/resources/'.$r->id.'/article/preview', $payload)->assertUnprocessable();
|
||||
}
|
||||
|
||||
public function test_team_selects_a_real_scanned_image_and_withdrawal_never_exposes_private_bytes(): void
|
||||
{
|
||||
config(['queue.default' => 'sync', 'services.malware.scanner' => 'clamd', 'services.malware.socket' => null, 'services.malware.host' => '127.0.0.1', 'services.malware.port' => 3310]);
|
||||
$this->assertTrue(app(MalwareScanner::class)->health()['available']);
|
||||
$name = 'synthetic-editorial-'.Str::uuid().'.png';
|
||||
$bytes = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jVmkAAAAASUVORK5CYII=');
|
||||
$this->post('/management/portal-services/upload', ['file' => UploadedFile::fake()->createWithContent($name, $bytes)])->assertSessionHasNoErrors();
|
||||
$doc = DB::table('documents')->where('file_name', $name)->first();
|
||||
$this->assertNotNull($doc);
|
||||
try {
|
||||
$this->assertSame('clean', $doc->malware_scan_status);
|
||||
$metadata = ['document_id' => $doc->id, 'title' => 'Imagem sintética QA', 'issuer' => 'Synthetic QA', 'category' => 'education', 'topic' => 'legacy-archive', 'document_type' => 'unknown', 'language' => 'pt', 'source_url' => 'https://example.invalid/qa-image.png', 'source_reference' => 'Imagem sintética para ensaio transaccional, não é original do legado.', 'source_confirmation' => 'identified', 'legal_status' => 'not_applicable', 'rights_status' => 'authorized', 'data_classification' => 'public', 'territorial_scope' => 'unknown', 'independent_review' => false, 'legislation' => false, 'essential' => false, 'associations' => []];
|
||||
$this->post('/management/portal-services/resources', $metadata)->assertSessionHasNoErrors();
|
||||
$image = DB::table('portal_resources')->where('document_id', $doc->id)->first();
|
||||
$this->post('/management/portal-services/resources/'.$image->id, ['published' => true])->assertSessionHasNoErrors();
|
||||
$r = $this->resources->first();
|
||||
$payload = $this->payload($r, 'Texto sintético de prévia com selecção de imagem.');
|
||||
$payload['image_mode'] = 'selected';
|
||||
$payload['image_resource_id'] = $image->id;
|
||||
$this->postJson('/management/portal-services/resources/'.$r->id.'/article/preview', $payload)->assertOk()->assertJsonPath('article.image_url', '/documentos/'.$image->id.'/view');
|
||||
$this->post('/management/portal-services/resources/'.$image->id, ['published' => false])->assertSessionHasNoErrors();
|
||||
$this->postJson('/management/portal-services/resources/'.$r->id.'/article/preview', $payload)->assertUnprocessable();
|
||||
Auth::logout();
|
||||
$this->app['auth']->forgetGuards();
|
||||
$this->get('/documentos/'.$image->id.'/view')->assertNotFound();
|
||||
} finally {
|
||||
Storage::disk('local')->delete($doc->storage_key);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\PrivateInvestmentPreparation;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PrivateInvestmentPreparationTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->withoutVite();
|
||||
$db = DB::selectOne('select current_database() as name, inet_server_port() as port');
|
||||
$this->assertSame('simulator_fiscal_delta_20261021_test', $db->name);
|
||||
$this->assertSame(55441, (int) $db->port);
|
||||
config(['investor-path.enabled' => true, 'investor-path.private_preparation' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]);
|
||||
}
|
||||
|
||||
private function declarations(): array
|
||||
{
|
||||
return ['investment_form' => 'direct', 'investment_preparation' => 'yes', 'investment_activity_scope' => 'general', 'implementation_mode' => 'domestic_entity', 'capital_origin' => 'national', 'site_tenure' => 'lease', 'project_concession' => 'no', 'significant_implications' => 'no', 'land_extension' => 'no', 'forest_concession' => 'no', 'total_investment' => '1000000', 'currency' => 'MZN', 'expected_start_date' => '2026-01-01', 'entity_stage' => 'being_formed'];
|
||||
}
|
||||
|
||||
private function dossier(User $user): string
|
||||
{
|
||||
return $this->actingAs($user)->postJson('/investimentos', ['title' => 'SINTÉTICO C — percurso adaptativo', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id');
|
||||
}
|
||||
|
||||
private function save(string $id, array $declarations, array $links = []): array
|
||||
{
|
||||
$view = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json();
|
||||
$body = ['version' => $view['case']['version'], 'selection' => [], 'declarations' => $declarations, 'document_links' => $links];
|
||||
$plan = $this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertOk()->json();
|
||||
// The preview is read-only.
|
||||
$this->getJson('/investimentos/'.$id.'/contexto')->assertJsonPath('context.revision', $view['context']['revision']);
|
||||
$control = [...$body, 'preview_hash' => $plan['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true];
|
||||
$this->postJson('/investimentos/'.$id.'/contexto/confirmar', $control)->assertOk();
|
||||
$this->postJson('/investimentos/'.$id.'/contexto/confirmar', $control)->assertOk()->assertJsonPath('replayed', true);
|
||||
|
||||
return $plan;
|
||||
}
|
||||
|
||||
public function test_boundaries_unknown_conditions_and_scope_are_not_silently_presumed(): void
|
||||
{
|
||||
$service = app(PrivateInvestmentPreparation::class);
|
||||
$assess = fn (array $changes = []) => $service->assess(['fields' => [...$this->declarations(), ...$changes]]);
|
||||
$this->assertSame('registration', $assess(['total_investment' => '32000000000'])['mode']);
|
||||
$this->assertSame('authorization', $assess(['total_investment' => '32000000000.0001'])['mode']);
|
||||
$this->assertSame('authorization', $assess(['land_extension' => 'yes', 'area_required' => '10000', 'area_unit' => 'ha'])['mode']);
|
||||
$this->assertSame('registration', $assess(['forest_concession' => 'yes', 'forest_concession_hectares' => '100000'])['mode']);
|
||||
$this->assertSame('authorization', $assess(['forest_concession' => 'yes', 'forest_concession_hectares' => '100000.01'])['mode']);
|
||||
$this->assertSame('indeterminate', $assess(['significant_implications' => 'unknown'])['mode']);
|
||||
$this->assertSame('indeterminate', $assess(['currency' => 'USD'])['mode']);
|
||||
$this->assertSame('indeterminate', $assess(['investment_form' => 'unknown'])['mode']);
|
||||
$this->assertSame('indeterminate', $assess(['investment_form' => 'indirect'])['mode']);
|
||||
$this->assertSame('authorization', $assess(['investment_form' => 'indirect', 'project_concession' => 'yes'])['mode']);
|
||||
foreach ([['investment_activity_scope' => 'extractive'], ['expected_start_date' => '2027-01-01']] as $changes) {
|
||||
$result = $assess($changes);
|
||||
$this->assertSame('indeterminate', $result['mode']);
|
||||
$this->assertFalse(collect($result['requirements'])->contains('required', true));
|
||||
}
|
||||
$this->assertSame('registration', $assess(['investor_origin' => 'foreign', 'tax_residency_country' => 'ZA'])['mode']);
|
||||
$this->assertFalse(collect($assess(['investor_origin' => 'foreign'])['requirements'])->firstWhere('key', 'representation_license')['required']);
|
||||
foreach (['zee', 'zfi'] as $regime) {
|
||||
$zone = $service->assess(['fields' => $this->declarations(), 'selection' => ['park' => ['fields' => ['investment_regime' => $regime]]]]);
|
||||
$this->assertStringContainsString('APIEX', $zone['authority']);
|
||||
$this->assertStringContainsString('condicionado', $zone['authority']);
|
||||
$this->assertArrayNotHasKey('benefit_rate', $zone);
|
||||
}
|
||||
$park = $service->assess(['fields' => $this->declarations(), 'selection' => ['park' => ['fields' => ['investment_regime' => 'industrial_park']]]]);
|
||||
$this->assertSame('Governador da Província', $park['authority']);
|
||||
}
|
||||
|
||||
public function test_context_changes_adapt_requirements_and_preserve_association_history_and_results(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$id = $this->dossier($owner);
|
||||
$doc = (string) Str::uuid();
|
||||
DB::table('documents')->insert(['id' => $doc, 'file_name' => 'SINTETICO-C-QA.txt', 'media_type' => 'text/plain', 'storage_key' => 'synthetic-test-no-download', 'sha256' => hash('sha256', 'synthetic'), 'status' => 'quarantined']);
|
||||
DB::table('desk_documents')->insert(['id' => (string) Str::uuid(), 'case_id' => $id, 'document_id' => $doc, 'category' => 'support', 'source_reference' => 'Synthetic QA only', 'linked_by' => $owner->id]);
|
||||
$initial = $this->save($id, $this->declarations(), ['lease' => $doc]);
|
||||
$this->assertSame('registration', $initial['private_investment']['after']['mode']);
|
||||
$view = $this->getJson('/investimentos/'.$id.'/contexto')->json();
|
||||
$this->assertSame('associated', collect($view['private_investment']['requirements'])->firstWhere('key', 'lease')['state']);
|
||||
$this->assertLessThan(20000, mb_strlen($view['prefill']['simulator']['context_applicability_snapshot']));
|
||||
$this->assertArrayNotHasKey('investment_preparation', $view['prefill']['simulator']);
|
||||
$personalKeys = ['person_type', 'investor_origin', 'tax_residency_country', 'fiscal_residency_country', 'entity_stage'];
|
||||
$this->postJson('/api/v1/simulation-drafts', ['context' => $id, 'personal_snapshot' => Arr::only($view['prefill']['simulator'], $personalKeys), 'economic_snapshot' => ['purpose' => 'simulate_only', ...Arr::except($view['prefill']['simulator'], $personalKeys)]], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated();
|
||||
$plan = $this->save($id, ['project_concession' => 'yes', 'implementation_mode' => 'foreign_representation', 'site_tenure' => 'duat']);
|
||||
$this->assertFalse($plan['calculation_inputs_changed'], 'Procedural changes do not change the inputs of the existing IVA calculation.');
|
||||
$this->assertContains('S06', $plan['review_steps']);
|
||||
$after = $plan['private_investment']['after'];
|
||||
$this->assertSame('authorization', $after['mode']);
|
||||
$this->assertSame('Conselho de Ministros', $after['authority']);
|
||||
foreach (['feasibility', 'financial_capacity', 'representation_license', 'name_reservation', 'duat'] as $key) {
|
||||
$this->assertTrue(collect($after['requirements'])->firstWhere('key', $key)['required']);
|
||||
}
|
||||
$lease = collect($after['requirements'])->firstWhere('key', 'lease');
|
||||
$this->assertSame('not_applicable', $lease['state']);
|
||||
$this->assertSame($doc, $lease['document_id']);
|
||||
$this->assertSame(1, DB::table('desk_documents')->where('case_id', $id)->count());
|
||||
$back = $this->save($id, ['project_concession' => 'no', 'implementation_mode' => 'domestic_entity', 'site_tenure' => 'lease', 'entity_stage' => 'existing']);
|
||||
$this->assertTrue(collect($back['private_investment']['after']['requirements'])->firstWhere('key', 'registration')['required']);
|
||||
$this->assertFalse(collect($back['private_investment']['after']['requirements'])->firstWhere('key', 'name_reservation')['required']);
|
||||
$this->assertSame('associated', collect($back['private_investment']['after']['requirements'])->firstWhere('key', 'lease')['state']);
|
||||
$this->assertSame('not_applicable', collect($back['private_investment']['after']['requirements'])->firstWhere('key', 'feasibility')['state']);
|
||||
$events = DB::table('desk_events')->where('case_id', $id)->where('action', 'investment_context_changed')->count();
|
||||
$this->assertSame(3, $events);
|
||||
$this->getJson('/investimentos/'.$id)->assertOk()->assertJsonPath('private_investment.mode', 'registration');
|
||||
$this->actingAs(User::factory()->create())->getJson('/investimentos/'.$id.'/contexto')->assertForbidden();
|
||||
$this->actingAs($owner)->getJson('/investimentos/'.$id.'/contexto')->assertOk()->assertJsonPath('private_investment.mode', 'registration');
|
||||
config(['investor-path.private_preparation' => false]);
|
||||
$this->save($id, ['project_title' => 'Edição independente durante suspensão']);
|
||||
$this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->assertJsonPath('private_investment', null)->assertJsonPath('context.preparation_snapshot.mode', 'registration')->assertJsonPath('context.preparation_documents.lease', $doc);
|
||||
}
|
||||
|
||||
public function test_foreign_document_and_tampered_preview_cannot_be_committed_and_disabled_feature_does_not_accept_writes(): void
|
||||
{
|
||||
$owner = User::factory()->create();
|
||||
$id = $this->dossier($owner);
|
||||
$view = $this->getJson('/investimentos/'.$id.'/contexto')->json();
|
||||
$body = ['version' => $view['case']['version'], 'selection' => [], 'declarations' => $this->declarations()];
|
||||
$this->postJson('/investimentos/'.$id.'/contexto/previa', [...$body, 'document_links' => ['identity' => (string) Str::uuid()]])->assertUnprocessable();
|
||||
$other = $this->dossier($owner);
|
||||
$foreignDocument = (string) Str::uuid();
|
||||
DB::table('documents')->insert(['id' => $foreignDocument, 'file_name' => 'OUTRO-DOSSIER-SINTETICO-QA.txt', 'media_type' => 'text/plain', 'storage_key' => 'synthetic-no-download', 'sha256' => hash('sha256', 'other synthetic'), 'status' => 'quarantined']);
|
||||
DB::table('desk_documents')->insert(['id' => (string) Str::uuid(), 'case_id' => $other, 'document_id' => $foreignDocument, 'category' => 'support', 'source_reference' => 'Synthetic isolated QA', 'linked_by' => $owner->id]);
|
||||
$this->postJson('/investimentos/'.$id.'/contexto/previa', [...$body, 'document_links' => ['identity' => $foreignDocument]])->assertUnprocessable();
|
||||
$plan = $this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertOk()->json();
|
||||
$this->postJson('/investimentos/'.$id.'/contexto/confirmar', [...$body, 'declarations' => [...$body['declarations'], 'currency' => 'USD'], 'preview_hash' => $plan['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true])->assertConflict();
|
||||
$this->getJson('/investimentos/'.$id.'/contexto')->assertJsonPath('context.revision', 0);
|
||||
config(['investor-path.private_preparation' => false]);
|
||||
$this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertConflict();
|
||||
$this->postJson('/investimentos/'.$id.'/contexto/previa', [...$body, 'declarations' => 'invalid'])->assertUnprocessable();
|
||||
$this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->assertJsonPath('private_investment', null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\Malware\MalwareScanner;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProcurementTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
private User $editor;
|
||||
|
||||
private string $organization;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$identity = DB::selectOne('select current_database() as database,inet_server_port() as port');
|
||||
$this->assertSame('simulator_fiscal_delta_20261021_test', $identity->database);
|
||||
$this->assertSame(55441, (int) $identity->port);
|
||||
$this->withoutVite();
|
||||
$this->editor = User::factory()->create();
|
||||
config(['procurement.enabled' => true, 'portal.managers' => [$this->editor->email]]);
|
||||
$this->organization = (string) Str::uuid();
|
||||
DB::table('organizations')->insert(['id' => $this->organization, 'name' => 'SINTÉTICO QA — Entidade de ensaio', 'organization_type' => 'private']);
|
||||
$this->actingAs($this->editor);
|
||||
}
|
||||
|
||||
private function payload(string $regime = 'state'): array
|
||||
{
|
||||
return ['title' => 'SINTÉTICO QA — '.$regime, 'reference' => 'QA-'.Str::uuid(), 'organization_id' => $this->organization,
|
||||
'regime' => $regime, 'modality' => 'Modalidade documentada de ensaio', 'object' => 'Objecto sintético de concurso para ensaio QA; não corresponde a contratação real.',
|
||||
'instrument' => $regime === 'state' ? 'Decreto n.º 79/2022 — referência do âmbito estatal' : 'Regras documentadas do procedimento sintético',
|
||||
'edition' => $regime === 'external' ? 'Banco Mundial, Setembro/2025 — exemplar de referência' : 'QA-2026',
|
||||
'rule_source' => $regime === 'state' ? 'https://www.ufsa.gov.mz/divnovoreg.php' : 'https://example.invalid/qa-rules',
|
||||
'origin_published_at' => '2026-10-20T09:00', 'deadline' => '2026-11-20T15:00', 'timezone' => 'Africa/Maputo',
|
||||
'delivery_channel' => 'Canal externo documentado', 'delivery_address' => 'Endereço sintético para ensaio; não enviar propostas.',
|
||||
'public_contact' => 'contacto-interno-que-nao-deve-sair@example.test', 'contact_authorized' => false, 'internal_notes' => 'SEGREDO-INTERNO-QA',
|
||||
'public_reason' => 'Divulgação de anúncio sintético para ensaio QA, sem contratação real.',
|
||||
'lots' => [['code' => 'L1', 'description' => 'Lote de bens sintéticos', 'quantities' => '10 unidades de ensaio']], 'attachments' => []];
|
||||
}
|
||||
|
||||
private function create(array $payload): string
|
||||
{
|
||||
$response = $this->post('/management/concursos', ['version' => 0, 'reason' => 'Preparação sintética documentada para ensaio QA.', 'draft' => $payload])->assertSessionHasNoErrors()->assertRedirect();
|
||||
|
||||
return basename(parse_url($response->headers->get('Location'), PHP_URL_PATH));
|
||||
}
|
||||
|
||||
private function save(string $id, array $payload): void
|
||||
{
|
||||
$this->put('/management/concursos/'.$id, ['version' => DB::table('portal_tenders')->where('id', $id)->value('version'), 'reason' => 'Actualização sintética de ensaio com histórico conservado.', 'draft' => $payload])->assertRedirect()->assertSessionHasNoErrors();
|
||||
}
|
||||
|
||||
private function publish(string $id, string $act = 'initial'): array
|
||||
{
|
||||
$plan = $this->postJson('/management/concursos/'.$id.'/previa', ['act' => $act])->assertOk()->json();
|
||||
$this->post('/management/concursos/'.$id.'/publicar', ['act' => $act, 'hash' => $plan['hash'], 'reason' => $plan['payload']['public_reason']])->assertRedirect()->assertSessionHasNoErrors();
|
||||
|
||||
return $plan;
|
||||
}
|
||||
|
||||
public function test_incomplete_draft_recovers_without_becoming_public_and_rejects_stale_save(): void
|
||||
{
|
||||
$id = $this->create(['title' => 'Preparação incompleta']);
|
||||
$this->get('/management/concursos/'.$id)->assertOk()->assertHeader('Cache-Control', 'no-store, private')->assertInertia(fn (Assert $p) => $p->component('procurement/edit')->where('record.draft.title', 'Preparação incompleta'));
|
||||
$this->get('/concursos/'.$id)->assertNotFound();
|
||||
$this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'initial'])->assertUnprocessable()->assertJsonValidationErrors(['organization_id', 'deadline', 'regime']);
|
||||
$this->putJson('/management/concursos/'.$id, ['version' => 0, 'reason' => 'Pedido concorrente obsoleto, sem sobrescrever o rascunho.', 'draft' => ['title' => 'Não guardar']])->assertConflict();
|
||||
$this->assertSame(1, DB::table('portal_tender_versions')->where('tender_id', $id)->count());
|
||||
}
|
||||
|
||||
public function test_state_external_private_regimes_and_filters_are_public_without_ug_ea_or_guarantee_defaults(): void
|
||||
{
|
||||
$ids = [];
|
||||
foreach (['state', 'external', 'private'] as $regime) {
|
||||
$p = $this->payload($regime);
|
||||
$ids[$regime] = $this->create($p);
|
||||
$plan = $this->publish($ids[$regime]);
|
||||
$this->assertArrayNotHasKey('responsible_unit', $plan['payload']);
|
||||
$this->assertArrayNotHasKey('internal_notes', $plan['payload']);
|
||||
$this->assertArrayNotHasKey('public_contact', $plan['payload']);
|
||||
}
|
||||
Auth::logout();
|
||||
$this->get('/concursos?regime=external&entity='.$this->organization.'&q=SINT')->assertOk()->assertInertia(fn (Assert $p) => $p->component('procurement/index')->where('filters.regime', 'external')->has('items.data', 1)->where('items.data.0.id', $ids['external']));
|
||||
$this->get('/concursos?q=&entity=§or=&state=open')->assertOk()->assertInertia(fn (Assert $p) => $p->where('filters', ['state' => 'open']));
|
||||
$this->get('/concursos/'.$ids['private'].'?regime=private&q=SINT&page=2')->assertOk()->assertInertia(fn (Assert $p) => $p->component('procurement/show')->where('backUrl', '/concursos?q=SINT®ime=private&page=2')->where('publication.payload.regime', 'private')->missing('publication.payload.internal_notes')->missing('publication.payload.public_contact')->has('history', 1));
|
||||
$this->get('/concursos/'.$ids['state'].'/imprimir')->assertOk()->assertInertia(fn (Assert $p) => $p->where('printMode', true));
|
||||
}
|
||||
|
||||
public function test_extension_preserves_publication_origin_and_history_and_expiry_is_not_an_award(): void
|
||||
{
|
||||
$p = $this->payload();
|
||||
$id = $this->create($p);
|
||||
$this->publish($id);
|
||||
$first = DB::table('portal_tender_versions')->where('tender_id', $id)->where('act', 'initial')->first();
|
||||
$p['deadline'] = '2026-11-25T16:00';
|
||||
$this->save($id, $p);
|
||||
$this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'correction'])->assertUnprocessable()->assertJsonValidationErrors('act');
|
||||
$plan = $this->publish($id, 'extension');
|
||||
$this->assertSame('2026-11-20T15:00', $plan['payload']['deadline_previous']);
|
||||
$this->assertSame('2026-10-20T09:00', $plan['payload']['origin_published_at']);
|
||||
$this->assertSame($first->payload, DB::table('portal_tender_versions')->where('id', $first->id)->value('payload'));
|
||||
$this->travelTo(now()->setDate(2026, 11, 26));
|
||||
$this->get('/concursos/'.$id)->assertInertia(fn (Assert $p) => $p->where('state', 'deadline_ended')->where('publication.act', 'extension')->has('history', 2));
|
||||
$this->travelBack();
|
||||
$p['result'] = 'Resultado sintético de ensaio, não adjudicação real.';
|
||||
$this->save($id, $p);
|
||||
$this->publish($id, 'award');
|
||||
$this->get('/concursos/'.$id)->assertInertia(fn (Assert $p) => $p->where('state', 'results')->has('history', 3));
|
||||
}
|
||||
|
||||
public function test_cancellation_and_later_clarification_do_not_reopen_the_procedure(): void
|
||||
{
|
||||
$p = $this->payload('private');
|
||||
$id = $this->create($p);
|
||||
$this->publish($id);
|
||||
$p['result'] = 'Cancelamento sintético fundamentado para ensaio.';
|
||||
$this->save($id, $p);
|
||||
$this->publish($id, 'cancellation');
|
||||
$p['object'] .= ' Esclarecimento posterior ao cancelamento.';
|
||||
$this->save($id, $p);
|
||||
$this->publish($id, 'clarification');
|
||||
$this->get('/concursos/'.$id)->assertInertia(fn (Assert $p) => $p->where('state', 'cancelled')->has('history', 3));
|
||||
}
|
||||
|
||||
public function test_stale_preview_cannot_publish_new_draft_and_public_snapshot_stays_unchanged_until_explicit_publication(): void
|
||||
{
|
||||
$p = $this->payload();
|
||||
$id = $this->create($p);
|
||||
$plan = $this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'initial'])->assertOk()->json();
|
||||
$p['title'] = 'Nova edição ainda privada';
|
||||
$this->save($id, $p);
|
||||
$this->postJson('/management/concursos/'.$id.'/publicar', ['act' => 'initial', 'hash' => $plan['hash'], 'reason' => $p['public_reason']])->assertConflict();
|
||||
$this->publish($id);
|
||||
$p['title'] = 'Rascunho futuro privado';
|
||||
$this->save($id, $p);
|
||||
$this->get('/concursos/'.$id)->assertInertia(fn (Assert $p) => $p->where('publication.payload.title', 'Nova edição ainda privada'));
|
||||
}
|
||||
|
||||
public function test_documented_minimum_dates_and_business_calendar_are_specific_to_the_procedure(): void
|
||||
{
|
||||
$p = $this->payload();
|
||||
$p['deadline'] = '2026-10-19T15:00';
|
||||
$id = $this->create($p);
|
||||
$this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'initial'])->assertUnprocessable()->assertJsonValidationErrors('deadline');
|
||||
$p['origin_published_at'] = '2026-10-23T09:00';
|
||||
$p['deadline'] = '2026-10-27T09:00';
|
||||
$p += ['minimum_days' => 2, 'minimum_source' => 'Regra sintética de contagem, cláusula QA-1', 'minimum_edition' => 'QA-2026', 'minimum_modality' => $p['modality'], 'minimum_day_basis' => 'business', 'minimum_working_days' => '1,2,3,4,5', 'minimum_holidays' => '2026-10-26', 'minimum_calendar_source' => 'Calendário sintético QA: segunda a sexta, exclui 26/10. Não é calendário oficial.'];
|
||||
$this->save($id, $p);
|
||||
$this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'initial'])->assertUnprocessable()->assertJsonValidationErrors('deadline');
|
||||
$p['deadline'] = '2026-10-28T09:00';
|
||||
$this->save($id, $p);
|
||||
$this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'initial'])->assertOk();
|
||||
$p['minimum_modality'] = 'Outra modalidade';
|
||||
$this->save($id, $p);
|
||||
$this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'initial'])->assertUnprocessable()->assertJsonValidationErrors('minimum_modality');
|
||||
}
|
||||
|
||||
public function test_simple_account_can_follow_but_cannot_edit_or_publish_and_tracking_is_not_a_bid(): void
|
||||
{
|
||||
$id = $this->create($this->payload());
|
||||
$this->publish($id);
|
||||
$reader = User::factory()->unverified()->create();
|
||||
$before = DB::table('portal_role_assignments')->count();
|
||||
$this->actingAs($reader)->put('/concursos/'.$id.'/acompanhar')->assertRedirect();
|
||||
$this->get('/concursos/acompanhar')->assertOk()->assertInertia(fn (Assert $p) => $p->where('watched', true)->has('items', 1)->where('items.0.changed', false));
|
||||
$this->actingAs($this->editor);
|
||||
$p = $this->payload();
|
||||
$this->save($id, $p);
|
||||
$this->publish($id, 'clarification');
|
||||
$reader->forceFill(['email_verified_at' => now()])->save();
|
||||
$this->actingAs($reader);
|
||||
$this->get('/concursos/acompanhar')->assertInertia(fn (Assert $p) => $p->where('items.0.changed', true));
|
||||
$this->get('/management/concursos')->assertForbidden();
|
||||
$this->postJson('/management/concursos/'.$id.'/publicar', [])->assertForbidden();
|
||||
$this->delete('/concursos/'.$id.'/acompanhar')->assertRedirect();
|
||||
$this->assertSame($before, DB::table('portal_role_assignments')->count());
|
||||
}
|
||||
|
||||
public function test_real_scanned_library_attachment_private_redaction_and_withdrawal_are_reused(): void
|
||||
{
|
||||
Storage::fake('local');
|
||||
config(['queue.default' => 'sync', 'services.malware.scanner' => 'clamd', 'services.malware.socket' => null, 'services.malware.host' => '127.0.0.1', 'services.malware.port' => 3310]);
|
||||
$this->assertTrue(app(MalwareScanner::class)->health()['available'], 'Current real scanner required');
|
||||
$name = 'synthetic-tender-'.Str::uuid().'.txt';
|
||||
$this->post('/management/portal-services/upload', ['file' => UploadedFile::fake()->createWithContent($name, 'Exemplo sintético de anúncio QA, sem valor jurídico.')])->assertSessionHasNoErrors();
|
||||
$doc = DB::table('documents')->where('file_name', $name)->first();
|
||||
$this->assertSame('clean', $doc->malware_scan_status);
|
||||
$metadata = ['document_id' => $doc->id, 'title' => $name, 'category' => 'Synthetic QA', 'topic' => 'Tender QA', 'document_type' => 'unknown', 'issuer' => 'Synthetic QA only', 'source_url' => 'https://example.invalid/qa', 'language' => 'pt', 'version_label' => 'QA-1', 'territorial_scope' => 'national', 'source_confirmation' => 'identified', 'legal_status' => 'unknown', 'rights_status' => 'authorized', 'data_classification' => 'public', 'source_reference' => 'Synthetic QA only, not an official notice.', 'legislation' => false, 'essential' => false, 'independent_review' => false, 'associations' => []];
|
||||
$this->post('/management/portal-services/resources', $metadata)->assertSessionHasNoErrors();
|
||||
$resource = DB::table('portal_resources')->where('document_id', $doc->id)->first();
|
||||
$this->assertNotNull($resource);
|
||||
$p = $this->payload('external');
|
||||
$p['attachments'] = [['resource_id' => $resource->id, 'public' => true, 'purpose' => 'Anúncio de ensaio']];
|
||||
$id = $this->create($p);
|
||||
$this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'initial'])->assertUnprocessable()->assertJsonValidationErrors('attachments');
|
||||
$this->post('/management/portal-services/resources/'.$resource->id, ['published' => true])->assertSessionHasNoErrors();
|
||||
$this->publish($id);
|
||||
Auth::logout();
|
||||
$this->get('/documentos/'.$resource->id.'/download')->assertOk()->assertDownload($name);
|
||||
$this->get('/concursos/'.$id)->assertInertia(fn (Assert $p) => $p->where('publication.payload.attachments.0.available', true));
|
||||
$this->actingAs($this->editor)->post('/management/portal-services/resources/'.$resource->id, ['published' => false])->assertSessionHasNoErrors();
|
||||
Auth::logout();
|
||||
$this->get('/documentos/'.$resource->id.'/download')->assertNotFound();
|
||||
$this->get('/concursos/'.$id)->assertInertia(fn (Assert $p) => $p->where('publication.payload.attachments.0.available', false));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\TerritoryGeometryImport;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TerritoryGeometryApprovedAdministrationTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
public function test_existing_superuser_with_completed_mfa_reaches_importer_without_legacy_manager_grant(): void
|
||||
{
|
||||
$actor = $this->holder();
|
||||
$this->actingAs($actor)->withSession(['portal.mfa_actor' => $actor->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $actor->portal_access_revision]);
|
||||
$this->assertFalse($actor->can('manage-portal'));
|
||||
// A missing exemplar must produce the source error, not deny the approved actor.
|
||||
try {
|
||||
app(TerritoryGeometryImport::class)->preview($actor, 1);
|
||||
$this->fail('The deliberately absent source must be reported.');
|
||||
} catch (HttpException $exception) {
|
||||
$this->assertSame(422, $exception->getStatusCode());
|
||||
}
|
||||
$this->getJson('/management/territory-geometry-import')->assertStatus(422);
|
||||
$this->postJson('/management/territory-geometry-import', [])->assertStatus(422);
|
||||
$this->assertFalse($actor->can('manage-portal'));
|
||||
}
|
||||
|
||||
public function test_superuser_without_mfa_session_cannot_import(): void
|
||||
{
|
||||
$actor = $this->holder();
|
||||
$this->actingAs($actor)->getJson('/management/territory-geometry-import')->assertForbidden();
|
||||
$this->postJson('/management/territory-geometry-import', [])->assertForbidden();
|
||||
}
|
||||
|
||||
public function test_ordinary_account_does_not_gain_cartographic_administration(): void
|
||||
{
|
||||
$this->holder();
|
||||
$actor = User::factory()->create();
|
||||
$this->actingAs($actor)->getJson('/management/territory-geometry-import')->assertForbidden();
|
||||
$this->postJson('/management/territory-geometry-import', [])->assertForbidden();
|
||||
}
|
||||
|
||||
private function holder(): User
|
||||
{
|
||||
$identity = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||
$this->assertSame('simulator_fiscal_delta_20261021_test', $identity->name);
|
||||
$this->assertSame(55441, (int) $identity->port);
|
||||
config(['portal.profile_access_enabled' => true, 'portal.managers' => [], 'geometry-import.enabled' => true,
|
||||
'geometry-import.directory' => storage_path('app/private/nonexistent-demarcation-source')]);
|
||||
$actor = User::factory()->create(['two_factor_secret' => Crypt::encryptString('JBSWY3DPEHPK3PXP'), 'two_factor_confirmed_at' => now()]);
|
||||
$other = User::factory()->create();
|
||||
DB::table('portal_superuser_pair')->updateOrInsert(['id' => 1], ['first_user_id' => $actor->id, 'second_user_id' => $other->id, 'approved_by' => $other->id]);
|
||||
return $actor;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,11 @@
|
||||
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
test('legacy draft without a compatible source keeps its shape and identity',()=>{
|
||||
const legacy={activity_id:'legacy-activity',infrastructure_needs:['energia']};
|
||||
assert.equal(inheritSimulatorFields(legacy),legacy);
|
||||
assert.equal(Object.hasOwn(inheritSimulatorFields(legacy),'compatible_fields_snapshot'),false);
|
||||
});
|
||||
import test from 'node:test';
|
||||
import { inheritCompatibleFields, inheritSimulatorFields } from '../../resources/js/lib/context-inheritance.ts';
|
||||
import { serializeBusinessPlanSnapshot } from '../../resources/js/lib/business-financial-plan.ts';
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createProjectDossierReport } from '../../resources/js/lib/project-dossier-report.ts';
|
||||
|
||||
test('report retains the frozen preparation and an inactive historical document without claiming approval or a tax benefit', () => {
|
||||
const preparation = {enabled:true,version:'private-investment-preparation-2026-v1',mode:'registration',next:'Preparar documentos do pedido de mero registo',calculation_notice:'O regime procedimental não altera a taxa de IVA.',notice:'Sem submissão à Administração.',requirements:[{label:'Estudo de viabilidade',state:'not_applicable',document_id:'synthetic-old-document',source:{reference:'R14',url:'https://example.test/official-source'},reason:'Apenas para autorização.'}]};
|
||||
const snapshot = JSON.stringify({version:1,context_revision:3,preparation});
|
||||
const report = createProjectDossierReport({context_applicability_snapshot:snapshot},{selectedSections:new Set(['sources'])});
|
||||
const sources=report.sections.find(s=>s.id==='sources');
|
||||
assert.equal(sources.rows.find(r=>r.field==='Preparação do investimento privado').value,'Pedido de mero registo');
|
||||
assert.match(sources.rows.find(r=>r.field==='Estudo de viabilidade').value,/Não aplicável; histórico preservado/);
|
||||
assert.ok(sources.paragraphs.includes(preparation.calculation_notice));
|
||||
preparation.mode='authorization';
|
||||
assert.equal(JSON.parse(snapshot).preparation.mode,'registration');
|
||||
});
|
||||
@@ -0,0 +1,27 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { searchTerritoryHref } from '../../resources/js/lib/search-territory.ts';
|
||||
|
||||
test('portfolio to catalogue to related screen retains only territorial search filters', () => {
|
||||
const start = '/carteira?province_id=inh&district_id=vil&q=airport&kind=project';
|
||||
const catalogue = searchTerritoryHref('/oportunidades?view=grid', start);
|
||||
assert.equal(catalogue, '/oportunidades?view=grid&province_id=inh&district_id=vil');
|
||||
const detail = searchTerritoryHref('/carteira/id', catalogue);
|
||||
assert.equal(searchTerritoryHref('/carteira', detail), '/carteira?province_id=inh&district_id=vil');
|
||||
assert.equal(searchTerritoryHref('/zonas?category=zee', start), '/zonas?category=zee&province_id=inh&district_id=vil');
|
||||
});
|
||||
test('nested historical return query restores territory without importing unrelated filters', () => {
|
||||
assert.equal(searchTerritoryHref('/oportunidades', '/carteira/id?catalogue_query='+encodeURIComponent('province_id=inh&district_id=vil&q=airport')), '/oportunidades?province_id=inh&district_id=vil');
|
||||
});
|
||||
test('explicit parent changes or clearing never import incompatible descendants', () => {
|
||||
const current = '/oportunidades?province_id=inh&district_id=vil&admin_post_id=post&locality_id=loc';
|
||||
assert.equal(searchTerritoryHref('/carteira?province_id=cabo', current), '/carteira?province_id=cabo');
|
||||
assert.equal(searchTerritoryHref('/carteira?district_id=other', current), '/carteira?district_id=other&province_id=inh');
|
||||
assert.equal(searchTerritoryHref('/carteira?province_id=', current), '/carteira?province_id=');
|
||||
});
|
||||
test('no cross-tab, project, account, external or unrelated route inheritance', () => {
|
||||
assert.equal(searchTerritoryHref('/oportunidades','/carteira'), '/oportunidades');
|
||||
assert.equal(searchTerritoryHref('/oportunidades','/investimentos/id?province_id=private'), '/oportunidades');
|
||||
assert.equal(searchTerritoryHref('/investimentos/id','/carteira?province_id=inh'), '/investimentos/id');
|
||||
assert.equal(searchTerritoryHref('https://other.test/carteira','/carteira?province_id=inh'), 'https://other.test/carteira');
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
"""Reconcile the 50 captured identities; public GETs only, no DB/publication writes."""
|
||||
from pathlib import Path
|
||||
from html.parser import HTMLParser
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
import collections, csv, datetime, hashlib, json, re, subprocess, sys, urllib.request
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT = ROOT / 'outputs/legacy-correspondence-20261022'
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
AUDIT = ROOT / 'docs/investmoz/v2/auditoria-portal'
|
||||
reconciliation = json.loads((ROOT / 'outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json').read_text(encoding='utf-8'))
|
||||
inventory = json.loads((ROOT / 'storage/app/private/investmoz-v2/portal-audit/legacy-discovery/inventory.json').read_text(encoding='utf-8'))
|
||||
config = json.loads(subprocess.check_output(['php', '-r', "$c=require 'config/legacy_content.php';echo json_encode($c);"], cwd=ROOT))
|
||||
mapped = {int(re.search(r'post-(\d+)-', item['external_id'])[1]): (slug, item) for slug, item in config['articles'].items() if item['publishable']}
|
||||
assert len(mapped) == 20 and len(reconciliation['articles']) == 50
|
||||
|
||||
class Page(HTMLParser):
|
||||
data = None
|
||||
reading = False
|
||||
chunks = None
|
||||
def handle_starttag(self, tag, attrs):
|
||||
attrs = dict(attrs)
|
||||
if tag == 'script' and attrs.get('type') == 'application/json' and 'data-page' in attrs:
|
||||
self.reading = True; self.chunks = []
|
||||
elif 'data-page' in attrs and attrs['data-page'].startswith('{'):
|
||||
self.data = json.loads(attrs['data-page'])
|
||||
def handle_data(self, value):
|
||||
if self.reading: self.chunks.append(value)
|
||||
def handle_endtag(self, tag):
|
||||
if tag == 'script' and self.reading:
|
||||
self.data = json.loads(''.join(self.chunks)); self.reading = False
|
||||
|
||||
def fetch(pair):
|
||||
legacy_id, (slug, identity) = pair
|
||||
url = 'https://teste-investidor.tempo.co.mz/' + slug
|
||||
try:
|
||||
request = urllib.request.Request(url, headers={'User-Agent': 'Mozilla/5.0 InvesteMoz-Legacy-Correspondence', 'Cache-Control': 'no-cache'})
|
||||
with urllib.request.urlopen(request, timeout=35) as response:
|
||||
raw = response.read(); status = response.status
|
||||
page = Page(); page.feed(raw.decode('utf-8'))
|
||||
article = page.data['props']['article']
|
||||
assert page.data['component'] == 'portal/legacy' and article['legacy_id'] == legacy_id
|
||||
assert article['source_url'] == identity['source_url'] and article['html'].strip()
|
||||
return legacy_id, {'url': url, 'status': status, 'identity_verified': True,
|
||||
'resource_id': article['id'], 'document_sha256': article['sha256'],
|
||||
'current_title': article['title'], 'image_url': article['image']['href'] if article.get('image') else None,
|
||||
'response_sha256': hashlib.sha256(raw).hexdigest()}
|
||||
except Exception as error:
|
||||
return legacy_id, {'url': url, 'identity_verified': False, 'error': type(error).__name__, 'status': getattr(error, 'code', None)}
|
||||
|
||||
if '--reuse-http' in sys.argv:
|
||||
cached = json.loads((OUT / 'public-pages.json').read_text(encoding='utf-8'))
|
||||
checks = {int(k): v for k, v in cached['pages'].items()}; stamp = cached['checked_utc']
|
||||
assert set(checks) == set(mapped)
|
||||
else:
|
||||
with ThreadPoolExecutor(max_workers=4) as pool:
|
||||
checks = dict(pool.map(fetch, mapped.items()))
|
||||
stamp = datetime.datetime.now(datetime.timezone.utc).isoformat()
|
||||
(OUT / 'public-pages.json').write_text(json.dumps({'checked_utc': stamp, 'method': 'Anonymous HTTP GET; Inertia article identity/source/content checked; no visual acceptance or new publication', 'pages': checks}, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
|
||||
captures = {}
|
||||
for record in inventory['records']:
|
||||
post = record.get('public_props', {}).get('post')
|
||||
if post:
|
||||
captures.setdefault(int(post['id']), []).append(record)
|
||||
staged = collections.defaultdict(list)
|
||||
prepared = {}
|
||||
for article in reconciliation['articles']:
|
||||
legacy_id = article['legacy_id']
|
||||
if legacy_id in mapped or article['group'] in ['mercado_financeiro', 'identidade_incompleta']:
|
||||
continue
|
||||
record = next(r for r in captures[legacy_id] if hashlib.sha256(r['public_props']['post']['content'].encode()).hexdigest() == article['body_sha256'])
|
||||
original = ROOT / record['private_file']
|
||||
assert original.is_file() and hashlib.sha256(original.read_bytes()).hexdigest() == record['sha256']
|
||||
post = record['public_props']['post']; seo = record['public_props']['seo']
|
||||
group = article['group']; directory = OUT / 'private-preparation' / group; directory.mkdir(parents=True, exist_ok=True)
|
||||
payload = {'format': 'investmoz-legacy-article-v1', 'legacy_id': legacy_id, 'title': article['title'],
|
||||
'author': seo.get('author', 'tempo'), 'legacy_category': article['category'],
|
||||
'source_url': record['url'], 'published_at': seo.get('publishedTime', ''),
|
||||
'modified_at': article['modified_at'], 'content_html': post['content'],
|
||||
'image_source_url': post.get('thumbnail'), 'image_sha256': None,
|
||||
'references': article['references']}
|
||||
data = (json.dumps(payload, ensure_ascii=False, indent=2) + '\n').encode()
|
||||
filename = f"post-{legacy_id}-{article['modified_at'][:10].replace('-', '')}.txt"
|
||||
path = directory / filename
|
||||
if path.exists():
|
||||
assert path.read_bytes() == data, 'Prepared version changed; preserve prior bytes'
|
||||
else:
|
||||
path.write_bytes(data)
|
||||
item = {'legacy_id': legacy_id, 'title': article['title'], 'source_url': record['url'],
|
||||
'external_id': f"legacy-tempo-post-{legacy_id}-{article['modified_at'][:10].replace('-', '')}",
|
||||
'file_name': filename, 'relative_path': f'{group}/{filename}',
|
||||
'sha256': hashlib.sha256(data).hexdigest(), 'source_capture_sha256': record['sha256'],
|
||||
'original_body_sha256': article['body_sha256'], 'bytes': len(data), 'published': False,
|
||||
'scan': 'not_run_on_prepared_derivative', 'dependencies': article['dependencies'],
|
||||
'note': 'Private preparation only. Not imported, scanned, published or assigned to a current opportunity. Image unresolved, never substituted by generic image.'}
|
||||
staged[group].append(item); prepared[legacy_id] = item
|
||||
assert len(prepared) == 28
|
||||
qa_receipt_path = OUT / 'qa-import-receipt.json'
|
||||
qa_rows = {}
|
||||
if qa_receipt_path.exists():
|
||||
proof = json.loads(qa_receipt_path.read_text(encoding='utf-8'))
|
||||
assert proof['qa_database'] == 'simulator_fiscal_delta_20261021_test' and proof['port'] == 55441
|
||||
assert proof['imported_private'] == proof['clean_actual_scans'] == proof['idempotent_replay_reused'] == 28
|
||||
qa_rows = {int(re.search(r'post-(\d+)-', r['external_id'])[1]): r for r in proof['resources']}
|
||||
assert set(qa_rows) == set(prepared)
|
||||
assert all(not r['published'] and r['malware_scan_status'] == 'clean' and r['sha256'] == prepared[i]['sha256'] for i, r in qa_rows.items())
|
||||
for group, items in staged.items():
|
||||
(OUT / 'private-preparation' / group / 'preparation.json').write_text(json.dumps({'importable_manifest': False, 'group': group, 'items': items}, ensure_ascii=False, indent=2), encoding='utf-8')
|
||||
|
||||
path = AUDIT / 'legado-conteudos-reconciliados.csv'
|
||||
with path.open(encoding='utf-8-sig', newline='') as f:
|
||||
reader = csv.DictReader(f); columns = reader.fieldnames; rows = list(reader)
|
||||
editorial_receipt = AUDIT / 'legado-editorial-qa-20261022.json'
|
||||
editorial_rows = {}
|
||||
if editorial_receipt.exists():
|
||||
editorial = json.loads(editorial_receipt.read_text(encoding='utf-8'))
|
||||
assert editorial['qa_only'] and not editorial['destination_imported'] and not editorial['destination_published']
|
||||
editorial_rows = {int(re.search(r'post-(\d+)-', r['external_id'])[1]): r for r in editorial['rows']}
|
||||
assert len(editorial_rows) == 28 and all(r['private'] for r in editorial_rows.values())
|
||||
extra = ['Endereço no novo portal', 'Estado actual consolidado', 'Pendência actual', 'Conferência HTTP UTC', 'Grupo de preparação QA']
|
||||
for col in extra:
|
||||
if col not in columns: columns.append(col)
|
||||
articles = {a['legacy_id']: a for a in reconciliation['articles']}
|
||||
for row in rows:
|
||||
legacy_id = int(row['ID legado']); article = articles[legacy_id]; check = checks.get(legacy_id)
|
||||
row['Endereço no novo portal'] = check['url'] if check else ''
|
||||
row['Conferência HTTP UTC'] = stamp if check else ''
|
||||
row['Grupo de preparação QA'] = article['group'] if legacy_id in prepared else ''
|
||||
if check:
|
||||
row['Estado actual consolidado'] = 'Publicado; identidade e conteúdo consultáveis na homologação' if check['identity_verified'] else 'Publicação documentada; consulta actual não confirmada'
|
||||
row['Pendência actual'] = 'Não foi feita aceitação visual ou conferência pela Tempo nesta passagem.' if check['identity_verified'] else str(check)
|
||||
if check['identity_verified']:
|
||||
row['Situação'] = 'publicado'; row['Recurso ativo'] = str(check['resource_id'])
|
||||
row['Grupo restante'] = 'publicados'
|
||||
row['Dependência concreta'] = 'Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente.'
|
||||
row['Próxima ação'] = 'Preservar a publicação; gerir edições e versões pela biblioteca existente.'
|
||||
row['Imagem atual'] = json.dumps({'scope': 'homologação', 'href': check['image_url'], 'status': 'referência na ficha; binário não reensaiado'}, ensure_ascii=False)
|
||||
elif legacy_id in prepared:
|
||||
row['Estado actual consolidado'] = 'Importado privado em QA; scan real limpo; por integrar na homologação' if legacy_id in qa_rows else 'Por integrar; exemplar derivado preparado privadamente, sem importação'
|
||||
if legacy_id in qa_rows: row['Recurso QA'] = str(qa_rows[legacy_id]['id'])
|
||||
row['Pendência actual'] = 'Fechar edição/destino de leitura e publicação por lote na homologação. Dependências históricas de 15/10, por confrontar com o estado actual (não são novos bloqueios): ' + ' | '.join(article['dependencies'])
|
||||
if legacy_id in qa_rows:
|
||||
row['Dependência concreta'] = 'Rascunho QA analisado; falta fechar leitor/edição e entrega/publicação na homologação. Referências externas e imagens não recolhidas continuam discriminadas no inventário.'
|
||||
row['Próxima ação'] = 'Continuar por lote, preservando o carácter histórico/editorial; não converter em oportunidade vigente nem em regra fiscal.'
|
||||
elif article['group'] == 'mercado_financeiro':
|
||||
row['Estado actual consolidado'] = 'Original vazio'; row['Pendência actual'] = 'Fornecer corpo editorial pela Tempo; imagem e Fintech não substituem o original. Sem novo endereço público.'
|
||||
else:
|
||||
row['Estado actual consolidado'] = 'Original sem título'; row['Pendência actual'] = 'Identificação editorial pela Tempo para o post 74364; corpo com 21 caracteres úteis preservado. Sem título ou endereço novo inventado.'
|
||||
row['Evidência atual'] = 'outputs/legacy-correspondence-20261022/public-pages.json; qa-import-receipt.json; editorial-gaps-current.json; inventário histórico de 15/10 preservado'
|
||||
if legacy_id in editorial_rows:
|
||||
assert editorial_rows[legacy_id]['original_sha256'] == prepared[legacy_id]['sha256']
|
||||
row['Estado actual consolidado'] = 'Privado em QA; edição, leitura e prévia disponíveis; entrega privada preparada'
|
||||
row['Pendência actual'] = 'Revisão editorial pela equipa; integração Git e transporte privado autenticado para homologação; publicação separada pelo portal. Imagens não verificadas mantêm referência/aviso.'
|
||||
if legacy_id == 62413:
|
||||
row['Pendência actual'] += ' Rever/corrigir a nota de demonstração QA antes de publicar.'
|
||||
row['Dependência concreta'] = row['Pendência actual']
|
||||
row['Próxima ação'] = 'Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites.'
|
||||
row['Evidência atual'] = 'legado-editorial-qa-20261022.json; manual: Legado — edição e leitura dos 28 rascunhos em QA; inventário e referências históricas preservados'
|
||||
assert len(rows) == 50 and len({row['ID legado'] for row in rows}) == 50
|
||||
with path.open('w', encoding='utf-8-sig', newline='') as f:
|
||||
writer = csv.DictWriter(f, columns); writer.writeheader(); writer.writerows(rows)
|
||||
|
||||
def esc(value): return str(value).replace('|', '\\|').replace('\n', ' ')
|
||||
table = ['# Correspondência integral dos 50 artigos do legado', '',
|
||||
'Conferência HTTP individual em ' + stamp + '. Lista por identidade editorial; imagens, recursos da biblioteca e variantes de URL não são artigos adicionais. Não altera publicações. Endereço em branco significa que não foi demonstrada uma página nova, não uma ligação a criar por suposição.', '',
|
||||
'| ID | Título original | Endereço original | Endereço no novo portal | Estado | Pendência |', '| --- | --- | --- | --- | --- | --- |']
|
||||
for row in rows:
|
||||
table.append('| ' + ' | '.join(esc(row[k]) for k in ['ID legado', 'Título', 'URLs observadas', 'Endereço no novo portal', 'Estado actual consolidado', 'Pendência actual']) + ' |')
|
||||
(AUDIT / 'legado-correspondencia-integral.md').write_text('\n'.join(table) + '\n', encoding='utf-8')
|
||||
summary = {'checked_utc': stamp, 'distinct_articles': 50, 'publications_documented': 20,
|
||||
'public_pages_identity_verified': sum(p['identity_verified'] for p in checks.values()),
|
||||
'pending_content': 28, 'private_preparation': {k: len(v) for k, v in staged.items()},
|
||||
'empty': 1, 'untitled': 1, 'database_writes_by_correspondence_script': 0, 'active_database_writes': 0, 'new_publications': 0,
|
||||
'qa_imports': len(qa_rows), 'qa_clean_scans': len(qa_rows), 'remaining_integration_complete': False}
|
||||
(OUT / 'receipt.json').write_text(json.dumps(summary, indent=2), encoding='utf-8')
|
||||
print(json.dumps(summary))
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
// Read-only export from the named QA; this does not import or publish in a destination.
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
$app = require dirname(__DIR__).'/bootstrap/app.php';
|
||||
$app->make(\Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
use App\Services\EditorialReading;
|
||||
use App\Services\VerifiedDocumentBinary;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
|
||||
$target = DB::selectOne('select current_database() as name,inet_server_port() as port');
|
||||
if (! app()->environment('testing') || $target->name !== 'simulator_fiscal_delta_20261021_test' || (int) $target->port !== 55441) {
|
||||
throw new RuntimeException('Named isolated QA only');
|
||||
}
|
||||
$directory = base_path('outputs/legacy-editorial-20261022');
|
||||
if (! is_dir($directory)) {
|
||||
mkdir($directory, 0700, true);
|
||||
}
|
||||
$identities = json_decode(file_get_contents(config_path('legacy_editorial_resources.json')), true, 512, JSON_THROW_ON_ERROR);
|
||||
if (count($identities) !== 28) {
|
||||
throw new RuntimeException('Exact 28-original scope required');
|
||||
}
|
||||
$label = $argv[1] ?? 'v1';
|
||||
if (! preg_match('/^[a-z0-9-]{1,32}$/D', $label)) {
|
||||
throw new RuntimeException('Invalid package label');
|
||||
}
|
||||
$zip = new ZipArchive;
|
||||
$archive = $directory.'/legacy-editorial-private-'.$label.'.zip';
|
||||
if (is_file($archive)) {
|
||||
throw new RuntimeException('Preserve existing package; choose a reviewed subsequent version rather than overwrite');
|
||||
}
|
||||
if ($zip->open($archive, ZipArchive::CREATE | ZipArchive::EXCL) !== true) {
|
||||
throw new RuntimeException('Cannot create private package');
|
||||
}
|
||||
$manifest = ['format' => 'investmoz-legacy-editorial-private-delivery-v1', 'prepared_at' => gmdate(DATE_ATOM), 'destination' => 'teste-investidor.tempo.co.mz', 'publish' => false, 'documents' => [], 'images' => [],
|
||||
'rules' => ['Transport/import is private and distinct from publication.', 'Resolve target IDs by original SHA256/external identity; no QA user/document/resource IDs are authoritative in the destination.', 'Keep source QA revisions as provenance, never impersonate their actors as destination audit authors.', 'Recheck target scan and existing identity/version; reuse matching originals, never overwrite conflicts.', 'Apply current metadata/edition through authenticated management, creating destination history. No automatic publication.']];
|
||||
$reading = app(EditorialReading::class);
|
||||
$rows = [];
|
||||
$imageHashes = [];
|
||||
DB::beginTransaction();
|
||||
DB::statement('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
||||
try {
|
||||
foreach ($identities as $identity) {
|
||||
$doc = DB::table('documents')->where('external_id', $identity['external_id'])->where('sha256', $identity['sha256'])->whereNull('deleted_at')->first();
|
||||
if (! $doc) {
|
||||
throw new RuntimeException('Missing original identity');
|
||||
}
|
||||
$resource = DB::table('portal_resources')->where('document_id', $doc->id)->first();
|
||||
if (! $resource || $resource->published || $doc->data_classification !== 'internal') {
|
||||
throw new RuntimeException('Expected private QA draft');
|
||||
}
|
||||
$binary = app(VerifiedDocumentBinary::class)->respond($doc);
|
||||
$bytes = $binary instanceof BinaryFileResponse ? file_get_contents($binary->getFile()->getPathname()) : $binary->getContent();
|
||||
if (hash('sha256', $bytes) !== $identity['sha256']) {
|
||||
throw new RuntimeException('Original changed');
|
||||
}
|
||||
$file = 'originals/'.$identity['external_id'].'.txt';
|
||||
$zip->addFromString($file, $bytes);
|
||||
$display = $reading->reading($resource, null, true);
|
||||
$edition = $resource->editorial_content ? json_decode($resource->editorial_content, true, 512, JSON_THROW_ON_ERROR) : null;
|
||||
$imageBinding = null;
|
||||
if ($edition && ! empty($edition['image_resource_id'])) {
|
||||
$imageDoc = DB::table('portal_resources as r')->join('documents as d', 'd.id', '=', 'r.document_id')->where('r.id', $edition['image_resource_id'])->first(['d.*']);
|
||||
if (! $imageDoc) {
|
||||
throw new RuntimeException('Selected image missing');
|
||||
}
|
||||
$imageBinding = ['sha256' => $imageDoc->sha256, 'source_url' => $imageDoc->original_source];
|
||||
unset($edition['image_resource_id']);
|
||||
}
|
||||
if ($display['image_url']) {
|
||||
preg_match('~/documentos/(\d+)/view~', $display['image_url'], $match);
|
||||
$imageDoc = $reading->publicResources()->where('r.id', (int) $match[1])->first(['d.*']);
|
||||
$imageBinary = app(VerifiedDocumentBinary::class)->respond($imageDoc);
|
||||
$imageBytes = $imageBinary instanceof BinaryFileResponse ? file_get_contents($imageBinary->getFile()->getPathname()) : $imageBinary->getContent();
|
||||
if (! isset($imageHashes[$imageDoc->sha256])) {
|
||||
$imageFile = 'images/'.$imageDoc->sha256;
|
||||
$zip->addFromString($imageFile, $imageBytes);
|
||||
$imageHashes[$imageDoc->sha256] = true;
|
||||
$manifest['images'][] = ['file' => $imageFile, 'sha256' => $imageDoc->sha256, 'source_url' => $imageDoc->original_source, 'mode' => 'reuse target matching hash first; import only if absent and authorized'];
|
||||
}
|
||||
}
|
||||
$revisions = DB::table('portal_resource_revisions')->where('resource_id', $resource->id)->orderBy('revision_no')->get();
|
||||
$historyFile = 'provenance/'.$identity['external_id'].'.json';
|
||||
$zip->addFromString($historyFile, json_encode(['scope' => 'QA provenance only; IDs and actors are not destination identities', 'resource_snapshot' => $resource, 'revisions' => $revisions], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
||||
$metadata = (array) $resource;
|
||||
foreach (['id', 'document_id', 'created_by', 'approved_by', 'last_reviewed_by', 'last_reviewed_at', 'metadata_version', 'document_group_id', 'duplicate_of_resource_id', 'document_identity_key', 'editorial_content', 'created_at', 'updated_at'] as $key) {
|
||||
unset($metadata[$key]);
|
||||
}
|
||||
$manifest['documents'][] = ['external_id' => $identity['external_id'], 'file' => $file, 'sha256' => $identity['sha256'], 'kind' => $identity['kind'], 'metadata' => $metadata, 'qa_metadata_version' => $resource->metadata_version, 'edition' => $edition, 'selected_image' => $imageBinding, 'original_image_reference' => $display['image_source_url'], 'original_image_sha256' => $display['image_expected_sha256'], 'body_image_references' => $display['image_references'], 'history_file' => $historyFile, 'publish' => false, 'version_identity' => 'Legado '.$identity['external_id']];
|
||||
$rows[] = ['external_id' => $identity['external_id'], 'qa_resource_id' => $resource->id, 'version' => $resource->metadata_version, 'original_sha256' => $doc->sha256, 'private' => ! $resource->published, 'image_available' => (bool) $display['image_url'], 'image_reference' => $display['image_source_url'], 'body_image_references' => count($display['image_references']), 'history_revisions' => $revisions->count()];
|
||||
}
|
||||
} finally {
|
||||
DB::rollBack();
|
||||
}
|
||||
$manifest['notice'] = 'The QA proof edition of original post 62413 contains an explicitly marked QA note. Review/correct it in the portal before any publication; no editorial text is silently removed from this package.';
|
||||
$zip->addFromString('manifest.json', json_encode($manifest, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
||||
$zip->close();
|
||||
$verify = new ZipArchive;
|
||||
if ($verify->open($archive, ZipArchive::CHECKCONS) !== true) {
|
||||
throw new RuntimeException('Archive invalid');
|
||||
}
|
||||
foreach ($manifest['documents'] as $item) {
|
||||
if (hash('sha256', $verify->getFromName($item['file'])) !== $item['sha256']) {
|
||||
throw new RuntimeException('Archive original mismatch');
|
||||
}
|
||||
}
|
||||
foreach ($manifest['images'] as $item) {
|
||||
if (hash('sha256', $verify->getFromName($item['file'])) !== $item['sha256']) {
|
||||
throw new RuntimeException('Archive image mismatch');
|
||||
}
|
||||
}
|
||||
$entries = $verify->numFiles;
|
||||
$verify->close();
|
||||
$receipt = ['qa_only' => true, 'originals' => count($rows), 'rows' => $rows, 'archive_entries' => $entries, 'archive_sha256' => hash_file('sha256', $archive), 'archive_bytes' => filesize($archive), 'verified_original_hashes' => 28, 'included_image_originals' => count($manifest['images']), 'destination_imported' => false, 'destination_published' => false, 'database_writes' => 0];
|
||||
file_put_contents($directory.'/private-delivery-'.$label.'-receipt.json', json_encode($receipt, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
||||
echo json_encode(['originals' => count($rows), 'archive_entries' => $entries, 'sha256' => $receipt['archive_sha256'], 'included_images' => count($manifest['images']), 'destination_imported' => false, 'destination_published' => false]);
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
// Rehearse recovery with the existing importer. Never accepts an active database.
|
||||
require dirname(__DIR__).'/vendor/autoload.php';
|
||||
$app = require dirname(__DIR__).'/bootstrap/app.php';
|
||||
$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
|
||||
$target = Illuminate\Support\Facades\DB::selectOne('select current_database() as database,inet_server_port() as port');
|
||||
throw_unless($target->database === 'simulator_fiscal_delta_20261021_test' && (int) $target->port === 55441
|
||||
&& config('database.connections.pgsql.host') === '127.0.0.1', RuntimeException::class, 'Isolated QA only.');
|
||||
$app->detectEnvironment(fn () => 'testing');
|
||||
config(['geometry-import.enabled' => true, 'portal.managers' => ['qa-content-promoter@example.test'],
|
||||
'services.malware.scanner' => 'clamd', 'services.malware.socket' => null,
|
||||
'services.malware.host' => '127.0.0.1', 'services.malware.port' => 3310]);
|
||||
$actor = App\Models\User::where('email', 'qa-content-promoter@example.test')->firstOrFail();
|
||||
$directory = config('geometry-import.directory');
|
||||
$out = storage_path('app/private/territory-demarcation-recovery');
|
||||
Illuminate\Support\Facades\File::ensureDirectoryExists($out, 0700);
|
||||
$manifest = [];
|
||||
foreach (glob($directory.'/moz-adm*-ine-*') as $file) {
|
||||
$manifest[] = ['file' => basename($file), 'bytes' => filesize($file), 'sha256' => hash_file('sha256', $file)];
|
||||
}
|
||||
file_put_contents($out.'/source-inventory.json', json_encode($manifest, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
$scanner = app(App\Services\Malware\MalwareScanner::class);
|
||||
$health = $scanner->health();
|
||||
throw_unless($health['available'], RuntimeException::class, 'Current scanner unavailable.');
|
||||
$scans = [];
|
||||
foreach ([1, 2, 3] as $level) {
|
||||
$file = $directory.'/moz-adm'.$level.'-ine-20190607.geojson';
|
||||
// Scope the stream budget to this retained GIS original in this process only.
|
||||
config(['services.malware.max_file_bytes' => filesize($file), 'services.malware.max_stream_bytes' => filesize($file)]);
|
||||
$scan = $scanner->scan($file, filesize($file), storage_path('app/private'));
|
||||
$scan['signatures_at'] = $scan['signatures_at']?->format(DATE_ATOM);
|
||||
if ($level === 3 && $scan['reason_code'] === 'scanner_decompression_limit') {
|
||||
$binary = getenv('GIS_CLAMSCAN_BINARY');
|
||||
$database = getenv('GIS_CLAMSCAN_DATABASE');
|
||||
throw_unless(is_file($binary ?: '') && is_dir($database ?: ''), RuntimeException::class, 'GIS scanner paths required.');
|
||||
$process = new Symfony\Component\Process\Process([$binary, '--database='.$database, '--max-filesize=40M', '--max-scansize=100M', '--no-summary', $file]);
|
||||
$process->setTimeout(120)->mustRun();
|
||||
throw_unless(str_ends_with(trim($process->getOutput()), ': OK'), RuntimeException::class, 'GIS original scan incomplete.');
|
||||
$version = new Symfony\Component\Process\Process([$binary, '--database='.$database, '--version']);
|
||||
$version->mustRun();
|
||||
$parts = explode('/', trim($version->getOutput()));
|
||||
$signatureTime = strtotime(end($parts));
|
||||
throw_unless($signatureTime && time() - $signatureTime < 48 * 3600, RuntimeException::class, 'Current GIS signatures required.');
|
||||
$scan = ['result' => 'clean', 'engine' => 'ClamAV', 'engine_version' => trim($version->getOutput()),
|
||||
'signatures_at' => gmdate(DATE_ATOM, $signatureTime), 'scope' => 'retained GIS original only',
|
||||
'original_sha256' => hash_file('sha256', $file), 'stream_attempt' => $scan, 'exit_code' => $process->getExitCode()];
|
||||
}
|
||||
$scans[] = ['file' => basename($file), 'sha256' => hash_file('sha256', $file), 'scan' => $scan];
|
||||
echo json_encode(['level' => $level, 'scan' => $scan]).PHP_EOL;
|
||||
throw_unless($scan['result'] === 'clean', RuntimeException::class, 'Original scan incomplete.');
|
||||
}
|
||||
file_put_contents($directory.'/geometry-source-scans.json', json_encode(['captured_at' => gmdate('c'), 'files' => $scans], JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
$service = app(App\Services\TerritoryGeometryImport::class);
|
||||
foreach ([1, 2, 3] as $level) {
|
||||
$plan = $service->preview($actor, $level);
|
||||
file_put_contents($out.'/preview-'.$level.'.json', json_encode($plan, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo json_encode(['level' => $level, 'resolved' => $plan['resolved'], 'pending' => $plan['pending']]).PHP_EOL;
|
||||
if (! in_array('--apply', $argv, true)) break; // Lower levels require imported parent correspondences.
|
||||
$result = $service->apply($actor, ['level' => $level, 'hash' => $plan['hash'], 'idempotency_key' => (string) Illuminate\Support\Str::uuid(),
|
||||
'reason' => 'Recuperação QA da demarcação anteriormente demonstrada, com originais e correspondências aceites.', 'confirm' => true]);
|
||||
file_put_contents($out.'/result-'.$level.'.json', json_encode($result, JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
|
||||
echo json_encode(['level' => $level, 'inserted' => $result['inserted'], 'reused' => $result['reused']]).PHP_EOL;
|
||||
}
|
||||
Reference in New Issue
Block a user