Compare commits

..
Author SHA1 Message Date
nala fb4e7d8970 Complete private legacy article editing and previews in QA 2026-10-22 23:36:07 +02:00
nala bc93ccdb9e docs: reconcile all legacy articles and verify private QA batches 2026-10-22 23:10:21 +02:00
nala 7ccf62adec docs: record authenticated tender installation and final links
tests / ci (push) Canceled after 0s
2026-10-22 22:54:59 +02:00
nala f4d0970917 merge: install reviewed tender publishing in homologation
tests / ci (push) Canceled after 0s
2026-10-22 22:49:18 +02:00
nala e46f973159 docs: plan staged tender schema installation and activation 2026-10-22 22:44:42 +02:00
nala 1bcf34c0c0 feat: add QA tender and announcement publishing workflow 2026-10-22 22:41:28 +02:00
nala ef0c2acd36 docs: record authenticated homologation geometry restoration and access proof
tests / ci (push) Canceled after 0s
2026-10-22 22:04:32 +02:00
nala 30aed7cd12 Merge approved administrative access for geometry restoration
tests / ci (push) Canceled after 0s
2026-10-22 21:55:48 +02:00
nala 95d4cf44d7 fix: reuse approved administration for authenticated geometry import 2026-10-22 21:55:36 +02:00
nala 73c046e104 Merge territorial demarcation recovery reviewed for homologation
tests / ci (push) Canceled after 0s
2026-10-22 21:45:58 +02:00
nala 15d82611b6 fix: recover territorial demarcation QA and prepare retained geometry restoration 2026-10-22 21:43:54 +02:00
nala 8286d7504d fix: preserve approved editorial access independently of technical reservation
tests / ci (push) Canceled after 0s
2026-10-22 21:03:11 +02:00
nala aa2b3d05b0 fix: route authorised content staff through management access entry
tests / ci (push) Waiting to run
2026-10-22 21:00:05 +02:00
nala 97d51db60a feat: add unified team content management entry in QA
tests / ci (push) Canceled after 0s
2026-10-22 20:55:08 +02:00
Nelson_Matsinhe a1b553372e ajuste do tamanho da navbar
tests / ci (push) Canceled after 0s
2026-10-22 15:26:56 +02:00
nala f80d30f478 docs: record Tempo approval for catalogue selector integration
tests / ci (push) Canceled after 0s
2026-10-22 14:36:36 +02:00
nala 916819eb35 docs: record keyboard selector proof and mouse observation limit 2026-10-22 14:33:59 +02:00
nala 1d921c9d03 fix: keep public catalogue selectors independent of opportunity results 2026-10-22 14:28:40 +02:00
nala 7b1c35793c Fix Node navigation import and use Tempo commercial footer contact
tests / ci (push) Canceled after 0s
2026-10-22 13:54:20 +02:00
nala 1ec318561c docs: record authorised Inhambane preparation publication
tests / ci (push) Canceled after 0s
2026-10-22 13:47:50 +02:00
nala 9ac623a433 fix: preserve territorial search across investment catalogues 2026-10-22 12:52:52 +02:00
nala 5f4eb5cfaa Record authorized adaptive path deployment and enabled destination state 2026-10-22 10:18:38 +02:00
82 changed files with 7205 additions and 222 deletions
+16
View File
@@ -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
File diff suppressed because one or more lines are too long
Binary file not shown.
+342
View File
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'],
+50
View File
@@ -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];
}
}
+8 -2
View File
@@ -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']);
}
});
}
+130
View File
@@ -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,
];
}
}
+19 -19
View File
@@ -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;
}
+3 -3
View File
@@ -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;
+5 -2
View File
@@ -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;
}
}
+287
View File
@@ -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]);
}
}
}
+1 -1
View File
@@ -20,7 +20,7 @@ class TerritoryGeometryImport
private function guard(User $actor, bool $write = false): void
{
abort_unless($actor->can('manage-portal'), 403);
abort_unless($actor->can('manage-portal') || app(ScopedPortalAdministration::class)->administrative($actor), 403);
abort_unless(config('geometry-import.enabled'), 503, 'Importação cartográfica não disponibilizada.');
if (! $write) {
return;
+170
View File
@@ -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
View File
@@ -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'),
+7
View File
@@ -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');
}
};
+19
View File
@@ -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 . .
+13
View File
@@ -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/envi­a 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/`
@@ -1839,5 +1839,167 @@
"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.
@@ -2136,3 +2150,334 @@ Procedimento: após a decisão, recuperar o main actual, rever a comparação e
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
1 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
2 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 13 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 6, "sha256": "5e3840705cf98226d1f189cf665aaa98cb05da96c645159a1332a1d429d642eb"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
3 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
4 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
5 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
6 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 publicado inventariado; cadastro/versão por reconciliar 29 publicado publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"scope": "homologação", "href": "/documentos/27/view", "status": "referência na ficha; binário não reensaiado"} Entrega concluída; nenhum novo envio ou republicação da imagem. Regulação específica Fintech permanece na matriz. Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente. Preservar publicação; restante acervo separado, sem repetir aceitação. Preservar a publicação; gerir edições e versões pela biblioteca existente. 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 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
7 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 25 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
8 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
9 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
10 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
11 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 18 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 23, "sha256": "0a2c50d91dad3a6007544634540e0f61b11d3506682a5a19066bae03cdcba490"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
12 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
13 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
14 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
15 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
16 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
17 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 publicado inventariado; cadastro/versão por reconciliar 28 7 publicado publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"scope": "homologação", "href": "/documentos/27/view", "status": "referência na ficha; binário não reensaiado"} Entrega concluída; nenhum novo envio ou republicação da imagem. Regulação específica Fintech permanece na matriz. Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente. Preservar publicação; restante acervo separado, sem repetir aceitação. Preservar a publicação; gerir edições e versões pela biblioteca existente. 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 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
18 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 17 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 26, "sha256": "8e58544e49e719f9c865c91e07ef1030155a9e3d59260e6dc93e9c9e25674e39"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
19 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 21 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
20 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) 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.
21 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
22 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 26 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
23 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
24 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 3 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
25 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 23 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
26 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 6 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
27 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 20 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
28 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 22 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
29 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 24 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
30 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 4 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
31 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 5 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
32 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
33 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
34 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
35 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 publicado inventariado; cadastro/versão por reconciliar 27 28 publicado publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"scope": "homologação", "href": "/documentos/27/view", "status": "referência na ficha; binário não reensaiado"} Entrega concluída; nenhum novo envio ou republicação da imagem. Regulação específica Fintech permanece na matriz. Sem pendência de integração desta identidade; mantém-se a revisão editorial corrente. Preservar publicação; restante acervo separado, sem repetir aceitação. Preservar a publicação; gerir edições e versões pela biblioteca existente. 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 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
36 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 19 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 25, "sha256": "fe9a2e382e385b35f6e61ae320e199a979678677fd327fb7d4b07d8ce0111c11"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
37 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) 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.
38 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
39 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
40 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
41 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 1 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
42 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
43 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
44 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
45 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
46 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
47 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 2 publicados {"status": "imagem já publicada; reutilizar", "resource_id": 24, "sha256": "28f41f12e10ab34545d8e432f6e8c74f86960e17e0c4a9956f514bcde4881eaa"} {"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. Reutilizar a versão e a aceitação existentes; não importar ou aceitar novamente. Preservar a publicação; gerir edições e versões pela biblioteca existente. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
48 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
49 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
50 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
51 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"} 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. 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. Preparar apenas se escolhido para o próximo lote; não publicar por esta reconciliação. Seguir o guia e a sequência de entrega privada do manual; não repetir importação QA ou provas dos 20 aceites. outputs/investmoz-legado-inventario-reconciliado-20261015/RECONCILIACAO-LEGADO.json; LEITURA-ESTADO.json (SQL READ ONLY) 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
+214
View File
@@ -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;
+5
View File
@@ -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()],
@@ -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
}
@@ -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>
+2 -1
View File
@@ -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]}
+2 -1
View File
@@ -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);
}
+2
View File
@@ -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',
+24
View File
@@ -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;
}
+2
View File
@@ -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 && (
+2
View File
@@ -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
+25 -2
View File
@@ -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}
>
+3 -2
View File
@@ -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>
+1
View File
@@ -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;
};
+3 -2
View File
@@ -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>
+10 -5
View File
@@ -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>
+7 -2
View File
@@ -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>
+2
View File
@@ -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"
+184 -25
View File
@@ -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>
);
+16 -31
View File
@@ -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>
+45 -11
View File
@@ -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}
+6 -4
View File
@@ -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">
+4 -3
View File
@@ -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>
+832
View File
@@ -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>
);
}
+196
View File
@@ -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('&laquo;', '«')
.replace('&raquo;', '»')}
</Link>
) : (
<span key={i} className="mx-2">
{link.label
.replace('&laquo;', '«')
.replace('&raquo;', '»')}
</span>
),
)}
</nav>
</Frame>
);
}
+106
View File
@@ -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>
);
}
+253
View File
@@ -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>
);
}
+89
View File
@@ -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>
);
}
+20
View File
@@ -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');
});
+5
View File
@@ -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);
}
}
}
+217
View File
@@ -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=&sector=&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&regime=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;
}
}
+27
View File
@@ -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');
});
+177
View File
@@ -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))
+117
View File
@@ -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;
}