Compare commits

...
Author SHA1 Message Date
nala 9d3bfa89b2 feat: prepare audited promotional disclosure interface in QA 2026-10-22 21:27:10 +02:00
nala c83ab90c68 docs: record served management entry and actual operational limits 2026-10-22 21:06:29 +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
nala 042ffbde44 Preserve legacy draft shape when no compatible context is inherited
tests / ci (push) Canceled after 0s
2026-10-22 10:14:46 +02:00
nala 064296e1fb Record current v4 report proof and adaptive path integration dependencies
tests / ci (push) Canceled after 0s
2026-10-22 10:06:08 +02:00
nala a1fbda06ad Wire grounded private investment preparation into QA dossier path 2026-10-22 10:00:23 +02:00
nala 52fbb1468a Close context increment review and ground first adaptive investment path 2026-10-22 09:25:14 +02:00
nala a76369eb86 Preserve dossier context and expose contextual applicability in QA 2026-10-22 09:16:15 +02:00
nala 57fc914821 Merge verified IVA monthly projection and Modelo A interface into homologation
tests / ci (push) Canceled after 0s
2026-10-21 14:34:47 +02:00
nala 0b0cf76137 Reconcile IVA projection with Tempo Modelo A in QA 2026-10-21 14:27:24 +02:00
nala 3f5430c3ce Integrate monthly IVA with projected sales in QA 2026-10-21 14:05:46 +02:00
nala 0d7b70459d fix: explain IVA query conditions and distinguish integration failures 2026-10-21 13:35:22 +02:00
paulomassigue f3ec686f8c diminuicao do tamanho do footer
tests / ci (push) Canceled after 0s
2026-10-21 13:16:35 +02:00
nala bd5166bd11 docs: record authorised fiscal rule publication and consumer activation 2026-10-21 12:57:40 +02:00
paulomassigue d3fe8a02ec Merge branch 'main' of https://git.tempo.co.mz/nala/Investidor-V2
tests / ci (push) Canceled after 0s
2026-10-21 12:46:54 +02:00
paulomassigue f5b1d67ee5 remocao de inicio em descoberta 2026-10-21 12:46:26 +02:00
paulomassigue 90179149c7 Merge branch 'main' of https://git.tempo.co.mz/nala/Investidor-V2 2026-10-21 12:42:05 +02:00
paulomassigue 364be835fe correcao de conflitos 2026-10-21 12:41:18 +02:00
nala 4a4d121275 fix: allow authorised fiscal maintenance by the same responsible person
tests / ci (push) Canceled after 0s
2026-10-21 12:39:07 +02:00
paulomassigue 480effa5fc separacao de sobre nos no dropdow Tempo 2026-10-21 12:36:08 +02:00
nala 226fd308dd feat: connect published IVA consumer to normal simulator in QA
tests / ci (push) Canceled after 0s
2026-10-21 11:59:50 +02:00
nala 2ced7b790c docs: delimit published IVA consumer and three rollout operations 2026-10-21 11:39:17 +02:00
nala d186f94206 feat: demonstrar percurso IVA QA e consolidar revisao 2026-10-21 09:40:49 +02:00
nala 83d2f97d82 feat: preparar lote IVA exacto em QA sem activacao 2026-10-21 09:22:11 +02:00
nala 37085b40c1 docs: fundamentar base IVA e delimitar lacuna de arredondamento 2026-10-21 09:03:01 +02:00
nala 325d58b694 docs: preparar ficha fiscal IVA v1 sem activacao 2026-10-21 08:57:56 +02:00
nala 0fafd348e7 docs: delimitar entrega de planeamento e propor primeiro lote fiscal 2026-10-21 08:47:07 +02:00
nala 13f68f1bd8 fix: concluir recuperacao de exportacoes e consolidar candidata QA
tests / ci (push) Canceled after 0s
2026-10-21 08:43:12 +02:00
nala c22003a3da fix: recover simulator cashflow and verify QA continuity and report assumptions 2026-10-20 22:00:48 +02:00
nala a690c665fb fix: validate simulator dates and reconcile QA calculations reports and language 2026-10-20 21:00:52 +02:00
nala 59ab4fe956 fix: validar datas fiscais e iniciar revisao integrada do simulador 2026-10-20 20:03:40 +02:00
189 changed files with 12804 additions and 1212 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
+5
View File
@@ -4,6 +4,11 @@ APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8000
# Separate from publication. Bind only an explicitly authorised published rule version.
INVESTMOZ_IVA_CONSUMER_ENABLED=false
INVESTMOZ_IVA_CONSUMER_RULE_ID=
INVESTMOZ_IVA_CONSUMER_RULE_VERSION=0
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
+7
View File
@@ -39,3 +39,10 @@ Referenciar estas orientações nos requisitos correspondentes da matriz existen
- Preservar o ficheiro original, a proveniência, o identificador exacto, a edição/data, o hash e o histórico. Não marcar autenticidade, vigência, direitos ou scan como confirmados sem evidência.
- Para fontes fiscais, inventariar também as fontes não analisadas e os diplomas/artigos sem correspondência documental. Parâmetros ausentes ou ambiguidades permanecem indeterminados; não usar zero nem a taxa mais recente como substituto.
- Antes de aplicar mudanças em dados activos, confirmar backup/restauro e a acção autenticada que continua necessária. Testes e migrações devem usar exclusivamente uma base QA isolada com guarda contra a base activa.
## Execução das fontes e decisões — orientação permanente da Tempo, 22/10/2026
A legislação, os formulários e os procedimentos oficiais disponibilizados são fontes válidas para implementar os requisitos. As decisões e procedimentos da Tempo orientam a sua utilização no portal. Traduzir essas fontes em campos, regras, documentos, cálculos e percursos executáveis com rastreabilidade, sem exigir nova validação jurídica genérica ou criar aprovadores, restrições ou condições por interpretação do Codex.
Uma divergência concreta deve identificar os trechos, períodos e comportamento afectados; prosseguir com os restantes requisitos. Uma decisão de implementação em falta deve ser apresentada especificamente à Tempo, sem a substituir por regra inventada. Rever e remover bloqueios anteriores sem fundamento numa fonte aplicável ou decisão expressa da Tempo, preservando dados e histórico. A rastreabilidade explica a execução e não constitui uma autoridade adicional. Mantêm-se os limites autorizados de QA e das alterações no activo.
File diff suppressed because one or more lines are too long
Binary file not shown.
+342
View File
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -0,0 +1,50 @@
<?php
namespace App\Console\Commands;
use App\Models\InvestmentLegalSource;
use App\Models\User;
use App\Services\SimulationTaxParameterResolver;
use App\Services\SimulationTaxRuleExpression;
use App\Services\SimulationTaxRuleGovernance;
use App\Support\IsolatedQaDatabase;
use Illuminate\Console\Command;
/** Explicit registration: never run during migration, deployment or document import. */
class RegisterFiscalQaDraft extends Command
{
protected $signature = 'fiscal:qa-register-iva {actor : Existing QA editor ID} {source : Existing accepted legal source UUID}';
protected $description = 'Cadastrar apenas o rascunho IVA do primeiro lote numa base QA isolada, sem publicação';
public function handle(SimulationTaxRuleGovernance $governance): int
{
abort_unless(IsolatedQaDatabase::allowsSchemaChanges(), 403, 'O cadastro deste lote só é permitido na base QA isolada.');
$actor = User::findOrFail($this->argument('actor'));
$source = InvestmentLegalSource::findOrFail($this->argument('source'));
abort_unless($source->validation_status === 'validated' && $source->diploma === 'Lei 22/2022'
&& $source->article === 'Artigo 17(1)', 422, 'Seleccione a referência aceite da taxa geral, Lei 22/2022, artigo 17(1).');
$payload = json_decode(file_get_contents(database_path('qa/iva-general-net-v1.json')), true, flags: JSON_THROW_ON_ERROR);
$actualHash = hash_file('sha256', base_path('docs/investmoz/simulations/source-archive/Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf'));
abort_unless(hash_equals($payload['source_sha256'], $actualHash), 422, 'O exemplar não corresponde ao hash da ficha.');
$payload['legal_source_id'] = (string) $source->id;
app(SimulationTaxRuleExpression::class)->validate($payload['rule_definition']);
app(SimulationTaxParameterResolver::class)->validateParameters($payload['parameters']);
$existing = $governance->listing($actor, ['rule_code' => $payload['rule_code']])->getCollection()
->firstWhere('rule_version', 1);
if ($existing) {
foreach ($payload as $field => $value) {
$stored = $existing->attributesToArray()[$field] ?? null;
abort_unless($stored == $value, 409, 'O cadastro existente difere desta ficha; não será sobrescrito.');
}
abort_unless($existing->status === 'draft', 409, 'A versão existente já não é um rascunho.');
$this->line(json_encode(['id' => $existing->id, 'status' => 'draft', 'reused' => true], JSON_THROW_ON_ERROR));
return self::SUCCESS;
}
$rule = $governance->create($actor, $payload);
$this->line(json_encode(['id' => $rule->id, 'status' => $rule->status, 'reused' => false], JSON_THROW_ON_ERROR));
return self::SUCCESS;
}
}
@@ -37,11 +37,17 @@ class SimulationController extends Controller
$token = $actorId === null ? $request->header('X-Simulation-Token') : null;
abort_if($actorId === null && (! is_string($token) || strlen($token) < 32 || strlen($token) > 256), 422, 'X-Simulation-Token opaco gerado pelo cliente é obrigatório para execução anónima.');
$validated = $request->validated();
$data = ['base' => (string) $validated['base'], 'rate' => (string) $validated['rate'],
'reduction' => (string) ($validated['reduction'] ?? '0'), 'organization_id' => $validated['organization_id'] ?? null];
$organizationId = $data['organization_id'] ?? null;
$organizationId = $validated['organization_id'] ?? null;
$execution->authorizeOrganization($actorId, $organizationId);
$result = $execution->create($data, $actorId, $organizationId, $token, $key);
if (($validated['scenario_kind'] ?? null) === 'fiscal_rule_published') {
$input = array_intersect_key($validated, array_flip(['base', 'currency', 'reference_date', 'operation', 'scope_declared', 'monthly_sales', 'sales_vat_basis']));
$input['scope_declared'] = (bool) $input['scope_declared'];
$result = $execution->createPublishedFiscal($input, $actorId, $organizationId, $token, $key);
} else {
$data = ['base' => (string) $validated['base'], 'rate' => (string) $validated['rate'],
'reduction' => (string) ($validated['reduction'] ?? '0')];
$result = $execution->create($data, $actorId, $organizationId, $token, $key);
}
$simulation = $result['simulation'];
$body = ['data' => $simulation, 'replayed' => $result['replayed']];
if ($actorId === null) {
@@ -19,13 +19,15 @@ class SimulationTaxParameterController extends Controller
$input['period_start'],
$input['period_end'],
$input['context'] ?? [],
$input['component'] === 'IVA',
);
return response()->json([
'component' => $input['component'],
'period' => ['start' => $input['period_start'], 'end' => $input['period_end']],
'assumptions' => $input['context'] ?? [],
'parameters' => $data,
'parameters' => $input['component'] === 'IVA' ? $data['parameters'] : $data,
'diagnostics' => $input['component'] === 'IVA' ? $data['diagnostics'] : [],
])->header('Cache-Control', 'no-store');
}
}
@@ -40,6 +40,7 @@ class InvestmentLegalSourceController extends Controller
$data = $request->validate(['version_no' => ['required', 'integer', 'min:1']]);
return DB::transaction(function () use ($request, $id, $action, $data) {
DB::select('select pg_advisory_xact_lock(261022, hashtext(?))', [$id]);
$source = InvestmentLegalSource::whereKey($id)->lockForUpdate()->firstOrFail();
abort_if((int) $source->version_no !== (int) $data['version_no'], 409, 'Versão alterada.');
if ($action === 'validate') {
@@ -170,8 +170,10 @@ class InvestmentSubmissionController extends Controller
// Binding does not save form edits or assert that the context has been reviewed.
$this->event($row, $request, 'context_bound');
$desk->event($desk->case($request->user(), $case->id), $request->user(), 'manifestation_context_bound', ['submission_id' => $id, 'previous_case_version' => (int) $case->version]);
return ['case_id' => $case->id, 'version' => $row->version, 'case_version' => (int) $case->version + 1, 'replayed' => false];
}, 3);
return response()->json($result);
}
@@ -190,11 +192,12 @@ class InvestmentSubmissionController extends Controller
{
$required = $submitting ? 'required' : 'nullable';
$data = $request->validate(['version' => ['required', 'integer', 'min:1'],
'payload' => ['required', 'array:full_name,email,phone,country,residence_country_code,incorporation_country_code,organization_country_snapshot,declared_origin_country_code,organisation,representative_role,experience,investment_capacity,profile_id,sector,objective,participation_id,partnership_id,estimated_amount,currency,horizon,location,cluster_ids,corridor_ids,project_ids,contact_consent,other_details'],
'payload' => ['required', 'array:context_reuse,full_name,email,phone,country,residence_country_code,incorporation_country_code,organization_country_snapshot,declared_origin_country_code,organisation,representative_role,experience,investment_capacity,profile_id,sector,objective,participation_id,partnership_id,estimated_amount,currency,horizon,location,cluster_ids,corridor_ids,project_ids,contact_consent,other_details'],
'payload.organization_country_snapshot' => ['sometimes', 'array', 'max:10'],
'payload.residence_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()],
'payload.incorporation_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()],
'payload.declared_origin_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()],
'payload.context_reuse' => ['nullable', 'string', 'max:10000'],
'payload.other_details' => ['nullable', 'string', 'max:5000'],
'payload.full_name' => [$required, 'string', ...($submitting ? ['min:3'] : []), 'max:150'], 'payload.email' => [$required, $submitting ? 'email:rfc' : 'string', 'max:150'],
'payload.phone' => ['nullable', 'string', 'max:30'], 'payload.country' => [$submitting ? 'required_without:payload.declared_origin_country_code' : 'nullable', 'nullable', 'string', 'max:100'],
@@ -103,6 +103,7 @@ class OpportunityManagementController extends Controller
$records = $query->orderByDesc('id')->paginate(20)->withQueryString();
$records->getCollection()->each(function (Opportunity $r) use ($access, $service, $request) {
$r->setAttribute('can_edit', $access->canEdit($request->user(), $r));
$r->setAttribute('can_promote', app(\App\Services\OpportunityPromotion::class)->allowed($request->user(), $r));
$r->setAttribute('working_status', $access->state($r));
$r->setAttribute('publication', $r->publicationPresentation());
$r->setAttribute('draft_title', $service->values($r)['canonical']['official_title'] ?? $r->title);
@@ -130,6 +131,7 @@ class OpportunityManagementController extends Controller
'profileReauthenticationRequired' => $record && $record->owner_user_id !== null
? app(OpportunityProfileWorkflow::class)->reauthenticationRequired($request->user(), $record) : false,
'publicationState' => $record?->publicationPresentation(),
'promotionHref' => $record && app(\App\Services\OpportunityPromotion::class)->allowed($request->user(), $record) ? '/minhas-oportunidades/'.$record->id.'/divulgacao' : null,
'canonicalResponse' => $record?->owner_user_id !== null ? app(OpportunityCanonicalResponse::class)->read($request->user(), $record) : null,
'canEditDraft' => $record ? $access->canEdit($request->user(), $record) : false,
'workflowParticipants' => $record && $record->owner_user_id !== null ? app(OpportunityProfileWorkflow::class)->participants($request->user(), $record) : ['validators' => [], 'publishers' => []],
@@ -0,0 +1,26 @@
<?php
namespace App\Http\Controllers;
use App\Models\Opportunity;
use App\Services\OpportunityPromotion;
use Illuminate\Http\Request;
use Inertia\Inertia;
final class OpportunityPromotionController extends Controller
{
public function show(Request $request, int $id, OpportunityPromotion $service): \Inertia\Response
{
return Inertia::render('management/opportunity-promotion', $service->editor($request->user(), Opportunity::findOrFail($id)));
}
public function preview(Request $request, int $id, OpportunityPromotion $service): \Illuminate\Http\JsonResponse
{
return response()->json($service->preview($request->user(), Opportunity::findOrFail($id), $request->all()));
}
public function confirm(Request $request, int $id, OpportunityPromotion $service): \Illuminate\Http\JsonResponse
{
return response()->json($service->confirm($request->user(), $id, $request->all()));
}
}
@@ -3,6 +3,7 @@
namespace App\Http\Controllers;
use App\Models\Opportunity;
use App\Models\User;
use App\Services\OpportunityCanonicalContract;
use App\Services\OpportunityContacts;
use App\Services\OpportunityManagement;
@@ -52,6 +53,9 @@ class PortalProfileController extends Controller
'assignments' => DB::table('portal_role_assignments')->get(), 'pair' => DB::table('portal_superuser_pair')->first(),
'operatorOrganizations' => app(PortalInternalDesignation::class)->organizations(),
'internalTypes' => OpportunityCanonicalContract::BASE_CATEGORIES,
'staffAccounts' => User::orderBy('name')->get(['id', 'name', 'email', 'email_verified_at', 'two_factor_confirmed_at'])
->map(fn ($user) => ['id' => $user->id, 'name' => $user->name, 'email' => $user->email,
'verified' => $user->hasVerifiedEmail(), 'mfa' => $user->two_factor_confirmed_at !== null]),
'actorId' => $request->user()->id, 'types' => OpportunityManagement::CATEGORIES,
'organizations' => DB::table('organizations')->whereNull('deleted_at')->get(['id', 'name']),
'securityRequests' => DB::table('portal_security_approvals')->latest()->limit(30)->get(),
@@ -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;
@@ -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'],
@@ -0,0 +1,46 @@
<?php
namespace App\Http\Controllers;
use App\Models\InvestmentLegalSource;
use App\Services\SimulationTaxGovernanceAccess;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
/** Explicit fiscal curation of an identified official source; not an independent legal opinion. */
final class SimulationFiscalSourceController extends Controller
{
public function store(Request $request): JsonResponse
{
SimulationTaxGovernanceAccess::requireRole($request->user(), 'editors');
$data = $request->validate([
'title' => ['required', 'string', 'max:255'], 'url' => ['required', 'url:https', 'max:2000'],
'diploma' => ['required', 'string', 'max:255'], 'article' => ['required', 'string', 'max:255'],
'competent_entity' => ['required', 'string', 'max:255'], 'valid_from' => ['required', 'date_format:Y-m-d'],
'valid_until' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:valid_from'],
'sha256' => ['required', 'regex:/^[a-f0-9]{64}$/D'],
'citation' => ['required', 'string', 'min:10', 'max:2000'],
'official_origin' => ['required', 'string', 'min:10', 'max:2000'],
]);
return DB::transaction(function () use ($request, $data): JsonResponse {
$actor = $request->user();
$acceptance = ['scope' => 'fiscal_parameters', 'basis' => 'Tempo-official-sources-20261008',
'responsible_user_id' => $actor->id, 'sha256' => $data['sha256'], 'citation' => $data['citation'],
'official_origin' => $data['official_origin'], 'accepted_at' => now()->toISOString(),
'legal_opinion' => false, 'independent_review' => false];
unset($data['sha256'], $data['citation'], $data['official_origin']);
$source = InvestmentLegalSource::create([...$data, 'owner_id' => $actor->id, 'validator_id' => null,
'validation_status' => 'pending', 'validated_at' => null, 'version_no' => 1, 'fiscal_acceptance' => $acceptance]);
DB::table('audit_events')->insert(['source_system' => 'simulation-fiscal-maintenance',
'entity_type' => 'investment_legal_sources', 'entity_id' => $source->id, 'actor_user_id' => $actor->id,
'action' => 'official_source_acceptance', 'after_data' => json_encode([
'source' => $source->attributesToArray(), 'basis' => $acceptance,
'sha256' => hash('sha256', $source->toJson()),
], JSON_THROW_ON_ERROR)]);
return response()->json($source->fresh(), 201);
});
}
}
@@ -2,17 +2,51 @@
namespace App\Http\Controllers;
use App\Services\SimulationFiscalQaPreview;
use App\Services\SimulationTaxParameterResolver;
use App\Services\SimulationTaxRuleExpression;
use App\Services\SimulationTaxRuleGovernance;
use App\Support\IsolatedQaDatabase;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
use InvalidArgumentException;
class SimulationTaxRuleGovernanceController extends Controller
{
public function qaReview(Request $request, string $id, SimulationTaxRuleGovernance $governance): Response
{
abort_unless(IsolatedQaDatabase::allowsSchemaChanges(), 404);
$rule = $governance->show($request->user(), $id);
abort_unless($rule->status === 'draft' && $rule->rule_code === 'IVA-GERAL-BASE-LIQUIDA', 422,
'Seleccione o rascunho do primeiro lote fiscal.');
return Inertia::render('management/fiscal-qa-review', ['rule' => [
'id' => $rule->id, 'code' => $rule->rule_code, 'version' => $rule->rule_version,
]]);
}
public function qaPreview(Request $request, string $id, SimulationFiscalQaPreview $preview): JsonResponse
{
abort_unless(IsolatedQaDatabase::allowsSchemaChanges(), 404);
$key = $request->header('Idempotency-Key');
abort_unless(is_string($key) && strlen($key) >= 16 && strlen($key) <= 128, 428, 'Idempotency-Key é obrigatório.');
$input = $request->validate([
'base' => ['required', 'string', 'max:40', 'regex:/^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/D'],
'reference_date' => ['required', 'date_format:Y-m-d'],
'currency' => ['required', 'in:MZN'],
'operation' => ['required', 'in:internal_general_net'],
]);
$result = $preview->create($request->user(), $id, $input, $key);
$simulation = $result['simulation'];
return response()->json(['data' => $simulation, 'replayed' => $result['replayed']], $result['replayed'] ? 200 : 202)
->header('Location', url('/api/v1/simulations/'.$simulation->id));
}
public function index(Request $request, SimulationTaxRuleGovernance $governance): JsonResponse
{
$filters = $request->validate([
@@ -1,31 +1,39 @@
<?php
namespace App\Http\Middleware;
use App\Services\PortalProfileAccess;
use App\Services\ScopedPortalAdministration;
use App\Services\SimulationTaxGovernanceAccess;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Services\ScopedPortalAdministration;
final class EnsureScopedPortalAdministration
{
public function handle(Request $request, Closure $next): Response
{
$actor=$request->user();abort_unless($actor,403);
$scope=app(ScopedPortalAdministration::class);
$path=$request->path();
$interests=$request->is('management/interests','management/interests/*','api/v1/investment/admin/interests/*');
// Exact areas of P1; unrelated management, fiscal rules and private records keep their gate.
$catalogue=$request->is('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',
'management/balcao/fontes') || ($request->isMethod('GET') && $path==='management/balcao-install');
$allowed=$interests ? $scope->interestOperator($actor) : ($catalogue && $scope->administrative($actor));
abort_unless($allowed || ($actor->can('manage-portal') && !app(\App\Services\PortalProfileAccess::class)->has($actor,'auditor')),403,'Acesso reservado ao âmbito já designado.');
$response=$next($request);
if (!$request->isMethod('GET') && $response->getStatusCode()<400) {
app(\App\Services\PortalProfileAccess::class)->audit($actor,'scoped_administration',[
'method'=>$request->method(),'route'=>$request->route()?->uri(),'scope'=>$interests?'assigned-interest':'catalogue-administration']);
$actor = $request->user();
abort_unless($actor, 403);
$scope = app(ScopedPortalAdministration::class);
$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/*',
'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',
'management/balcao/fontes') || ($request->isMethod('GET') && $path === 'management/balcao-install');
$fiscal = $request->is('api/v2/investment/admin/tax-rules', 'api/v2/investment/admin/tax-rules/*',
'api/v2/investment/admin/tax-rule-sources', 'management/investment/tax-rules/*');
$allowed = ($fiscal && SimulationTaxGovernanceAccess::maintainer($actor)) || ($interests ? $scope->interestOperator($actor) : ($catalogue && $scope->administrative($actor)));
abort_unless($allowed || ($actor->can('manage-portal') && ! app(PortalProfileAccess::class)->has($actor, 'auditor')), 403, 'Acesso reservado ao âmbito já designado.');
$response = $next($request);
if (! $request->isMethod('GET') && $response->getStatusCode() < 400) {
app(PortalProfileAccess::class)->audit($actor, 'scoped_administration', [
'method' => $request->method(), 'route' => $request->route()?->uri(), 'scope' => $fiscal ? 'fiscal-maintenance' : ($interests ? 'assigned-interest' : 'catalogue-administration')]);
}
return $response;
}
}
@@ -53,6 +53,7 @@ class HandleInertiaRequests extends Middleware
'name' => config('app.name'),
'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 +69,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'],
@@ -29,7 +29,9 @@ class ProtectProfileOpportunity
if ($record && ($record->owner_user_id !== null || str_starts_with($request->path(), 'minhas-oportunidades'))) {
abort_if($record->owner_user_id === null && str_starts_with($request->path(), 'minhas-oportunidades'), 403, 'Registo legado não convertido para a política v1.');
$access = app(PortalProfileAccess::class);
$access->authorize($request->user(), $request->isMethod('GET') || str_ends_with($request->path(), '/workflow') || str_ends_with($request->path(), '/preview') || $request->is('minhas-oportunidades/*/classification/review') ? 'view' : 'edit', $record);
// The promotional controller checks its own scoped disclosure capability,
// like workflow decisions; publication does not grant draft editing.
$access->authorize($request->user(), $request->isMethod('GET') || str_ends_with($request->path(), '/workflow') || str_ends_with($request->path(), '/preview') || $request->is('minhas-oportunidades/*/classification/review', 'minhas-oportunidades/*/divulgacao/previa', 'minhas-oportunidades/*/divulgacao/confirmar') ? 'view' : 'edit', $record);
abort_if($request->is('management/opportunity-origin/apply'), 403, 'O cadastro privado v1 não usa a classificação editorial dos exemplos legados.');
abort_if($request->isMethod('POST') && str_ends_with($request->path(), '/lifecycle') && ! str_contains($request->path(), '/resources/'), 403, 'Use o workflow com decisões segregadas; o perfil não concede exclusão.');
}
@@ -21,7 +21,7 @@ class ResolveSimulationTaxParametersRequest extends FormRequest
'component' => ['required', 'in:IRPC,IVA,ISPC,IRPS,INSS,RETENCOES,DIREITOS_ADUANEIROS,ICE'],
'period_start' => ['required', 'date_format:Y-m-d'],
'period_end' => ['required', 'date_format:Y-m-d', 'after_or_equal:period_start'],
'context' => ['sometimes', 'array:fiscal_periodicity,fiscal_operation,fiscal_residency_assessment,fiscal_residency_country,fiscal_income_category,fiscal_establishments,fiscal_documentation,fiscal_customs_code,fiscal_vat_refund_state'],
'context' => ['sometimes', 'array:currency,fiscal_periodicity,fiscal_operation,fiscal_residency_assessment,fiscal_residency_country,fiscal_income_category,fiscal_establishments,fiscal_documentation,fiscal_customs_code,fiscal_vat_refund_state'],
'context.*' => ['string', 'max:160'],
];
}
@@ -33,7 +33,8 @@ class StoreLandAssistanceCaseRequest extends FormRequest
return [
'version' => [$this->isMethod('post') && ! $this->routeIs('land-assistance.support') ? 'nullable' : 'required', 'integer', 'min:1'],
'context_revision' => ['nullable', 'integer', 'min:0'], 'context_reviewed' => ['nullable', 'boolean'],
'payload' => ['required', 'array:support_kind,request_description,project_reference,investor_path_case_id,applicant_category,full_name,contact_person,email,phone,preferred_contact,country,declared_origin_country_code,nationality,representative_name,representative_role,representation_basis,nuit,constitution_or_residency_note,area_status,parcels,purpose,activity,known_occupants,occupant_description,known_improvements,improvements_description,prior_right_status,prior_right_description,conflict_status,conflict_description,official_process,attachments_to_follow,exploration_plan,consultation,additional_information,truth_declaration,contact_consent'],
'payload' => ['required', 'array:context_reuse,support_kind,request_description,project_reference,investor_path_case_id,applicant_category,full_name,contact_person,email,phone,preferred_contact,country,declared_origin_country_code,nationality,representative_name,representative_role,representation_basis,nuit,constitution_or_residency_note,area_status,parcels,purpose,activity,known_occupants,occupant_description,known_improvements,improvements_description,prior_right_status,prior_right_description,conflict_status,conflict_description,official_process,attachments_to_follow,exploration_plan,consultation,additional_information,truth_declaration,contact_consent'],
'payload.context_reuse' => ['nullable', 'string', 'max:10000'],
'payload.support_kind' => ['nullable', 'in:investment,home,family_exploitation,recognition,regularization,delimitation,change_of_use,transfer,existing_process,conflict_guidance,other'],
'payload.request_description' => ['nullable', 'string', 'max:4000'],
'payload.project_reference' => ['nullable', 'string', 'max:160'],
@@ -37,7 +37,7 @@ class StoreSimulationFormDraftRequest extends FormRequest
'contact_province', 'contact_district', 'preferred_channel',
];
$economic = [
'journey_type', 'detail_level', 'business_plan_snapshot', 'financial_tools_snapshot', 'project_team_snapshot', 'project_execution_snapshot', 'purpose', 'manifestation_type', 'preferred_language', 'project_title', 'project_summary', 'project_problem', 'target_customers', 'value_proposition', 'market_assumption', 'market_source', 'project_milestones', 'selection_labels', 'project_type',
'context_applicability_snapshot', 'investment_regime', 'context_reuse', 'compatible_fields_snapshot', 'published_iva_base', 'published_iva_currency', 'published_iva_date', 'published_iva_scope', 'monthly_iva_settings', 'monthly_iva_snapshot', 'journey_type', 'detail_level', 'business_plan_snapshot', 'financial_tools_snapshot', 'cashflow_snapshot', 'project_team_snapshot', 'project_execution_snapshot', 'purpose', 'manifestation_type', 'preferred_language', 'project_title', 'project_summary', 'project_problem', 'target_customers', 'value_proposition', 'market_assumption', 'market_source', 'project_milestones', 'selection_labels', 'project_type',
'competitor_assumption', 'sales_channel_assumption', 'marketing_approach', 'location_rationale', 'project_risk_responses_snapshot', 'project_questionnaire_version',
'customer_validation_status', 'cost_estimate_status', 'location_assessment_status', 'funding_status', 'receivables_timing_status',
'project_type_other', 'sector_id', 'activity_id', 'products_services', 'maturity_stage', 'expected_start_date',
@@ -50,7 +50,7 @@ class StoreSimulationFormDraftRequest extends FormRequest
'environmental_category', 'community_benefits', 'tax_calculation', 'fiscal_scenario_mode', 'base', 'rate', 'reduction',
'sales_taxable', 'sales_exempt', 'sales_zero_rate', 'exports', 'deductible_costs', 'non_deductible_costs',
'carried_losses', 'payroll', 'outbound_payments', 'imports', 'claimed_benefits', 'reference_date',
'fiscal_period', 'fiscal_periodicity', 'fiscal_month', 'fiscal_quarter', 'fiscal_reference_date', 'irps_salary_view', 'fiscal_component', 'fiscal_income_category', 'fiscal_residency_assessment',
'fiscal_currency', 'fiscal_period', 'fiscal_periodicity', 'fiscal_month', 'fiscal_quarter', 'fiscal_reference_date', 'irps_salary_view', 'fiscal_component', 'fiscal_income_category', 'fiscal_residency_assessment',
'fiscal_operation', 'fiscal_activity', 'fiscal_establishments', 'fiscal_documentation',
'fiscal_customs_code', 'fiscal_vat_refund_state',
'offer_or_demand', 'opportunity_type', 'opportunity_type_other', 'opportunity_description',
@@ -71,6 +71,17 @@ class StoreSimulationFormDraftRequest extends FormRequest
'economic_snapshot.detail_level' => ['nullable', 'in:simple,advanced'],
'economic_snapshot.business_plan_snapshot' => ['nullable', 'string', 'max:60000'],
'economic_snapshot.financial_tools_snapshot' => ['nullable', 'string', 'max:30000'],
'economic_snapshot.cashflow_snapshot' => ['nullable', 'string', 'max:20000'],
'economic_snapshot.context_applicability_snapshot' => ['nullable', 'string', 'max:20000'],
'economic_snapshot.investment_regime' => ['nullable', 'in:general,industrial_park,zee,zfi'],
'economic_snapshot.context_reuse' => ['nullable', 'string', 'max:10000'],
'economic_snapshot.compatible_fields_snapshot' => ['nullable', 'string', 'max:10000'],
'economic_snapshot.published_iva_base' => ['nullable', 'string', 'max:40'],
'economic_snapshot.published_iva_currency' => ['nullable', 'string', 'max:3'],
'economic_snapshot.published_iva_date' => ['nullable', 'string', 'max:10'],
'economic_snapshot.published_iva_scope' => ['nullable', 'string', 'in:yes,no'],
'economic_snapshot.monthly_iva_settings' => ['nullable', 'string', 'max:2000'],
'economic_snapshot.monthly_iva_snapshot' => ['nullable', 'string', 'max:60000'],
'economic_snapshot.project_team_snapshot' => ['nullable', 'string', 'max:30000'],
'economic_snapshot.project_execution_snapshot' => ['nullable', 'string', 'max:30000'],
'economic_snapshot.project_risk_responses_snapshot' => ['nullable', 'string', 'max:30000'],
@@ -278,7 +289,7 @@ class StoreSimulationFormDraftRequest extends FormRequest
'currency', 'funds_origin_countries', 'environmental_category', 'tax_calculation', 'fiscal_scenario_mode', 'offer_or_demand',
'opportunity_type', 'confidentiality', 'publish_requested', 'terms_accepted', 'truth_declaration',
'reuse_simulation', 'contact_authorisation', 'publication_authorisation', 'public_fields_confirmation', 'validation_acknowledged',
'fiscal_period', 'fiscal_periodicity', 'fiscal_month', 'fiscal_quarter', 'fiscal_reference_date', 'irps_salary_view', 'fiscal_component', 'fiscal_income_category', 'fiscal_residency_assessment', 'fiscal_operation',
'fiscal_currency', 'fiscal_period', 'fiscal_periodicity', 'fiscal_month', 'fiscal_quarter', 'fiscal_reference_date', 'irps_salary_view', 'fiscal_component', 'fiscal_income_category', 'fiscal_residency_assessment', 'fiscal_operation',
'fiscal_documentation', 'fiscal_vat_refund_state'],
'personal_snapshot.selection_labels' => ['person_type', 'investor_origin', 'tax_residency_country', 'fiscal_residency_country', 'entity_stage', 'legal_form', 'preferred_channel'],
];
@@ -11,10 +11,71 @@ class StoreSimulationRequest extends FormRequest
return true;
}
public function messages(): array
{
return [
'required' => 'Preencha o campo :attribute. As entradas foram preservadas.',
'string' => 'Indique :attribute em formato textual.',
'regex' => 'Indique uma base decimal positiva ou zero, com ponto decimal e sem separadores de milhares.',
'max' => 'O campo :attribute excede o tamanho permitido.',
'in' => 'A opção indicada em :attribute não pertence a este cálculo.',
'date_format' => 'Indique uma data de exigibilidade válida.',
'boolean' => 'Indique se declara o enquadramento da operação.',
'prohibited' => 'O campo :attribute não pode ser definido pelo cliente neste cálculo.',
'array' => 'Indique uma lista válida em :attribute.',
'monthly_sales.min' => 'Indique pelo menos um mês de vendas.',
'monthly_sales.max' => 'Este lote aceita até 12 meses dentro de 2026.',
'monthly_sales.*.date.regex' => 'Indique o primeiro dia de um mês de 2026.',
'monthly_sales.*.date.distinct' => 'Cada mês deve aparecer apenas uma vez.',
'monthly_sales.*.amount.present' => 'Indique o valor das vendas ou deixe-o explicitamente sem informação.',
];
}
public function attributes(): array
{
return ['base' => 'base tributável', 'currency' => 'moeda', 'reference_date' => 'data de exigibilidade',
'operation' => 'operação', 'scope_declared' => 'declaração de enquadramento', 'scenario_kind' => 'tipo de cálculo',
'rule_id' => 'regra', 'rule_version' => 'versão da regra', 'rate' => 'taxa', 'reduction' => 'redução',
'monthly_sales' => 'vendas mensais', 'monthly_sales.*.date' => 'mês da projecção',
'monthly_sales.*.amount' => 'vendas declaradas do mês', 'sales_vat_basis' => 'tratamento do IVA nos preços'];
}
/** @return array<string, list<mixed>> */
public function rules(): array
{
if ($this->input('scenario_kind') === 'fiscal_rule_published') {
if ($this->exists('monthly_sales')) {
return [
'scenario_kind' => ['required', 'in:fiscal_rule_published'],
'monthly_sales' => ['required', 'array', 'min:1', 'max:12'],
'monthly_sales.*' => ['required', 'array:date,amount'],
'monthly_sales.*.date' => ['required', 'date_format:Y-m-d', 'regex:/^2026-\d{2}-01$/D', 'distinct'],
'monthly_sales.*.amount' => ['present', 'nullable', 'string', 'max:40', 'regex:/^\d+(?:\.\d+)?$/D'],
'sales_vat_basis' => ['required', 'in:net,included,unknown'],
'currency' => ['required', 'in:MZN'],
'scope_declared' => ['required', 'boolean'],
'organization_id' => ['nullable', 'uuid', 'exists:organizations,id'],
'base' => ['prohibited'], 'rate' => ['prohibited'], 'reduction' => ['prohibited'],
'rule_id' => ['prohibited'], 'rule_version' => ['prohibited'], 'fiscal_preview' => ['prohibited'],
];
}
return [
'scenario_kind' => ['required', 'in:fiscal_rule_published'],
'base' => ['required', 'string', 'max:40', 'regex:/^\d+(?:\.\d+)?$/'],
'currency' => ['required', 'in:MZN'],
'reference_date' => ['required', 'date_format:Y-m-d'],
'operation' => ['required', 'in:internal_general_net'],
'scope_declared' => ['required', 'boolean'],
'organization_id' => ['nullable', 'uuid', 'exists:organizations,id'],
'rate' => ['prohibited'], 'reduction' => ['prohibited'],
'rule_id' => ['prohibited'], 'rule_version' => ['prohibited'],
'fiscal_preview' => ['prohibited'],
];
}
return [
'scenario_kind' => ['sometimes', 'in:custom_planning'],
'base' => ['required', 'numeric', 'min:0'],
'rate' => ['required', 'numeric', 'between:0,100'],
'reduction' => ['required', 'numeric', 'between:0,100'],
+47 -22
View File
@@ -4,6 +4,7 @@ namespace App\Jobs;
use App\Models\Simulation;
use App\Services\SimulationExecution;
use App\Support\IsolatedQaDatabase;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
@@ -62,29 +63,39 @@ class RunSimulation implements ShouldQueue
throw new \RuntimeException('Simulation has no scenario.');
}
$input = json_decode($scenario->parameters, true, flags: JSON_THROW_ON_ERROR);
$row = DB::selectOne('select (CAST(? AS numeric) * CAST(? AS numeric) / 100 * (1 - CAST(? AS numeric) / 100))::text as value', [$input['base'], $input['rate'], $input['reduction']]);
$resultId = (string) Str::uuid();
$engineVersion = 'portal-arithmetic-v1+simulation-v'.$simulation->version_no;
$result = ['base' => $input['base'], 'rate_percent' => $input['rate'], 'reduction_percent' => $input['reduction'], 'scenario_kind' => 'custom_planning', 'indicative_total' => $row->value,
'currency' => null, 'legal_interpretation' => false, 'formula_version' => 'portal-arithmetic-v1',
'contract_version' => 1,
'context' => [
'reference_date' => substr((string) $scenario->created_at, 0, 10),
'currency' => null,
'currency_fraction_digits' => null,
'scenario_id' => (string) $scenario->id,
'scenario_version' => (int) $scenario->version_no,
'engine_version' => $engineVersion,
'ruleset_version' => null,
],
'result' => [
'status' => 'calculated',
'value' => (string) $row->value,
'unit' => null,
'explanation' => 'Resultado aritmético determinístico; não constitui cálculo nem conclusão fiscal.',
'missing_inputs' => [],
'source_ids' => [],
]];
if (in_array($input['scenario_kind'] ?? null, ['fiscal_rule_qa_preview', 'fiscal_rule_published'], true)) {
if ($input['scenario_kind'] === 'fiscal_rule_qa_preview') {
abort_unless(IsolatedQaDatabase::allowsSchemaChanges(), 404);
}
$result = $input['fiscal_preview'];
$engineVersion = $result['context']['engine_version'];
$result['context']['scenario_id'] = (string) $scenario->id;
$result['context']['scenario_version'] = (int) $scenario->version_no;
} else {
$row = DB::selectOne('select (CAST(? AS numeric) * CAST(? AS numeric) / 100 * (1 - CAST(? AS numeric) / 100))::text as value', [$input['base'], $input['rate'], $input['reduction']]);
$result = ['base' => $input['base'], 'rate_percent' => $input['rate'], 'reduction_percent' => $input['reduction'], 'scenario_kind' => 'custom_planning', 'indicative_total' => $row->value,
'currency' => null, 'legal_interpretation' => false, 'formula_version' => 'portal-arithmetic-v1',
'contract_version' => 1,
'context' => [
'reference_date' => substr((string) $scenario->created_at, 0, 10),
'currency' => null,
'currency_fraction_digits' => null,
'scenario_id' => (string) $scenario->id,
'scenario_version' => (int) $scenario->version_no,
'engine_version' => $engineVersion,
'ruleset_version' => null,
],
'result' => [
'status' => 'calculated',
'value' => (string) $row->value,
'unit' => null,
'explanation' => 'Resultado aritmético determinístico; não constitui cálculo nem conclusão fiscal.',
'missing_inputs' => [],
'source_ids' => [],
]];
}
$json = json_encode($result, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
$canonicalResult = DB::selectOne('select CAST(? AS jsonb)::text as canonical', [$json])->canonical;
$resultHash = hash('sha256', $canonicalResult);
@@ -96,11 +107,25 @@ class RunSimulation implements ShouldQueue
DB::table('simulation_jobs')->where('id', $job->id)->update(['status' => 'succeeded', 'finished_at' => now(), 'updated_at' => now()]);
DB::table('simulation_reports')->insert(['id' => (string) Str::uuid(), 'simulation_id' => $simulation->id, 'version_no' => $scenario->version_no,
'status' => 'ready', 'report_snapshot' => $json, 'report_sha256' => $resultHash, 'created_at' => now(), 'updated_at' => now()]);
$sourceJson = json_encode($input, JSON_THROW_ON_ERROR);
$sourceJson = json_encode(isset($result['fiscal_provenance']) ? [
'base' => $result['base'], 'currency' => $result['currency'],
'reference_date' => $result['context']['reference_date'],
'context' => $result['fiscal_provenance']['context'], 'scope_status' => $result['fiscal_provenance']['scope_status'],
] : $input, JSON_THROW_ON_ERROR);
$canonicalSource = DB::selectOne('select CAST(? AS jsonb)::text as canonical', [$sourceJson])->canonical;
DB::table('simulation_result_sources')->insertOrIgnore(['id' => (string) Str::uuid(), 'result_id' => $resultId,
'source_type' => 'user_input', 'source_id' => $scenario->id, 'source_version' => (string) $scenario->version_no,
'source_snapshot' => $sourceJson, 'source_sha256' => hash('sha256', $canonicalSource), 'created_at' => now()]);
if (isset($result['fiscal_provenance'])) {
$provenance = $result['fiscal_provenance'];
$provenanceJson = json_encode($provenance, JSON_THROW_ON_ERROR);
$canonicalProvenance = DB::selectOne('select CAST(? AS jsonb)::text as canonical', [$provenanceJson])->canonical;
DB::table('simulation_result_sources')->insert([
'id' => (string) Str::uuid(), 'result_id' => $resultId, 'source_type' => $input['scenario_kind'] === 'fiscal_rule_published' ? 'tax_rule_published' : 'tax_rule_draft',
'source_id' => $provenance['rule']['id'], 'source_version' => (string) $provenance['rule']['rule_version'],
'source_snapshot' => $provenanceJson, 'source_sha256' => hash('sha256', $canonicalProvenance), 'created_at' => now(),
]);
}
DB::table('simulation_audit_events')->insert(['simulation_id' => $simulation->id, 'actor_user_id' => $simulation->owner_user_id,
'action' => 'simulation.completed', 'correlation_id' => $simulation->id, 'data' => json_encode(['result_sha256' => hash('sha256', $json), 'attempt' => $job->attempts + 1], JSON_THROW_ON_ERROR), 'occurred_at' => now()]);
}, 3);
+5
View File
@@ -16,4 +16,9 @@ class InvestmentLegalSource extends Model
use HasUuids;
protected $guarded = ['id'];
protected function casts(): array
{
return ['fiscal_acceptance' => 'array'];
}
}
+2 -1
View File
@@ -68,7 +68,8 @@ class Opportunity extends Model
$scope = $public ? 'public' : ($visible && $this->owner_user_id !== null ? 'restricted' : 'unpublished');
return ['scope' => $scope, 'public_catalog' => $public, 'label' => match ($scope) {
'public' => 'Publicada no catálogo público',
'public' => data_get($this->workflow_context, 'publication.purpose') === 'promotional_preparation'
? 'Resumo divulgado — Em preparação; não autoriza execução' : 'Publicada no catálogo público',
'restricted' => 'Disponibilizada com acesso restrito — perfis e organização autorizados',
default => 'Sem versão disponibilizada; aprovação interna é uma etapa distinta',
}];
+49
View File
@@ -0,0 +1,49 @@
<?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 ($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];
}
}
+3 -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);
+31 -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;
}
@@ -308,6 +308,18 @@ class InvestmentCatalogue
/** @return array<string,mixed> */
public function context(int $id, bool $management = false): array
{
$opportunity = Opportunity::findOrFail($id);
if (! $management && data_get($opportunity->workflow_context, 'publication.purpose') === OpportunityPromotion::PURPOSE) {
// Promotional disclosure includes only its explicit public coverage.
// Private profiles, source evidence and other dossier relations are not auto-published.
$public = ['profile' => null, 'facets' => collect(), 'zones' => collect(), 'classification' => [], 'source_evidence' => [],
'primary_territory_path' => app(InvestmentGeography::class)->path($opportunity->primary_territory_id, true),
'opportunity_locations' => app(InvestmentRelations::class)->opportunityLocations($id)];
foreach (self::RELATIONS as $relation => $_) $public[$relation] = collect();
$public['locations'] = DB::table('opportunity_locations as l')->join('geo_locations as g', 'g.id', '=', 'l.geo_location_id')
->where('l.opportunity_id', $id)->where('g.data_classification', 'public')->whereNull('g.deleted_at')->get(['g.id', 'g.name']);
return $public;
}
$result = ['profile' => DB::table('opportunity_profiles')->where('opportunity_id', $id)->first(),
'facets' => DB::table('opportunity_facets as f')->join('investment_reference_options as o', 'o.id', '=', 'f.option_id')
->where('f.opportunity_id', $id)->get(['o.id', 'o.group', 'o.label'])];
+118 -24
View File
@@ -5,6 +5,7 @@ namespace App\Services;
use App\Models\ExistingBusiness;
use App\Models\Opportunity;
use App\Models\Sector;
use App\Models\SimulationFormDraft;
use App\Models\User;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Support\Arr;
@@ -12,6 +13,7 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Symfony\Component\HttpKernel\Exception\HttpException;
/** Context belongs to the existing encrypted dossier. Sources remain read-only. */
@@ -21,7 +23,7 @@ final class InvestorContext
public const SOURCE = 'investor-context';
public const FIELDS = ['project_title', 'project_summary', 'sector_id', 'sector', 'province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location'];
public const FIELDS = ['project_title', 'project_summary', 'sector_id', 'sector', 'province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location', 'currency', 'expected_start_date', 'fiscal_period', 'fiscal_periodicity', 'fiscal_month', 'investor_origin', 'person_type', 'entity_stage', 'tax_residency_country', 'fiscal_residency_country', 'activity_id', 'project_type', 'maturity_stage', 'special_zone_id', 'investment_regime', ...PrivateInvestmentPreparation::FIELDS];
public function __construct(private readonly VirtualDesk $desk) {}
@@ -34,8 +36,26 @@ final class InvestorContext
{
$payload = $this->desk->decode($case->payload);
return $payload['investor_path']['context'] ?? ['revision' => 0, 'selection' => [],
$context = $payload['investor_path']['context'] ?? ['revision' => 0, 'selection' => [],
'fields' => ['project_title' => $case->title], 'provenance' => ['project_title' => ['kind' => 'dossier', 'id' => $case->id, 'version' => $case->version]], 'review_steps' => []];
// Explicit dossier anchor only: never the account's most recent simulation or another owner's draft.
if (($case->simulation_id ?? null) && request()->user()?->id === (int) $case->user_id) {
$draft = SimulationFormDraft::whereKey($case->simulation_id)->where('owner_user_id', $case->user_id)->whereNull('deleted_at')->first();
if ($draft) {
$values = [...SimulationFormDraft::decryptSnapshot($draft->personal_snapshot), ...SimulationFormDraft::decryptSnapshot($draft->economic_snapshot)];
foreach (Arr::only($values, self::FIELDS) as $key => $value) {
if (is_scalar($value) && (string) $value !== '' && (! array_key_exists($key, $context['fields']) || ($context['provenance'][$key]['kind'] ?? null) === 'linked_simulation')) {
$context['fields'][$key] = (string) $value;
$context['provenance'][$key] = ['kind' => 'linked_simulation', 'id' => $draft->id, 'version' => $draft->version_no, 'sha256' => $draft->snapshot_sha256];
}
}
}
}
$zone = $context['selection']['park'] ?? null;
$context['fields']['investment_regime'] = $zone['fields']['investment_regime'] ?? 'general';
$context['fields']['special_zone_id'] = $zone['id'] ?? '';
return $context;
}
public function dependencyHash(array $selection): string
@@ -53,17 +73,20 @@ final class InvestorContext
$documents = [];
$visibility = 'public';
$state = null;
$zoneAnalysis = null;
if ($kind === 'opportunity') {
abort_unless(ctype_digit($id), 422, 'Identificador de oportunidade inválido.');
$o = Opportunity::with('sector')->findOrFail((int) $id);
$published = Opportunity::published()->whereKey($o->id)->exists();
if (!$published) {
if (! $published) {
abort_unless(request()->user() && $o->status !== 'archived' && app(PortalProfileAccess::class)->canView(request()->user(), $o), 404, 'Ficha privada indisponível neste âmbito.');
$visibility = 'private';
$state = app(PortalProfileAccess::class)->state($o);
}
if (request()->user()) $documents = app(OpportunityLibrarySupport::class)->snapshot(request()->user(), $o);
$draft = !$published ? app(OpportunityManagement::class)->values($o) : [];
if (request()->user()) {
$documents = app(OpportunityLibrarySupport::class)->snapshot(request()->user(), $o);
}
$draft = ! $published ? app(OpportunityManagement::class)->values($o) : [];
$title = $draft['canonical']['official_title'] ?? $o->title;
$href = $published ? '/oportunidades/'.$o->slug : '/minhas-oportunidades/'.$o->id.'/view';
$version = $o->management_version;
@@ -122,19 +145,28 @@ final class InvestorContext
$fields = $this->territoryFields($id);
} elseif ($kind === 'park') {
abort_unless(Str::isUuid($id), 422);
$p = app(InvestmentZones::class)->visible()->where('category', 'industrial_park')->findOrFail($id);
$p = app(InvestmentZones::class)->visible()->whereIn('category', ['industrial_park', 'zee', 'zfi'])->findOrFail($id);
$title = $p->name;
$fields = ['special_zone_id' => (string) $p->id, 'investment_regime' => (string) $p->category];
$version = $p->version_no;
$zoneAnalysis = ['conditions' => app(InvestmentZoneOperationalDetails::class)->publicData($p)['conditions'] ?? [],
'source' => $p->legal_source_id ? DB::table('investment_legal_sources')->where('id', $p->legal_source_id)->first(['title', 'url', 'diploma', 'article', 'valid_from', 'valid_until']) : null,
'recognition' => 'undetermined', 'executable_benefit' => false];
$href = '/zonas/'.$id;
if ($p->primary_territory_id) {
$fields = $this->territoryFields($p->primary_territory_id);
$fields = [...$fields, ...$this->territoryFields($p->primary_territory_id)];
}
} else {
abort(422, 'Tipo de referência inválido.');
}
$fields = array_filter($fields, fn ($v) => is_scalar($v) && trim((string) $v) !== '');
$record = ['key' => $key, 'kind' => $kind, 'id' => $id, 'title' => $title, 'version' => $version, 'href' => $href, 'fields' => $fields];
if ($documents || $visibility === 'private') $record += ['documents' => $documents, 'visibility' => $visibility, 'state' => $state];
if ($zoneAnalysis !== null) {
$record['analysis'] = $zoneAnalysis;
}
if ($documents || $visibility === 'private') {
$record += ['documents' => $documents, 'visibility' => $visibility, 'state' => $state];
}
return [...$record, 'sha256' => hash('sha256', json_encode($record, JSON_THROW_ON_ERROR))];
}
@@ -170,7 +202,11 @@ final class InvestorContext
}
if (request()->user()) {
$private = app(PortalProfileAccess::class)->scope(Opportunity::query(), request()->user())->where('status', '!=', 'published')->where('status', '!=', 'archived')->when($query, fn ($q) => $q->where('title', 'ilike', '%'.$query.'%'))->orderBy('title')->limit(51)->get();
foreach ($private as $o) if (app(PortalProfileAccess::class)->canView(request()->user(), $o)) $items[] = ['key' => 'opportunity:'.$o->id, 'title' => $o->title.' — preparação privada', 'kind' => 'opportunity'];
foreach ($private as $o) {
if (app(PortalProfileAccess::class)->canView(request()->user(), $o)) {
$items[] = ['key' => 'opportunity:'.$o->id, 'title' => $o->title.' — preparação privada', 'kind' => 'opportunity'];
}
}
}
if (config('existing-business.enabled')) {
foreach (ExistingBusiness::published()->when($query, fn ($q) => $q->where('public_title', 'ilike', '%'.$query.'%'))->orderBy('public_title')->limit(51)->get(['id', 'public_title']) as $o) {
@@ -179,7 +215,7 @@ final class InvestorContext
}
return ['opportunities' => $items, 'search_limited' => count($items) > 50, 'sectors' => Sector::choices()->orderBy('name')->get(['id', 'name', 'slug']),
'parks' => app(InvestmentZones::class)->visible()->where('category', 'industrial_park')->orderBy('name')->get(['id', 'name', 'version_no'])];
'parks' => app(InvestmentZones::class)->visible()->whereIn('category', ['industrial_park', 'zee', 'zfi'])->orderBy('name')->get(['id', 'name', 'category', 'version_no'])];
}
public function current(User $actor): array
@@ -232,10 +268,21 @@ final class InvestorContext
private function input(array $input): array
{
abort_if(array_diff(array_keys($input), ['version', 'selection', 'declarations', 'replace_fields', 'preview_hash', 'operation_key', 'confirm', '_token']), 422, 'Campo de controlo desconhecido.');
abort_if(array_diff(array_keys($input), ['version', 'selection', 'declarations', 'replace_fields', 'preview_hash', 'operation_key', 'confirm', 'document_links', '_token']), 422, 'Campo de controlo desconhecido.');
return Validator::make($input, ['version' => 'required|integer|min:1', 'selection' => 'present|array:opportunity,sector,territory,park',
'selection.*' => 'nullable|string|max:100', 'declarations' => 'nullable|array:project_title,project_summary', 'declarations.project_title' => 'nullable|string|max:200', 'declarations.project_summary' => 'nullable|string|max:5000',
abort_if(! app(PrivateInvestmentPreparation::class)->available() && (! empty($input['document_links']) || array_intersect(array_keys(is_array($input['declarations'] ?? null) ? $input['declarations'] : []), PrivateInvestmentPreparation::FIELDS)), 409, 'Esta preparação ainda não está disponível neste ambiente. As respostas foram preservadas.');
$inputRules = [
'document_links' => 'sometimes|array:'.implode(',', PrivateInvestmentPreparation::DOCUMENTS),
'document_links.*' => 'nullable|uuid',
];
foreach (app(PrivateInvestmentPreparation::class)->questions() as $question) {
$inputRules['declarations.'.$question['key']] = isset($question['options']) ? ['nullable', Rule::in(array_keys($question['options']))] : ['nullable', 'regex:/^\d+(?:\.\d+)?$/D', 'numeric', 'min:0', 'max:999999999999999999'];
}
return Validator::make($input, [...$inputRules, 'version' => 'required|integer|min:1', 'selection' => 'present|array:opportunity,sector,territory,park',
'selection.*' => 'nullable|string|max:100', 'declarations' => 'nullable|array:project_title,project_summary,currency,expected_start_date,investor_origin,person_type,entity_stage,tax_residency_country,fiscal_residency_country,project_type,maturity_stage,'.implode(',', PrivateInvestmentPreparation::FIELDS), 'declarations.project_title' => 'nullable|string|max:200', 'declarations.project_summary' => 'nullable|string|max:5000',
'declarations.currency' => 'nullable|regex:/^[A-Z]{3}$/', 'declarations.expected_start_date' => 'nullable|date_format:Y-m-d', 'declarations.investor_origin' => 'nullable|in:national,foreign,mixed', 'declarations.person_type' => 'nullable|in:singular,collective,sole_trader,consortium', 'declarations.entity_stage' => 'nullable|in:existing,being_formed', 'declarations.tax_residency_country' => ['nullable', 'string', app(CountryCatalogue::class)->rule()], 'declarations.fiscal_residency_country' => ['nullable', 'string', app(CountryCatalogue::class)->rule()], 'declarations.project_type' => 'nullable|string|max:100', 'declarations.maturity_stage' => 'nullable|string|max:100',
'replace_fields' => 'nullable|array', 'replace_fields.*' => 'string|in:'.implode(',', self::FIELDS)])->validate();
}
@@ -247,6 +294,15 @@ final class InvestorContext
abort_unless((int) $case->version === $data['version'], 409, 'O dossier mudou; recupere a versão atual.');
app(InvestorPath::class)->view($actor, $id);
$old = $this->state($case);
$links = $old['preparation_documents'] ?? [];
foreach ($data['document_links'] ?? [] as $key => $document) {
if ($document) {
abort_unless(DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $id)->where('d.id', $document)->whereNull('d.deleted_at')->exists(), 422, 'Seleccione um documento já associado a este dossier; os dados anteriores foram preservados.');
$links[$key] = $document;
} else {
unset($links[$key]);
}
}
$selection = $old['selection'];
foreach ($data['selection'] as $slot => $key) {
if ($key === null || $key === '') {
@@ -299,7 +355,7 @@ final class InvestorContext
}
// Remove inherited subordinate geography only when its source was actually replaced.
foreach ($fields as $key => $value) {
if (! array_key_exists($key, $proposed) && in_array($key, ['province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location', 'sector_id', 'sector'], true) && isset($provenance[$key]['key'])) {
if (! array_key_exists($key, $proposed) && in_array($key, ['province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location', 'sector_id', 'sector', 'special_zone_id', 'investment_regime'], true) && isset($provenance[$key]['key'])) {
unset($fields[$key],$provenance[$key]);
}
}
@@ -314,14 +370,32 @@ final class InvestorContext
$changed[] = $slot;
}
}
$dependencyChanged = $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection);
$changedFields = array_column($replaced, 'field');
$conditionsChanged = (bool) array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary']));
$dependencyChanged = $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || $conditionsChanged;
$privateConditionsChanged = (bool) array_intersect($changedFields, PrivateInvestmentPreparation::FIELDS);
$calculationInputsChanged = $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || (bool) array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary', ...array_diff(PrivateInvestmentPreparation::FIELDS, ['total_investment', 'area_required', 'area_unit'])]));
$payload = $this->desk->decode($case->payload);
$results = $payload['investor_path']['results'] ?? [];
$requests = DB::table('desk_requests')->where('case_id', $id)->orderBy('id')->get(['id', 'status', 'protocol', 'version'])->all();
$steps = $dependencyChanged ? array_values(array_unique([...(array_intersect($changed, ['opportunity', 'sector']) ? ['S02', 'S03', 'S04', 'S08', 'S12', 'S13', 'S14', 'S15', 'S17'] : []), ...(array_intersect($changed, ['territory', 'park']) ? ['S08', 'S09', 'S10', 'S11', 'S17'] : [])])) : [];
if (array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary', ...PrivateInvestmentPreparation::FIELDS]))) {
$steps = array_values(array_unique([...$steps, 'S02', 'S03', 'S08', 'S12', 'S13', 'S17']));
}
if ($privateConditionsChanged) {
$steps = array_values(array_unique([...$steps, 'S02', 'S05', 'S06', 'S07']));
}
$privatePreparation = null;
if (app(PrivateInvestmentPreparation::class)->available()) {
$docs = $this->preparationDocuments($id);
$beforePreparation = app(PrivateInvestmentPreparation::class)->assess($old, $docs);
$afterPreparation = app(PrivateInvestmentPreparation::class)->assess(['fields' => $fields, 'selection' => $selection, 'preparation_documents' => $links], $docs);
$privatePreparation = ['before' => $beforePreparation, 'after' => $afterPreparation];
}
$plan = ['case_id' => $id, 'version' => (int) $case->version, 'revision' => (int) $old['revision'], 'selection' => $selection, 'fields' => $fields, 'provenance' => $provenance,
'updated' => $replaced, 'maintained' => $kept, 'changed_dimensions' => $changed, 'dependent_results' => (int) $case->user_id === $actor->id && $dependencyChanged ? array_map(fn ($r) => ['id' => $r['id'], 'engine' => $r['engine']], $results) : [],
'requests' => $requests, 'review_steps' => $steps, 'dependency_changed' => $dependencyChanged, 'documents_preserved' => DB::table('desk_documents')->where('case_id', $id)->count(),
'preparation_documents' => $links, 'private_investment' => $privatePreparation,
'updated' => $replaced, 'maintained' => $kept, 'changed_dimensions' => $changed, 'dependent_results' => (int) $case->user_id === $actor->id && $calculationInputsChanged ? array_map(fn ($r) => ['id' => $r['id'], 'engine' => $r['engine']], $results) : [],
'requests' => $requests, 'review_steps' => $steps, 'dependency_changed' => $dependencyChanged, 'calculation_inputs_changed' => $calculationInputsChanged, 'documents_preserved' => DB::table('desk_documents')->where('case_id', $id)->count(),
'notice' => 'Não confirma incentivos, licenças, disponibilidade de terra, reserva ou autorização. Submissões e decisões anteriores mantêm o conteúdo enviado.'];
$hash = hash_hmac('sha256', json_encode(Arr::sortRecursive([$actor->id, $data, $plan]), JSON_THROW_ON_ERROR), config('app.key'));
@@ -349,7 +423,7 @@ final class InvestorContext
$before = $this->state($case);
$plan = $this->preview($actor, $id, $input);
abort_unless(hash_equals($plan['preview_hash'], $control['preview_hash']), 409, 'O dossier ou uma fonte mudou desde a prévia; compare novamente.');
if (! $plan['changed_dimensions'] && ! $plan['updated'] && empty($input['declarations']) && empty($input['replace_fields'])) {
if (! $plan['changed_dimensions'] && ! $plan['updated'] && empty($input['declarations']) && empty($input['replace_fields']) && empty($input['document_links'])) {
$result = ['case_id' => $id, 'version' => (int) $case->version, 'revision' => $before['revision'], 'changed' => false, 'replayed' => false];
DB::table('audit_events')->insert(['source_system' => self::SOURCE, 'entity_type' => 'desk_cases', 'entity_id' => $id, 'actor_user_id' => $actor->id, 'correlation_id' => $control['operation_key'], 'action' => 'context_unchanged', 'data_classification' => 'restricted', 'after_data' => json_encode(['case_id' => $id, 'request_hash' => $requestHash, 'result' => $result], JSON_THROW_ON_ERROR)]);
@@ -361,9 +435,9 @@ final class InvestorContext
foreach ($plan['review_steps'] as $step) {
$review[$step] = ['revision' => $revision, 'reason' => 'O contexto mudou; reveja os pressupostos e a evidência aplicável.'];
}
$context = ['revision' => $revision, 'selection' => $plan['selection'], 'fields' => $plan['fields'], 'provenance' => $plan['provenance'], 'review_steps' => $review,
'dependency_hash' => $this->dependencyHash($plan['selection']), 'updated_at' => now()->toIso8601String(), 'updated_by' => $actor->id];
if ($plan['dependency_changed']) {
$context = ['preparation_documents' => $plan['preparation_documents'], 'preparation_snapshot' => $plan['private_investment']['after'] ?? $before['preparation_snapshot'] ?? null, 'revision' => $revision, 'selection' => $plan['selection'], 'fields' => $plan['fields'], 'provenance' => $plan['provenance'], 'review_steps' => $review,
'dependency_hash' => $this->dependencyHash(['selection' => $plan['selection'], 'fields' => Arr::except($plan['fields'], ['project_title', 'project_summary'])]), 'updated_at' => now()->toIso8601String(), 'updated_by' => $actor->id];
if ($plan['calculation_inputs_changed']) {
foreach ($payload['investor_path']['results'] ?? [] as $i => $r) {
$payload['investor_path']['results'][$i] = [...$r, 'context_state' => 'review_required', 'context_invalidated_revision' => $revision, 'context_invalidated_at' => now()->toIso8601String()];
}
@@ -384,6 +458,8 @@ final class InvestorContext
abort_unless($this->desk->owns($actor, $case, false) || $actor->can('manage-portal'), 403);
$ctx = $this->state($case);
$path = app(InvestorPath::class)->view($actor, $id);
$ctx['path_applicability'] = ['definition_id' => $path['definition_id'], 'definition_version' => $path['definition_version'], 'source_sha256' => $path['source_sha256'],
'steps' => array_values(array_map(fn ($step) => Arr::only($step, ['code', 'title', 'applicability', 'reason', 'state', 'context_review_required']), array_filter($path['steps'], fn ($step) => ! empty($step['branch']))))];
$simulatorRevision = null;
if ((int) $case->user_id === $actor->id && $case->simulation_id) {
@@ -396,18 +472,36 @@ final class InvestorContext
}
}
return ['simulator_context_revision' => $simulatorRevision, 'case' => $path['case'], 'context' => $ctx, 'steps' => array_map(fn ($s) => Arr::only($s, ['code', 'title', 'state', 'context_review_required']), $path['steps']),
$preparation = app(PrivateInvestmentPreparation::class)->available() ? app(PrivateInvestmentPreparation::class)->assess($ctx, $this->preparationDocuments($id)) : null;
$ctx['preparation_assessment'] = $preparation;
return ['private_investment' => $preparation, 'preparation_documents' => $this->preparationDocuments($id), 'simulator_context_revision' => $simulatorRevision, 'case' => $path['case'], 'context' => $ctx, 'steps' => array_map(fn ($s) => Arr::only($s, ['code', 'title', 'state', 'context_review_required']), $path['steps']),
'can_write' => $path['canWrite'], 'next' => $path['next'], 'results' => $path['results'], 'requests' => $path['requests'],
'prefill' => $this->prefill($ctx), 'notice' => 'A seleção apoia a preparação e não confirma benefícios, licenças, direitos ou disponibilidade.'];
}
public function preparationDocuments(string $id): array
{
return DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $id)->whereNull('d.deleted_at')->get(['l.id', 'l.document_id', 'd.file_name', 'd.status', 'd.malware_scan_status', 'd.version_no', 'd.sha256'])->map(fn ($row) => (array) $row)->all();
}
public function prefill(array $ctx): array
{
$f = $ctx['fields'];
$sim = Arr::only($f, ['project_title', 'project_summary', 'sector_id', 'province_id', 'district_id', 'admin_post_id', 'locality_id']);
$sim = Arr::except(Arr::only($f, self::FIELDS), ['sector', 'province', 'district', 'location', ...array_diff(PrivateInvestmentPreparation::FIELDS, ['total_investment', 'area_required', 'area_unit'])]);
$zone = $ctx['selection']['park'] ?? null;
$sim['context_applicability_snapshot'] = json_encode([
'version' => 1, 'context_revision' => $ctx['revision'],
'zone' => $zone ? Arr::only($zone, ['id', 'title', 'href', 'version', 'sha256', 'fields', 'analysis']) : null,
'provenance' => $ctx['provenance'],
'path' => $ctx['path_applicability'] ?? null,
'preparation' => isset($ctx['preparation_assessment']) ? Arr::except($ctx['preparation_assessment'], ['questions']) : null,
'fields' => Arr::only($f, ['investor_origin', 'tax_residency_country', 'entity_stage', 'currency', 'expected_start_date', 'investment_regime']),
'notice' => 'Referências e declarações para preparação; sem reconhecimento automático de elegibilidade ou benefício.',
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
return ['simulator' => $sim, 'land' => array_filter(['project_reference' => $f['project_title'] ?? null, 'activity' => $f['sector'] ?? null, 'purpose' => $f['project_summary'] ?? null, 'parcels' => [Arr::only($f, array_keys(InvestmentGeography::FIELDS))]], fn ($v) => $v !== null),
'desk' => array_filter(['title' => $f['project_title'] ?? null, 'territory_id' => $f['locality_id'] ?? $f['admin_post_id'] ?? $f['district_id'] ?? $f['province_id'] ?? null, 'location_source' => isset($f['province_id']) ? 'Referência consultada no contexto v'.($ctx['revision'] ?? 0).': '.($f['location'] ?? 'território identificado').'. Não confirma disponibilidade.' : null, 'description' => $f['project_summary'] ?? null, 'sector' => $f['sector'] ?? null, ...Arr::only($f, array_keys(InvestmentGeography::FIELDS))], fn ($v) => $v !== null),
'submission' => array_filter(['sector' => $f['sector'] ?? null, 'objective' => $f['project_summary'] ?? null, 'location' => $f['location'] ?? null], fn ($v) => $v !== null)];
'submission' => array_filter(['currency' => $f['currency'] ?? null, 'sector' => $f['sector'] ?? null, 'objective' => $f['project_summary'] ?? null, 'location' => $f['location'] ?? null], fn ($v) => $v !== null)];
}
}
+8 -3
View File
@@ -136,7 +136,7 @@ final class InvestorPath
$documents = DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')
->where('l.case_id', $id)->whereNull('d.deleted_at')
->when(! $owner && ! $manager, fn ($q) => $q->where(fn ($p) => $p->whereNull('l.request_id')->orWhereIn('l.request_id', $requests->pluck('id'))))
->get(['l.id', 'l.document_id', 'd.file_name', 'd.status', 'd.malware_scan_status']);
->get(['l.id', 'l.document_id', 'd.file_name', 'd.status', 'd.malware_scan_status', 'd.version_no', 'd.sha256']);
// Only the private draft owner can receive the financial snapshot/result. Dossier access alone is insufficient.
$results = (int) $case->user_id === $actor->id ? ($path['results'] ?? []) : [];
$next = collect($steps)->first(fn ($step) => $step['preparation_allowed'] && ! in_array($step['state'], ['completed', 'approved', 'waiting_external', 'submitted', 'reviewing'], true));
@@ -145,12 +145,17 @@ final class InvestorPath
? LandAssistanceCase::where('user_id', $actor->id)->latest('updated_at')->get()
->first(fn ($land) => ($land->payload['investor_path_case_id'] ?? null) === $id) : null;
return ['case' => ['id' => $id, 'title' => $case->title, 'version' => $case->version, 'closed' => $case->closed_at !== null],
$preparation = ($owner || $manager) && app(PrivateInvestmentPreparation::class)->available() ? app(PrivateInvestmentPreparation::class)->assess($context, $documents->map(fn ($d) => (array) $d)->all()) : null;
if ($preparation && $preparation['enabled']) {
$next = ['code' => 'S06', 'title' => $preparation['next'], 'action' => 'Preparar', 'href' => '/investimentos/'.$id.'/contexto/editar#preparation-questions'];
}
return ['private_investment' => $preparation, 'case' => ['id' => $id, 'title' => $case->title, 'version' => $case->version, 'closed' => $case->closed_at !== null],
'journey_id' => $journey->id, 'definition_id' => $version->id, 'definition_version' => $version->version_no, 'source_sha256' => $definition['source_sha256'],
'phases' => $definition['phases'], 'steps' => $steps, 'facts' => $facts, 'fact_reasons' => $path['fact_reasons'] ?? [],
'fintech' => ($owner || $manager) && (isset($path['fintech']) || collect($path['sector_contexts'] ?? [])->contains(fn ($s) => in_array($s['slug'], ['fintech', 'tecnologias-de-informacao', 'mercado-financeiro'], true)) || collect($path['content_sources'] ?? [])->contains('return_href', '/tech-fintech')) ? app(FintechInvestment::class)->view($payload, $steps) : null,
'canWrite' => $owner && ! $case->closed_at, 'canReview' => $manager && (int) $case->user_id !== $actor->id && ! $case->closed_at,
'next' => $next ? ['code' => $next['code'], 'title' => $next['title'], 'action' => 'Preparar', 'href' => '#'.$next['code']] : null,
'next' => $next ? ['code' => $next['code'], 'title' => $next['title'], 'action' => 'Preparar', 'href' => $next['href'] ?? '#'.$next['code']] : null,
'investment_context' => ($owner || $manager) ? $context : null,
'results' => $results, 'requests' => $requests, 'documents' => $documents,
'zone_contexts' => ($owner || $manager) ? ($path['zone_contexts'] ?? []) : [],
+3
View File
@@ -27,6 +27,9 @@ class OpportunityManagement
public function values(Opportunity $record): array
{
$base = $record->only(self::FIELDS);
if (data_get($record->workflow_context, 'publication.purpose') === OpportunityPromotion::PURPOSE) {
$base = [...$base, ...($record->workflow_context['promotional_private_base'] ?? [])];
}
$draft = $record->management_draft ? json_decode($record->management_draft, true, 512, JSON_THROW_ON_ERROR) : [];
return [...$base, ...($this->closed($record) ? $draft : array_intersect_key($draft, array_flip(self::PRIVATE_DRAFT_FIELDS)))];
+167
View File
@@ -0,0 +1,167 @@
<?php
namespace App\Services;
use App\Models\Opportunity;
use App\Models\User;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
/** Tempo's promotional disclosure, independent of investment execution approval. */
final class OpportunityPromotion
{
public const PURPOSE = 'promotional_preparation';
public const NOTICE = 'Em preparação. A divulgação não confirma financiamento, disponibilidade de terreno, licenças ou autorização de execução do investimento.';
public function allowed(User $actor, Opportunity $record): bool
{
$access = app(PortalProfileAccess::class);
if ($record->trashed() || $record->owner_user_id === null || ! $access->canView($actor, $record)
|| $access->has($actor, 'auditor') || in_array($record->status, ['archived', 'rejected'], true)) return false;
// Never overwrite a normal approved publication or its workflow decisions.
if ($record->published_at && data_get($record->workflow_context, 'publication.purpose') !== self::PURPOSE) return false;
$internal = $access->internalAssignment($actor, $record->opportunity_category);
return ($internal && $access->promoterRecordScope($actor, $record) && $access->operational($actor, 'edit', $record->id))
|| ($access->scoped($actor, 'publisher', $record->organization_id, $record->opportunity_category)
&& $access->operational($actor, 'publish', $record->id));
}
private function authorize(User $actor, Opportunity $record): void
{
abort_unless($this->allowed($actor, $record), 403, 'A designação e o âmbito existentes não autorizam esta divulgação.');
if (! app(PortalProfileAccess::class)->mfa($actor)) {
throw new \Illuminate\Http\Exceptions\HttpResponseException(response()->json(['message' => 'Confirme a entrada normal com MFA recente para divulgar.'], 403)->header('X-Portal-Reauthentication', 'required'));
}
}
public function editor(User $actor, Opportunity $record): array
{
abort_unless($this->allowed($actor, $record), 403);
$saved = data_get($record->workflow_context, 'promotional_summary', []);
// Only previously public text is recovered automatically. No private contact/document is copied.
$existing = data_get($record->workflow_context, 'publication.purpose') === self::PURPOSE;
$form = $saved['input'] ?? ['title' => $existing ? $record->title : '', 'summary' => $existing ? $record->summary : '',
'description' => $existing ? $record->description : '', 'source_reference' => '', 'edition' => '', 'source_date' => '',
'limitations' => '', 'territory_ids' => $existing ? DB::table('opportunity_locations')->where('opportunity_id', $record->id)->pluck('geo_location_id')->all() : [],
'sector_id' => $record->sector_id];
return ['record' => ['id' => $record->id, 'reference' => $record->reference], 'etag' => app(OpportunityManagement::class)->etag($record),
'initial' => $form, 'publicNow' => $existing && $record->status === 'published',
'reauthenticationRequired' => ! app(PortalProfileAccess::class)->mfa($actor),
'sectors' => \App\Models\Sector::catalog()->orderBy('name')->get(['id', 'name']),
'territories' => app(InvestmentGeography::class)->publicReferences()->orderBy('name')->get(['id', 'name', 'location_type']),
'history' => DB::table('audit_events')->where('source_system', 'tempo-preparation-publication')
->where('after_data->record_id', $record->id)->orderByDesc('occurred_at')->get(['id', 'action', 'actor_user_id', 'occurred_at'])];
}
public function preview(User $actor, Opportunity $record, array $input): array
{
$this->authorize($actor, $record);
$data = Validator::make($input, ['etag' => 'required|string|size:64', 'action' => 'required|in:disclose,withdraw',
'title' => 'required_if:action,disclose|nullable|string|max:255', 'summary' => 'required_if:action,disclose|nullable|string|max:499',
'description' => 'nullable|string|max:6000', 'limitations' => 'nullable|string|max:2500',
'source_reference' => 'nullable|string|max:2000', 'edition' => 'nullable|string|max:150', 'source_date' => 'nullable|date_format:Y-m-d',
'sector_id' => ['nullable', 'integer', Rule::in(\App\Models\Sector::catalog()->pluck('id')->all())],
'territory_ids' => 'sometimes|array|max:30', 'territory_ids.*' => 'required|uuid|distinct',
])->validate();
abort_unless(hash_equals(app(OpportunityManagement::class)->etag($record), $data['etag']), 409, 'A ficha mudou. Reabra antes de preparar outra prévia.');
if ($data['action'] === 'withdraw') abort_unless(data_get($record->workflow_context, 'publication.purpose') === self::PURPOSE, 409);
$paths = [];
foreach ($data['territory_ids'] ?? [] as $id) {
$path = app(InvestmentGeography::class)->path($id, true);
abort_unless(count($path) > 0 && collect($path)->contains('id', $id), 422, 'Seleccione apenas territórios públicos existentes.');
$paths[] = $path;
}
$text = fn ($key) => trim(strip_tags($data[$key] ?? ''));
$body = implode("\n\n", array_filter([$text('description'), self::NOTICE,
'Limitações e lacunas: '.($text('limitations') ?: 'Informação ainda por completar.'),
'Fonte: '.($text('source_reference') ?: 'Não indicada.'), 'Edição: '.($text('edition') ?: 'Não indicada.'),
'Data-base: '.($data['source_date'] ?? 'Não indicada.')]));
$projection = ['title' => $text('title'), 'summary' => $text('summary'), 'description' => $body,
'sector_id' => $data['sector_id'] ?? null, 'maturity_label' => 'Em preparação', 'notice' => self::NOTICE,
'territories' => array_map(fn ($p) => ['id' => end($p)->id, 'label' => implode(' / ', array_column($p, 'name'))], $paths)];
if ($data['action'] === 'disclose') abort_unless($projection['title'] !== '' && $projection['summary'] !== '', 422, 'Indique título e resumo em texto simples.');
if ($data['action'] === 'withdraw') $projection = [...$projection, ...$record->only(['title', 'summary', 'description', 'sector_id', 'maturity_label'])];
$payload = ['actor_id' => $actor->id, 'record_id' => $record->id, 'etag' => $data['etag'], 'action' => $data['action'],
'input' => $data, 'projection' => $projection, 'expires' => now()->addMinutes(15)->timestamp];
return ['action' => $data['action'], 'projection' => $projection, 'token' => Crypt::encryptString(json_encode($payload, JSON_THROW_ON_ERROR)),
'current' => $record->only(['title', 'summary', 'description', 'status']), 'writes' => false];
}
public function confirm(User $actor, int $id, array $input): array
{
$data = Validator::make($input, ['token' => 'required|string|max:300000', 'operation_key' => 'required|uuid',
'confirm' => 'required|accepted', 'reason' => 'required|string|min:10|max:1000'])->validate();
try { $preview = json_decode(Crypt::decryptString($data['token']), true, flags: JSON_THROW_ON_ERROR); }
catch (\Throwable) { abort(422, 'Prévia inválida. Prepare outra prévia.'); }
abort_unless(($preview['actor_id'] ?? null) === $actor->id && ($preview['record_id'] ?? null) === $id, 403);
return DB::transaction(function () use ($actor, $id, $data, $preview) {
DB::select('SELECT pg_advisory_xact_lock(hashtext(?))', ['promotional-operation:'.$data['operation_key']]);
$record = Opportunity::whereKey($id)->lockForUpdate()->firstOrFail();
$this->authorize($actor, $record);
$digest = hash('sha256', $data['token'].'|'.$data['reason']);
$replay = DB::table('audit_events')->where('source_system', 'tempo-preparation-publication')
->where('after_data->operation_key', $data['operation_key'])->first();
if ($replay) {
$prior = json_decode($replay->after_data, true);
abort_unless($prior['record_id'] === $id && (int) $replay->actor_user_id === $actor->id && hash_equals($prior['request_hash'], $digest), 409, 'Chave usada noutra operação.');
return ['id' => $id, 'replayed' => true, 'message' => 'Operação já registada; nenhuma alteração repetida.'];
}
abort_unless(($preview['expires'] ?? 0) >= now()->timestamp, 409, 'A prévia expirou. Prepare outra prévia.');
abort_unless(hash_equals(app(OpportunityManagement::class)->etag($record), $preview['etag']), 409, 'Alteração concorrente: a ficha foi preservada. Reabra e reveja a nova versão.');
$before = $record->getRawOriginal();
$oldLocations = DB::table('opportunity_locations')->where('opportunity_id', $id)->pluck('geo_location_id')->all();
$context = $record->workflow_context ?? [];
if ($preview['action'] === 'withdraw') {
$record->status = 'suspended';
$record->published_at = null;
$context['publication']['visibility'] = 'withdrawn';
$action = 'promotional_summary_withdrawn';
} else {
$p = $preview['projection'];
if ($p['sector_id'] !== null) abort_unless(\App\Models\Sector::catalog()->whereKey($p['sector_id'])->exists(), 409, 'O sector deixou de estar disponível no catálogo. Prepare outra prévia.');
$paths = [];
foreach ($preview['input']['territory_ids'] ?? [] as $territory) {
$path = app(InvestmentGeography::class)->path($territory, true);
abort_unless(count($path) && collect($path)->contains('id', $territory), 409, 'Um território deixou de estar disponível. Prepare outra prévia.');
$paths[] = $path;
}
$currentTerritories = array_map(fn ($path) => ['id' => end($path)->id, 'label' => implode(' / ', array_column($path, 'name'))], $paths);
abort_unless($currentTerritories === $p['territories'], 409, 'O enquadramento territorial mudou. Prepare outra prévia.');
// Preserve the original private draft bytes; retain its former base
// columns in the existing hidden context before projecting public text.
$context['promotional_private_base'] ??= $record->only(OpportunityManagement::FIELDS);
$context['working_state'] ??= app(PortalProfileAccess::class)->state($record);
$context['promotional_summary'] = ['input' => $preview['input'], 'projection' => $p];
$context['publication'] = ['visibility' => 'public', 'purpose' => self::PURPOSE, 'actor_id' => $actor->id,
'at' => now()->toIso8601String(), 'execution_approved' => false, 'private_draft_preserved' => true];
$action = data_get($before, 'status') === 'published' ? 'promotional_summary_updated' : 'promotional_summary_published';
foreach (['title', 'summary', 'description', 'sector_id', 'maturity_label'] as $field) $record->{$field} = $p[$field];
foreach (['promoter_name', 'promoter_type', 'financing_modality', 'expected_impact', 'maturity_level', 'estimated_investment_amount', 'estimated_investment_currency'] as $field) $record->{$field} = null;
$main = $paths[0] ?? [];
$record->primary_territory_id = $main ? end($main)->id : null;
$record->province = collect($main)->firstWhere('location_type', 'province')?->name;
$record->district = collect($main)->firstWhere('location_type', 'district')?->name;
$record->geography_scope = count($paths) > 1 ? 'multiple' : ($main ? 'specific' : 'unspecified');
DB::table('opportunity_locations')->where('opportunity_id', $id)->delete();
foreach ($preview['input']['territory_ids'] ?? [] as $territory) DB::table('opportunity_locations')->insert(['opportunity_id' => $id, 'geo_location_id' => $territory]);
$record->status = 'published';
$record->is_featured = false;
$record->last_validated_at = null;
$record->published_at = now();
}
$record->workflow_context = $context;
$record->management_version++;
$record->save();
DB::table('audit_events')->insert(['source_system' => 'tempo-preparation-publication', 'entity_type' => 'opportunity',
'actor_user_id' => $actor->id, 'action' => $action, 'data_classification' => 'restricted', 'correlation_id' => (string) Str::uuid(),
'before_data' => json_encode(['record_id' => $id, 'record' => $before, 'territories' => $oldLocations], JSON_THROW_ON_ERROR),
'after_data' => json_encode(['record_id' => $id, 'operation_key' => $data['operation_key'], 'request_hash' => $digest,
'reason' => $data['reason'], 'projection' => $preview['projection'], 'execution_approved' => false,
'record' => $record->getRawOriginal()], JSON_THROW_ON_ERROR)]);
return ['id' => $id, 'replayed' => false, 'message' => $preview['action'] === 'withdraw' ? 'Resumo retirado; cadastro e histórico preservados.' : 'Resumo divulgado como Em preparação. Não autoriza execução do investimento.'];
});
}
}
+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;
}
}
@@ -0,0 +1,189 @@
<?php
namespace App\Services;
use Brick\Math\BigDecimal;
/** Adapter of the dossier's declared context to preparation of a private-investment request. No tax or entitlement decision. */
final class PrivateInvestmentPreparation
{
public const VERSION = 'private-investment-preparation-2026-v1';
public const SOURCE = 'https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf';
public const HASH = 'ba1e9eaabfec5d0c4cb2d9f2444a6860f7a90f222034764197829be402d408ed';
public const FIELDS = ['investment_form', 'investment_preparation', 'investment_activity_scope', 'implementation_mode', 'capital_origin', 'site_tenure', 'project_concession', 'significant_implications', 'land_extension', 'forest_concession', 'forest_concession_hectares', 'total_investment', 'area_required', 'area_unit'];
public const DOCUMENTS = ['identity', 'registration', 'name_reservation', 'site_plan', 'lease', 'duat', 'representation_license', 'feasibility', 'financial_capacity', 'foreign_capital'];
public function available(): bool
{
return (bool) config('investor-path.private_preparation', false);
}
public function questions(): array
{
$tri = ['unknown' => 'Por determinar', 'yes' => 'Sim', 'no' => 'Não'];
return [
['key' => 'investment_preparation', 'label' => 'Pretende preparar o pedido de garantias/incentivos ao investimento privado?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 2'],
['key' => 'investment_activity_scope', 'label' => 'Natureza efectiva da actividade do projecto', 'options' => ['unknown' => 'Por determinar', 'general' => 'Outra actividade económica', 'extractive' => 'Prospecção, pesquisa ou produção extractiva', 'processing' => 'Processamento industrial mineiro/petrolífero', 'public_nonprofit' => 'Investimento público ou exclusivamente social/sem fins lucrativos'], 'source' => 'Lei 8/2023, artigos 2 e 22'],
['key' => 'investment_form', 'label' => 'Tipo deste investimento', 'options' => ['unknown' => 'Por determinar', 'direct' => 'Directo', 'indirect' => 'Indirecto'], 'source' => 'Lei 8/2023, artigo 14(b); Regulamento, artigo 13', 'help' => 'O limiar monetário do artigo 13 aplica-se ao investimento directo. A forma indirecta não é convertida silenciosamente em directa.'],
['key' => 'implementation_mode', 'label' => 'Modalidade da empresa implementadora', 'options' => ['unknown' => 'Por determinar', 'domestic_entity' => 'Empresa constituída/em constituição em Moçambique', 'foreign_representation' => 'Representação comercial estrangeira'], 'source' => 'Regulamento 8/2024, artigo 9(2)'],
['key' => 'capital_origin', 'label' => 'Origem dos capitais/recursos deste investimento', 'options' => ['unknown' => 'Por determinar', 'national' => 'Nacionais', 'external' => 'Provenientes do exterior', 'mixed' => 'Mistos'], 'source' => 'Lei 8/2023, glossário; Regulamento, artigos 4–6'],
['key' => 'site_tenure', 'label' => 'Fundamento declarado da utilização das instalações', 'options' => ['unknown' => 'Por determinar', 'lease' => 'Arrendamento', 'duat' => 'DUAT'], 'source' => 'Regulamento, artigos 9(1)(d) e 14(1)(d)'],
['key' => 'project_concession', 'label' => 'O empreendimento é PPP ou concessão empresarial?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(ii)'],
['key' => 'significant_implications', 'label' => 'Existem previsíveis implicações económicas, ambientais, de segurança ou saúde pública que exijam autorização?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(i)', 'help' => 'Declare o enquadramento conhecido; o nome do sector não responde a esta pergunta. Sem fundamento suficiente, mantenha por determinar.'],
['key' => 'land_extension', 'label' => 'O projecto requer extensão de terra?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(iii)'],
['key' => 'forest_concession', 'label' => 'O projecto requer concessão florestal?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(iv)'],
['key' => 'forest_concession_hectares', 'label' => 'Área da concessão florestal (hectares)', 'type' => 'number', 'when' => ['field' => 'forest_concession', 'equals' => 'yes'], 'source' => 'Lei 8/2023, artigo 22(1)(b)(iv)'],
['key' => 'total_investment', 'label' => 'Valor total do investimento na moeda do projecto', 'type' => 'number', 'source' => 'Regulamento, artigo 13(2)'],
['key' => 'area_required', 'label' => 'Área de terra requerida', 'type' => 'number', 'when' => ['field' => 'land_extension', 'equals' => 'yes'], 'source' => 'Lei 8/2023, artigo 22(1)(b)(iii)'],
['key' => 'area_unit', 'label' => 'Unidade da área requerida', 'options' => ['ha' => 'Hectares', 'm2' => 'Metros quadrados'], 'when' => ['field' => 'land_extension', 'equals' => 'yes'], 'source' => 'Conversão de unidade exacta; não é benefício ou condição adicional'],
];
}
private function number(array $f, string $key): ?BigDecimal
{
$value = $f[$key] ?? '';
return is_scalar($value) && preg_match('/^\d+(?:\.\d+)?$/D', (string) $value) ? BigDecimal::of((string) $value) : null;
}
public function assess(array $context, array $documents = []): array
{
$f = $context['fields'] ?? [];
$missing = [];
$answer = function (string $key) use ($f, &$missing): ?bool {
if (! in_array($f[$key] ?? '', ['yes', 'no'], true)) {
$missing[] = $key;
return null;
}
return $f[$key] === 'yes';
};
$enabled = ($f['investment_preparation'] ?? '') === 'yes';
$date = $f['expected_start_date'] ?? '';
$period = preg_match('/^2026-\d{2}-\d{2}$/D', $date) === 1;
$scope = $f['investment_activity_scope'] ?? 'unknown';
$covered = in_array($scope, ['general', 'processing'], true);
$mode = 'indeterminate';
$reasons = [];
if ($enabled) {
if (! $date) {
$missing[] = 'expected_start_date';
}
if ($scope === 'unknown' || $scope === '') {
$missing[] = 'investment_activity_scope';
}
if (empty($f['currency'])) {
$missing[] = 'currency';
}
if ($covered && $period) {
$triggers = [];
if ($scope === 'processing') {
$triggers['processing'] = true;
$reasons[] = 'Processamento industrial mineiro/petrolífero: Lei 8/2023, artigo 22(1)(b)(v).';
}
$amount = $this->number($f, 'total_investment');
$triggers['amount'] = $amount && ($f['currency'] ?? '') === 'MZN' && ($f['investment_form'] ?? '') === 'direct' ? $amount->isGreaterThan('32000000000') : null;
if (! in_array($f['investment_form'] ?? '', ['direct', 'indirect'], true)) {
$missing[] = 'investment_form';
}
if (($f['investment_form'] ?? '') === 'indirect') {
$reasons[] = 'Investimento indirecto: a classificação pela dimensão e o encaminhamento monetário não são determinados por este lote. As restantes condições documentadas continuam avaliadas.';
}
if (! $amount) {
$missing[] = 'total_investment';
}
if (($f['currency'] ?? '') !== 'MZN') {
$reasons[] = 'A condição monetária não é determinada noutra moeda: não se fez conversão.';
}
foreach (['project_concession', 'significant_implications'] as $key) {
$triggers[$key] = $answer($key);
}
$land = $answer('land_extension');
if ($land === true) {
$area = $this->number($f, 'area_required');
$unit = $f['area_unit'] ?? '';
if (! $area) {
$missing[] = 'area_required';
}if (! in_array($unit, ['ha', 'm2'], true)) {
$missing[] = 'area_unit';
}
$triggers['land_extension'] = $area && in_array($unit, ['ha', 'm2'], true) ? $area->isGreaterThanOrEqualTo($unit === 'ha' ? '10000' : '100000000') : null;
} else {
$triggers['land_extension'] = $land;
}
$forest = $answer('forest_concession');
if ($forest === true) {
$area = $this->number($f, 'forest_concession_hectares');
if (! $area) {
$missing[] = 'forest_concession_hectares';
}$triggers['forest_concession'] = $area ? $area->isGreaterThan('100000') : null;
} else {
$triggers['forest_concession'] = $forest;
}
foreach (['amount' => 'Valor superior a 32 mil milhões MZN — R13(2), R21(2)(a)', 'project_concession' => 'PPP/concessão empresarial — Lei 8/2023, art. 22(1)(b)(ii), R21(2)(b)', 'significant_implications' => 'Implicações significativas declaradas — Lei 8/2023, art. 22(1)(b)(i), R21(1)', 'land_extension' => 'Extensão de terra igual/superior a 10 mil hectares — Lei 8/2023, art. 22(1)(b)(iii), R21(2)(c)', 'forest_concession' => 'Concessão florestal superior a 100 mil hectares — Lei 8/2023, art. 22(1)(b)(iv), R21(2)(d)'] as $condition => $description) {
if (($triggers[$condition] ?? null) === true) {
$reasons[] = $description.'.';
}
}
$mode = in_array(true, $triggers, true) ? 'authorization' : (in_array(null, $triggers, true) ? 'indeterminate' : 'registration');
$reasons[] = $mode === 'authorization' ? 'Foi identificada uma condição de autorização; as restantes condições desconhecidas não foram presumidas negativas.' : ($mode === 'registration' ? 'As condições de autorização deste lote foram declaradas negativas e o montante não ultrapassa o limite.' : 'Falta resolver pelo menos uma condição de autorização.');
}
foreach (['implementation_mode', 'capital_origin', 'site_tenure', 'entity_stage'] as $key) {
if (empty($f[$key]) || $f[$key] === 'unknown') {
$missing[] = $key;
}
}
}
if ($mode === 'authorization' && in_array(true, array_intersect_key($triggers, array_flip(['amount', 'project_concession', 'land_extension', 'forest_concession'])), true)) {
$missing = array_values(array_diff($missing, ['investment_form', 'project_concession', 'significant_implications', 'land_extension', 'forest_concession', 'forest_concession_hectares', 'area_required', 'area_unit', 'total_investment']));
}
$base = $enabled && $covered && $period ? true : null;
if (! $enabled || in_array($scope, ['extractive', 'public_nonprofit'], true) || ($date && ! $period)) {
$base = false;
}
$rules = [
['identity', 'Identificação do investidor', $base, 'S06', 'R9(1)(a) / R14(1)(a)', 41, 'Identificação do proponente do pedido.'],
['registration', 'Certidão de registo comercial', $base === true ? (($f['entity_stage'] ?? '') === 'existing' ? true : (($f['entity_stage'] ?? '') === 'being_formed' ? false : null)) : $base, 'S05', 'R9(1)(b) / R14(1)(b)', 41, 'Alternativa para empresa já registada.'],
['name_reservation', 'Reserva da denominação social', $base === true ? (($f['entity_stage'] ?? '') === 'being_formed' ? true : (($f['entity_stage'] ?? '') === 'existing' ? false : null)) : $base, 'S05', 'R9(1)(b) / R14(1)(b)', 41, 'Alternativa para empresa em constituição.'],
['site_plan', 'Planta ou esboço do local', $base, 'S06', 'R9(1)(c) / R14(1)(c)', 42, 'Local de implantação identificado; não certifica disponibilidade.'],
];
foreach (['lease' => 'Contrato de arrendamento', 'duat' => 'Título DUAT'] as $key => $label) {
$rules[] = [$key, $label, $base === true ? (($f['site_tenure'] ?? 'unknown') === 'unknown' ? null : ($f['site_tenure'] ?? '') === $key) : $base, 'S06', 'R9(1)(d) / R14(1)(d)', 42, 'Alternativa correspondente à utilização declarada das instalações.'];
}
$rules[] = ['representation_license', 'Licença de representação comercial', $base === true ? (($f['implementation_mode'] ?? 'unknown') === 'unknown' ? null : ($f['implementation_mode'] ?? '') === 'foreign_representation') : $base, 'S06', 'R9(2)', 42, 'Só para a modalidade representação comercial estrangeira; não decorre da nacionalidade.'];
foreach (['feasibility' => 'Estudo de viabilidade técnica e económico-financeira', 'financial_capacity' => 'Comprovação da capacidade financeira'] as $key => $label) {
$rules[] = [$key, $label, $base === true ? ($mode === 'indeterminate' ? null : $mode === 'authorization') : $base, 'S02', 'R14(1)–(2)', 45, 'Integra a preparação do pedido de autorização.'];
}
$rules[] = ['foreign_capital', 'Prova de realização/registo do aporte externo', $base === true ? (($f['capital_origin'] ?? 'unknown') === 'unknown' ? null : in_array($f['capital_origin'] ?? '', ['external', 'mixed'], true)) : $base, 'S06', 'R5(1)–(5)', 39, 'Preparação específica do aporte externo; formalidades cambiais concretas não calculadas.'];
$links = $context['preparation_documents'] ?? [];
$requirements = [];
foreach ($rules as [$key,$label,$applies,$stage,$article,$page,$reason]) {
$doc = isset($links[$key]) ? collect($documents)->firstWhere('document_id', $links[$key]) : null;
$requirements[] = ['key' => $key, 'label' => $label, 'state' => $applies === false ? 'not_applicable' : ($applies === null ? 'indeterminate' : ($doc ? 'associated' : 'missing')), 'required' => $applies === true, 'document_id' => $links[$key] ?? null, 'document' => $doc, 'stage' => $stage, 'reason' => $reason, 'source' => ['reference' => $article, 'url' => self::SOURCE.'#page='.$page, 'sha256' => self::HASH, 'period' => '2026; Regulamento desde 07/03/2024'], 'legal_verification' => 'not_inferred'];
}
$authoritySource = ['reference' => 'R13(1)', 'url' => self::SOURCE.'#page=45'];
$zone = $context['selection']['park'] ?? null;
$amount = $this->number($f, 'total_investment');
$authority = 'Por determinar neste lote';
if ($base === true && $mode === 'registration' && $amount && ($f['currency'] ?? '') === 'MZN') {
$special = in_array($zone['fields']['investment_regime'] ?? '', ['zee', 'zfi'], true);
$authority = $special ? ($amount->isLessThanOrEqualTo('6500000000') ? 'APIEX — condicionado ao enquadramento ZEE/ZFI efectivamente reconhecido' : 'Ministro que superintende Finanças') : ($amount->isLessThanOrEqualTo('3500000000') ? 'Governador da Província' : ($amount->isLessThanOrEqualTo('6500000000') ? 'APIEX' : 'Ministro que superintende Finanças'));
}
if ($base === true && $mode === 'authorization') {
$council = array_intersect_key($triggers, array_flip(['amount', 'project_concession', 'land_extension', 'forest_concession']));
$authority = in_array(true, $council, true) ? 'Conselho de Ministros' : (in_array(null, $council, true) ? 'Autorização identificada; completar as condições em falta para distinguir a entidade decisória' : 'Ministro que superintende Finanças; eventual submissão ao Conselho de Ministros nos termos do R21(3)');
$authoritySource = ['reference' => 'R21(1)–(3)', 'url' => self::SOURCE.'#page=50'];
}
$labels = array_column($this->questions(), 'label', 'key') + ['currency' => 'Moeda do projecto', 'expected_start_date' => 'Data prevista de início', 'entity_stage' => 'Situação da entidade'];
$missingLabels = array_map(fn ($key) => $labels[$key] ?? $key, array_values(array_unique($missing)));
return ['version' => self::VERSION, 'enabled' => $enabled, 'period_covered' => $period, 'scope_covered' => $covered, 'mode' => $mode, 'reason' => $reasons, 'missing_fields' => array_values(array_unique($missing)), 'missing_labels' => $missingLabels, 'questions' => $this->questions(), 'requirements' => $requirements, 'authority' => $authority, 'authority_source' => $authoritySource, 'next' => $base === false ? 'Rever o âmbito/período; este lote não cobre o pedido' : ($mode === 'indeterminate' ? 'Completar condições do enquadramento' : ($mode === 'authorization' ? 'Preparar estudo e documentos do pedido de autorização' : 'Preparar documentos do pedido de mero registo')), 'calculation_notice' => 'O regime procedimental não altera a taxa de IVA. Reutilize a simulação geral apenas no seu âmbito e período; benefícios, apuramento final e conversão cambial não são presumidos.', 'source' => ['url' => self::SOURCE, 'sha256' => self::HASH], 'notice' => 'Preparação informativa baseada em declarações; documentos associados não constituem validação jurídica, certificado ou submissão à Administração.'];
}
}
+39 -6
View File
@@ -4,6 +4,7 @@ namespace App\Services;
use App\Jobs\RunSimulation;
use App\Models\Simulation;
use App\Support\IsolatedQaDatabase;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
@@ -17,15 +18,39 @@ final class SimulationExecution
*/
public function create(array $data, ?int $actorId, ?string $organizationId, ?string $token, string $key): array
{
$tokenHash = $actorId === null ? hash_hmac('sha256', (string) $token, (string) config('app.key')) : null;
$scope = $actorId !== null ? (string) $actorId : ($tokenHash ?? 'anonymous');
$scopeHash = hash('sha256', $scope);
$keyHash = hash('sha256', strtolower($key));
$payload = ['base' => (string) $data['base'], 'rate' => (string) $data['rate'], 'reduction' => (string) $data['reduction'], 'scenario_kind' => 'custom_planning', 'organization_id' => $organizationId];
return $this->createPayload($payload, $actorId, $organizationId, $token, $key);
}
public function createFiscalQaPreview(array $snapshot, int $actorId, string $key): array
{
abort_unless(IsolatedQaDatabase::allowsSchemaChanges(), 404);
abort_unless(($snapshot['scenario_kind'] ?? null) === 'fiscal_rule_qa_preview'
&& in_array($snapshot['result']['status'] ?? null, ['calculated', 'indeterminate'], true)
&& ($snapshot['presentation']['rounding_applied'] ?? null) === false, 422);
return $this->createPayload(['scenario_kind' => 'fiscal_rule_qa_preview', 'fiscal_preview' => $snapshot, 'organization_id' => null],
$actorId, null, null, $key);
}
public function createPublishedFiscal(array $input, ?int $actorId, ?string $organizationId, ?string $token, string $key): array
{
$payload = ['scenario_kind' => 'fiscal_rule_published', 'fiscal_input' => $input, 'organization_id' => $organizationId];
return $this->createPayload($payload, $actorId, $organizationId, $token, $key,
fn (): array => app(SimulationPublishedFiscal::class)->admit($input));
}
private function createPayload(array $payload, ?int $actorId, ?string $organizationId, ?string $token, string $key, ?callable $admit = null): array
{
$tokenHash = $actorId === null ? hash_hmac('sha256', (string) $token, (string) config('app.key')) : null;
$scopeHash = hash('sha256', $actorId !== null ? (string) $actorId : ($tokenHash ?? 'anonymous'));
$keyHash = hash('sha256', strtolower($key));
ksort($payload);
$payloadHash = hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR));
return DB::transaction(function () use ($actorId, $organizationId, $token, $tokenHash, $scopeHash, $keyHash, $payloadHash, $payload): array {
return DB::transaction(function () use ($actorId, $organizationId, $token, $tokenHash, $scopeHash, $keyHash, $payloadHash, $payload, $admit): array {
$this->setContext($actorId, $tokenHash);
DB::select('select pg_advisory_xact_lock(261001, hashtext(?))', [$scopeHash.$keyHash]);
$receipt = DB::table('simulation_idempotency')->where('scope_hash', $scopeHash)->where('key_hash', $keyHash)->first();
@@ -37,6 +62,10 @@ final class SimulationExecution
return ['simulation' => Simulation::with(['scenarios', 'reports'])->whereKey((string) $receipt->simulation_id)->firstOrFail(), 'token' => null, 'replayed' => true];
}
if ($admit !== null) {
$payload['fiscal_preview'] = $admit();
}
$plainToken = $actorId === null ? $token : null;
$tokenHash = $actorId === null ? hash_hmac('sha256', $plainToken, (string) config('app.key')) : null;
$this->setContext($actorId, $tokenHash);
@@ -44,7 +73,9 @@ final class SimulationExecution
$simulation = Simulation::create([
'id' => $id, 'questionnaire_version_id' => '61000100-0000-4000-8000-000000000001', 'owner_user_id' => $actorId, 'organization_id' => $organizationId,
'anonymous_token_hash' => $tokenHash, 'status' => 'queued', 'version_no' => 1,
'input_snapshot' => $payload, 'calculation_snapshot' => ['engine' => 'portal-arithmetic-v1', 'formula' => 'base * rate / 100 * (1 - reduction / 100)', 'legal_rate_source' => null],
'input_snapshot' => $payload, 'calculation_snapshot' => in_array($payload['scenario_kind'], ['fiscal_rule_qa_preview', 'fiscal_rule_published'], true)
? ['engine' => $payload['fiscal_preview']['context']['engine_version'], 'mode' => $payload['fiscal_preview']['fiscal_provenance']['mode'], 'arithmetic' => 'exact_decimal_no_rounding']
: ['engine' => 'portal-arithmetic-v1', 'formula' => 'base * rate / 100 * (1 - reduction / 100)', 'legal_rate_source' => null],
]);
$scenarioId = (string) Str::uuid();
DB::table('simulation_scenarios')->insert([
@@ -85,6 +116,8 @@ final class SimulationExecution
return DB::transaction(function () use ($id, $scenarioId, $data, $request): Simulation {
$this->setContext($request->user()?->id, $this->tokenHash($request));
$simulation = Simulation::whereNull('deleted_at')->lockForUpdate()->findOrFail($id);
abort_if(in_array($simulation->input_snapshot['scenario_kind'] ?? null, ['fiscal_rule_qa_preview', 'fiscal_rule_published'], true), 422,
'Crie uma nova pré-visualização fiscal; este snapshot conserva a regra e as entradas originais.');
$this->requireMatch($request, $simulation);
$scenario = DB::table('simulation_scenarios')->where('simulation_id', $id)->where('id', $scenarioId)->lockForUpdate()->first();
abort_if($scenario === null, 404);
@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\InvestmentLegalSource;
use App\Models\User;
use App\Support\IsolatedQaDatabase;
use Illuminate\Support\Facades\DB;
/** Exact operation-level IVA calculation against an explicit QA draft; never a payment/return total. */
final class SimulationFiscalQaPreview
{
public function create(User $actor, string $ruleId, array $input, string $key): array
{
abort_unless(IsolatedQaDatabase::allowsSchemaChanges(), 404);
return DB::transaction(function () use ($actor, $ruleId, $input, $key): array {
$rule = app(SimulationTaxRuleGovernance::class)->show($actor, $ruleId);
abort_unless($rule->status === 'draft' && $rule->rule_code === 'IVA-GERAL-BASE-LIQUIDA', 422,
'Seleccione o rascunho do primeiro lote fiscal.');
abort_if(collect($rule->rule_definition['nodes'] ?? [])->contains(fn (array $node): bool => ($node['op'] ?? null) === 'round'), 422,
'Este percurso preserva o valor exacto e não aplica arredondamento automático.');
$source = InvestmentLegalSource::findOrFail($rule->legal_source_id);
$version = $rule->attributesToArray();
$version['source'] = $source->attributesToArray();
$date = $input['reference_date'];
$context = ['operation' => $input['operation'], 'currency' => $input['currency']];
$missing = [];
$resolution = [];
$evaluation = null;
if ($date < '2026-01-01' || $date > '2026-12-31') {
$missing[] = 'demonstrated_coverage';
} else {
$resolution = app(SimulationTaxParameterResolver::class)->resolveDraftForQa([$version], 'IVA', $date, $date, $context);
$rate = collect($resolution)->firstWhere('parameter_code', 'IVA_GENERAL_RATE');
if (($rate['status'] ?? null) !== 'resolved' || ($rate['unit'] ?? null) !== 'percent') {
$missing[] = 'eligible_parameter';
} else {
$evaluation = app(SimulationTaxRuleExpression::class)->evaluate($rule->rule_definition,
['base_tributavel' => $input['base']], ['iva_general_rate' => $rate['value']]);
}
}
$snapshot = app(SimulationFiscalSnapshot::class)->build($version, $input, $resolution, $evaluation, $missing);
return app(SimulationExecution::class)->createFiscalQaPreview($snapshot, $actor->id, $key);
});
}
}
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Services;
use Brick\Math\BigRational;
/** Shared snapshot representation; eligibility is resolved by each admission adapter. */
final class SimulationFiscalSnapshot
{
public function build(array $version, array $input, array $resolution, ?array $evaluation, array $missing, bool $published = false): array
{
$date = $input['reference_date'];
$context = ['operation' => $input['operation'], 'currency' => $input['currency']];
$exact = ($evaluation['status'] ?? null) === 'determinate'
? (string) BigRational::of($evaluation['result'])->toBigDecimal()
: null;
if ($exact === null && $missing === []) {
$missing = $evaluation['missing'] ?? ['eligible_parameter'];
}
$snapshot = [
'contract_version' => 1, 'scenario_kind' => $published ? 'fiscal_rule_published' : 'fiscal_rule_qa_preview',
'base' => $input['base'], 'currency' => $input['currency'], 'indicative_total' => $exact,
'legal_interpretation' => false, 'formula_version' => 'investmoz.tax.v1',
'context' => [
'reference_date' => $date, 'currency' => $input['currency'], 'currency_fraction_digits' => 2,
'scenario_id' => null, 'scenario_version' => 1, 'engine_version' => $published ? 'fiscal-published-v1' : 'fiscal-draft-preview-v1',
'ruleset_version' => $version['rule_code'].':'.$version['rule_version'],
],
'result' => [
'status' => $exact === null ? 'indeterminate' : 'calculated', 'value' => $exact, 'unit' => 'MZN',
'explanation' => $exact === null
? 'IVA indeterminado: o contexto, a fonte ou o período não permitem resolver o parâmetro deste lote QA.'
: 'IVA calculado com precisão decimal exacta, sem arredondamento. Ensaio QA de regra em rascunho; enquadramento declarado, não validado. Não é saldo de declaração nem valor a pagar.',
'missing_inputs' => $missing, 'source_ids' => [(string) $version['legal_source_id']],
],
'presentation' => ['value' => $exact, 'currency' => $input['currency'], 'rounding_applied' => false, 'purpose' => 'operation_tax_not_payment'],
// Exact rational steps supplement the decimal result; no float conversion.
'intermediate' => $evaluation,
'fiscal_provenance' => [
'mode' => $published ? 'published_version' : 'qa_draft_only', 'scope_status' => ($input['scope_declared'] ?? true) ? 'user_declared' : 'undetermined', 'context' => $context,
'rounding_policy' => $version['rounding_policy'], 'resolution' => $resolution,
'rule' => $version, 'source_acceptance' => 'Fonte oficial aceite pela decisão Tempo de 08/10/2026; não constitui aprovação jurídica desta operação ou publicação da regra.',
],
];
if ($published) {
$snapshot['result']['explanation'] = $exact === null
? 'IVA indeterminado: falta contexto, fonte ou parâmetro elegível para esta operação.'
: 'IVA calculado pela versão publicada identificada, sem arredondamento. Enquadramento declarado, não validado. Não é saldo de declaração nem valor a pagar.';
$snapshot['fiscal_provenance']['admitted_at'] = now()->toISOString();
$snapshot['fiscal_provenance']['source_acceptance'] = 'Fonte oficial aceite pela Tempo em 08/10/2026; não constitui validação jurídica desta operação.';
}
return $snapshot;
}
}
@@ -0,0 +1,20 @@
<?php
namespace App\Services;
final class SimulationFiscalSourceAcceptance
{
public static function eligible(array $source, ?string $hash = null): bool
{
if (($source['validation_status'] ?? null) === 'validated') {
return true;
}
$basis = $source['fiscal_acceptance'] ?? null;
return ($source['validation_status'] ?? null) === 'pending' && is_array($basis)
&& ($basis['scope'] ?? null) === 'fiscal_parameters'
&& ($basis['basis'] ?? null) === 'Tempo-official-sources-20261008'
&& preg_match('/^[a-f0-9]{64}$/D', (string) ($basis['sha256'] ?? '')) === 1
&& ($hash === null || $basis['sha256'] === $hash);
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace App\Services;
/** Explains a failed selection; never changes answers, eligibility or calculation. */
final class SimulationIvaDiagnostics
{
public function explain(array $versions, string $start, string $end, array $context): array
{
$diagnostics = [];
foreach ($versions as $version) {
$parameter = $version['parameters']['IVA_GENERAL_RATE'] ?? null;
if (! is_array($parameter)) {
continue;
}
$issues = [];
$add = function (string $category, ?string $field, string $label, mixed $received, string $expected, string $message) use (&$issues): void {
$issues[] = compact('category', 'field', 'label', 'received', 'expected', 'message');
};
if ($start < $version['valid_from'] || ($version['valid_until'] !== null && $end > $version['valid_until'])) {
$add('period_unavailable', 'fiscal_period', 'Período', "$start a $end", $version['valid_from'].' a '.($version['valid_until'] ?? 'sem data final indicada'), 'Esta versão da regra não abrange todo o período seleccionado. Não altere a data real da operação para obter um resultado.');
}
foreach ($parameter['conditions'] ?? [] as $key => $expected) {
$received = $context[$key] ?? null;
if ($received === $expected) {
continue;
}
if ($key === 'currency') {
$category = $received === null || $received === '' ? 'missing_field' : (preg_match('/^[A-Z]{3}$/D', $received) ? 'out_of_scope' : 'invalid_value');
$add($category, 'fiscal_currency', 'Moeda', $received, $expected, $category === 'missing_field' ? 'Indique a moeda da operação.' : 'Esta versão aceita apenas '.$expected.'. Reveja a moeda ou utilize um cálculo adequado à operação; a resposta não foi alterada.');
} elseif ($key === 'operation' && ! array_key_exists('operation', $context) && $expected === 'internal_general_net' && in_array($context['fiscal_operation'] ?? null, ['import', 'export', 'non_resident', 'refund'], true)) {
$add('out_of_scope', 'fiscal_operation', 'Operação', $context['fiscal_operation'], 'Operação interna à taxa geral, sobre base líquida já apurada', 'A operação declarada está excluída desta versão. Utilize um cálculo adequado à operação; não altere a resposta para obter um resultado.');
} elseif ($key === 'operation' && ! array_key_exists('operation', $context)) {
$add('system_configuration', 'fiscal_operation', 'Operação', $context['fiscal_operation'] ?? null, $expected === 'internal_general_net' ? 'Operação interna à taxa geral, sobre base líquida já apurada' : $expected, 'Limitação de integração do sistema: esta consulta não comunica o enquadramento específico exigido pela versão. A resposta genérica não permite inferi-lo. Utilize o formulário IVA — taxa geral, com declaração explícita do âmbito; não altere respostas para forçar compatibilidade.');
} elseif ($key === 'operation') {
$add($received === null || $received === '' ? 'missing_field' : 'out_of_scope', 'fiscal_operation', 'Operação', $received, $expected === 'internal_general_net' ? 'Operação interna à taxa geral, sobre base líquida já apurada' : $expected, 'A operação não está abrangida por esta versão; utilize um cálculo adequado à operação declarada.');
} else {
$add('system_configuration', null, 'Configuração da regra', null, 'Condições publicadas ligadas a campos deste formulário', 'O sistema não consegue relacionar todas as condições desta versão com o formulário. Não é uma falha das suas respostas.');
}
}
if (! SimulationFiscalSourceAcceptance::eligible($version['source'] ?? [], $version['source_sha256'] ?? null)
|| ($version['source_version_no'] ?? null) !== ($version['source']['version_no'] ?? null)
|| ($version['rule_definition']['language'] ?? null) !== 'investmoz.tax.v1') {
$add('system_configuration', null, 'Regra', null, 'Versão publicada com fonte e configuração coerentes', 'A configuração ou a fonte da regra não permite resolver este parâmetro. Não altere as respostas; é necessária correcção do sistema.');
}
if ($issues !== []) {
$source = array_intersect_key($version['source'] ?? [], array_flip(['title', 'diploma', 'article', 'url']));
$diagnostics[] = ['label' => 'IVA — taxa geral', 'rule_version' => $version['rule_version'], 'validity' => ['start' => $version['valid_from'], 'end' => $version['valid_until']], 'source' => $source, 'issues' => $issues];
}
}
return $diagnostics;
}
}
+136
View File
@@ -0,0 +1,136 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\InvestmentLegalSource;
use App\Models\SimulationTaxRuleVersion;
use Brick\Math\BigRational;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/** Admission freezes eligibility without granting the public consumer governance write access. */
final class SimulationPublishedFiscal
{
public function availability(): array
{
$enabled = (bool) config('simulation-fiscal-consumer.enabled');
$eligible = $enabled && $this->boundRule()->where('status', 'published')->exists();
return ['enabled' => $eligible, 'coverage_start' => '2026-01-01', 'coverage_end' => '2026-12-31',
'message' => $eligible ? 'IVA geral sobre base líquida disponível; enquadramento declarado.'
: 'O cálculo de IVA geral sobre base líquida está indisponível para novas execuções. Os históricos mantêm-se acessíveis.'];
}
private function boundRule(): Builder
{
if (! Str::isUuid((string) config('simulation-fiscal-consumer.rule_id')) || config('simulation-fiscal-consumer.rule_version') < 1) {
return SimulationTaxRuleVersion::query()->whereRaw('false');
}
return SimulationTaxRuleVersion::query()->whereKey(config('simulation-fiscal-consumer.rule_id'))
->where('rule_version', config('simulation-fiscal-consumer.rule_version'))
->where('rule_code', 'IVA-GERAL-BASE-LIQUIDA')->where('component', 'IVA')
->where('scope_key', 'mz.internal-general-net.mzn');
}
public function admit(array $input, bool $missingBase = false): array
{
if (isset($input['monthly_sales'])) {
return $this->admitMonthly($input);
}
// Called inside createPayload, after idempotent replay, never by the worker.
abort_unless(DB::transactionLevel() > 0, 500);
abort_unless(config('simulation-fiscal-consumer.enabled'), 503,
'O cálculo de IVA está temporariamente indisponível. Os dados e históricos foram preservados.');
DB::select('select pg_advisory_xact_lock_shared(261021, hashtext(?))', [(string) config('simulation-fiscal-consumer.rule_id')]);
$rule = $this->boundRule()->first();
abort_unless($rule && $rule->status === 'published', 503,
'A versão autorizada de IVA não está disponível para novas execuções. Os históricos foram preservados.');
abort_if($input['reference_date'] < '2026-01-01' || $input['reference_date'] > '2026-12-31', 422,
'Este cálculo abrange apenas exigibilidade em 2026. As entradas foram preservadas.');
abort_if(collect($rule->rule_definition['nodes'] ?? [])->contains(fn (array $node): bool => ($node['op'] ?? null) === 'round'), 422,
'A versão seleccionada não é compatível com o cálculo exacto deste consumidor.');
DB::select('select pg_advisory_xact_lock_shared(261022, hashtext(?))', [(string) $rule->legal_source_id]);
$source = InvestmentLegalSource::whereKey($rule->legal_source_id)->firstOrFail();
// Publish calculation provenance, not private governance notes or staff identifiers.
$version = array_intersect_key($rule->attributesToArray(), array_flip([
'id', 'rule_code', 'rule_version', 'component', 'scope_key', 'status', 'valid_from', 'valid_until',
'legal_source_id', 'source_version_no', 'source_sha256', 'rule_definition', 'parameters', 'rounding_policy',
]));
$version['source'] = array_intersect_key($source->attributesToArray(), array_flip([
'id', 'title', 'url', 'diploma', 'article', 'competent_entity', 'valid_from', 'valid_until', 'validation_status', 'version_no',
]));
$resolution = app(SimulationTaxParameterResolver::class)->resolvePublishedForPeriod('IVA',
$input['reference_date'], $input['reference_date'], ['currency' => $input['currency'], 'operation' => $input['operation']]);
$rate = collect($resolution)->firstWhere('parameter_code', 'IVA_GENERAL_RATE');
$missing = [];
if ($missingBase) {
$missing[] = 'taxable_sales';
}
$evaluation = null;
if (! ($input['scope_declared'] ?? false)) {
$missing[] = 'operation_scope';
}
if (($rate['status'] ?? null) !== 'resolved' || ($rate['unit'] ?? null) !== 'percent'
|| ($rate['rule_id'] ?? null) !== (string) $rule->id
|| (int) ($rate['rule_version'] ?? 0) !== (int) $rule->rule_version) {
$missing[] = 'eligible_parameter';
}
if ($missing === []) {
$evaluation = app(SimulationTaxRuleExpression::class)->evaluate($rule->rule_definition,
['base_tributavel' => $input['base']], ['iva_general_rate' => $rate['value']]);
}
return app(SimulationFiscalSnapshot::class)->build($version, $input, $resolution, $evaluation, $missing, true);
}
private function admitMonthly(array $input): array
{
$rows = [];
$baseTotal = BigRational::zero();
$vatTotal = BigRational::zero();
$complete = true;
$previous = null;
foreach ($input['monthly_sales'] as $sale) {
abort_unless($previous === null || $sale['date'] === date('Y-m-d', strtotime($previous.' +1 month')), 422, 'A projecção deve conter meses consecutivos, sem duplicações.');
$previous = $sale['date'];
$known = $sale['amount'] !== null && $sale['amount'] !== '';
$net = $input['sales_vat_basis'] === 'net';
$month = $this->admit(['base' => $known && $net ? $sale['amount'] : null,
'currency' => $input['currency'], 'reference_date' => $sale['date'],
'operation' => 'internal_general_net', 'scope_declared' => $input['scope_declared']], ! $known || ! $net);
$snapshot ??= $month;
$rate = collect($month['fiscal_provenance']['resolution'])->firstWhere('parameter_code', 'IVA_GENERAL_RATE');
$vat = $month['result']['value'];
$status = ! $known ? 'missing_sales' : (! $net ? 'unsupported_basis' : ($vat === null ? 'indeterminate' : 'calculated'));
$rows[] = ['date' => $sale['date'], 'sales_declared' => $sale['amount'],
'net_sales' => $known && $net ? $sale['amount'] : null,
'rate' => $vat !== null && ($rate['status'] ?? null) === 'resolved' ? $rate['value'] : null,
'vat' => $vat, 'gross_sales' => $vat === null ? null : (string) BigRational::of($sale['amount'])->plus($vat)->toBigDecimal(),
'status' => $status, 'rule_id' => $month['fiscal_provenance']['rule']['id'],
'rule_version' => $month['fiscal_provenance']['rule']['rule_version']];
if ($vat === null) {
$complete = false;
} else {
$baseTotal = $baseTotal->plus($sale['amount']);
$vatTotal = $vatTotal->plus($vat);
}
}
$value = $complete ? (string) $vatTotal->toBigDecimal() : null;
$snapshot['base'] = $complete ? (string) $baseTotal->toBigDecimal() : null;
$snapshot['indicative_total'] = $value;
$snapshot['result']['value'] = $value;
$snapshot['result']['status'] = $complete ? 'calculated' : 'indeterminate';
$snapshot['result']['missing_inputs'] = $complete ? [] : ['monthly_sales_or_net_basis_or_scope'];
$snapshot['result']['explanation'] = 'IVA mensal sobre vendas declaradas sem IVA, pela versão publicada congelada na admissão. Valores com IVA incluído ou sem informação permanecem indeterminados. Não é imposto final a pagar.';
$snapshot['presentation']['value'] = $value;
$snapshot['intermediate'] = null;
$snapshot['monthly_projection'] = ['input' => $input, 'rows' => $rows,
'cash_treatment' => 'Informação separada da tesouraria: os recebimentos declarados não são aumentados pelo IVA nem o IVA é deduzido como custo ou imposto a pagar. Calendário de cobrança e liquidação fiscal não é inferido.'];
return $snapshot;
}
}
+10 -1
View File
@@ -8,15 +8,24 @@ final class SimulationTaxGovernanceAccess
{
private const ROLES = ['editors', 'reviewers', 'approvers', 'publishers'];
public static function maintainer(User $user): bool
{
return $user->hasVerifiedEmail() && ! app(PortalProfileAccess::class)->has($user, 'auditor') && ($user->can('manage-portal')
|| app(ScopedPortalAdministration::class)->administrative($user));
}
public static function requireRole(User $user, string $role): void
{
abort_unless(in_array($role, self::ROLES, true), 500, 'Função de governação desconhecida.');
abort_unless(in_array(strtolower($user->email), config('simulation-tax-governance.'.$role, []), true),
abort_unless(self::maintainer($user) || in_array(strtolower($user->email), config('simulation-tax-governance.'.$role, []), true),
403, 'O utilizador não está designado para esta função de governação fiscal.');
}
public static function requireAnyRole(User $user): void
{
if (self::maintainer($user)) {
return;
}
foreach (self::ROLES as $role) {
if (in_array(strtolower($user->email), config('simulation-tax-governance.'.$role, []), true)) {
return;
+44 -20
View File
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Services;
use App\Support\IsolatedQaDatabase;
use Brick\Math\BigRational;
use DateTimeImmutable;
use Illuminate\Support\Facades\DB;
@@ -16,32 +17,32 @@ final class SimulationTaxParameterResolver
public function validateParameters(array $parameters): void
{
if ($parameters === [] || count($parameters) > 100) {
throw new InvalidArgumentException('Provide between 1 and 100 versioned parameters.');
throw new InvalidArgumentException('Indique entre 1 e 100 parâmetros versionados.');
}
foreach ($parameters as $code => $parameter) {
if (! is_string($code) || ! preg_match('/^[A-Z][A-Z0-9._-]{1,63}$/', $code) || ! is_array($parameter)
|| array_diff(array_keys($parameter), ['value', 'unit', 'conditions']) !== []
|| array_diff(['value', 'unit', 'conditions'], array_keys($parameter)) !== []) {
throw new InvalidArgumentException('Each parameter requires only value, unit, and conditions.');
throw new InvalidArgumentException('Cada parâmetro deve conter apenas valor, unidade e condições.');
}
if ((! is_string($parameter['value']) && ! is_int($parameter['value'])) || (string) $parameter['value'] === ''
|| ! is_string($parameter['unit']) || ! preg_match('/^[a-z][a-z0-9._-]{0,31}$/', $parameter['unit'])
|| ! is_array($parameter['conditions'])) {
throw new InvalidArgumentException('Invalid parameter value, unit, or conditions.');
throw new InvalidArgumentException('Reveja o valor, a unidade e as condições do parâmetro.');
}
$isNumericValue = preg_match('/^-?\d+(?:\.\d+)?$/', (string) $parameter['value']) === 1;
if (in_array($parameter['unit'], ['percent', 'mzn', 'amount', 'days', 'count', 'mzn_per_unit', 'per_unit'], true) && ! $isNumericValue) {
throw new InvalidArgumentException('Numeric parameter units require a plain decimal value.');
throw new InvalidArgumentException('Esta unidade requer um valor numérico decimal.');
}
if ($isNumericValue) {
$value = BigRational::of((string) $parameter['value']);
if ($value->isNegative() || ($parameter['unit'] === 'percent' && $value->compareTo(100) > 0)) {
throw new InvalidArgumentException('Numeric parameters cannot be negative; percentages must be between 0 and 100.');
throw new InvalidArgumentException('Os valores não podem ser negativos; as percentagens devem estar entre 0 e 100.');
}
}
foreach ($parameter['conditions'] as $condition => $expected) {
if (! is_string($condition) || ! is_string($expected)) {
throw new InvalidArgumentException('Conditions must be a flat map of string assumptions.');
throw new InvalidArgumentException('Indique as condições numa lista simples de nomes e valores em texto.');
}
}
}
@@ -52,32 +53,34 @@ final class SimulationTaxParameterResolver
* never executes a tax formula and requires each parameter to declare context.
*
* @param array<string,string> $context
* @return list<array<string,mixed>>
* @return list<array<string,mixed>>|array{parameters:list<array<string,mixed>>,diagnostics:list<array<string,mixed>>}
*/
public function resolvePublishedForPeriod(string $component, string $periodStart, string $periodEnd, array $context): array
public function resolvePublishedForPeriod(string $component, string $periodStart, string $periodEnd, array $context, bool $diagnostics = false): array
{
$start = $this->date($periodStart);
$end = $this->date($periodEnd);
if ($end < $start || $start->diff($end)->days > 365) {
throw new InvalidArgumentException('Invalid effective period.');
throw new InvalidArgumentException('Reveja o período de aplicação indicado.');
}
$rows = DB::table('simulation_tax_rule_versions as rule')
->join('investment_legal_sources as source', 'source.id', '=', 'rule.legal_source_id')
->where('rule.component', $component)
->where('rule.status', 'published')
->whereDate('rule.valid_from', '<=', $periodEnd)
->where(fn ($query) => $query->whereNull('rule.valid_until')->orWhereDate('rule.valid_until', '>=', $periodStart))
->where('source.validation_status', 'validated')
->when(! $diagnostics, fn ($query) => $query->whereDate('rule.valid_from', '<=', $periodEnd)
->where(fn ($query) => $query->whereNull('rule.valid_until')->orWhereDate('rule.valid_until', '>=', $periodStart)))
->where(fn ($query) => $query->where('source.validation_status', 'validated')->orWhere(fn ($fiscal) => $fiscal->where('source.validation_status', 'pending')->where('source.fiscal_acceptance->scope', 'fiscal_parameters')
->where('source.fiscal_acceptance->basis', 'Tempo-official-sources-20261008')
->whereRaw("source.fiscal_acceptance->>'sha256'=rule.source_sha256")))
->whereColumn('source.version_no', 'rule.source_version_no')
->whereDate('source.valid_from', '<=', $periodStart)
->where(fn ($query) => $query->whereNull('source.valid_until')->orWhereDate('source.valid_until', '>=', $periodEnd))
->when(! $diagnostics, fn ($query) => $query->whereDate('source.valid_from', '<=', $periodStart)
->where(fn ($query) => $query->whereNull('source.valid_until')->orWhereDate('source.valid_until', '>=', $periodEnd)))
->select([
'rule.id', 'rule.rule_code', 'rule.rule_version', 'rule.component', 'rule.scope_key',
'rule.valid_from', 'rule.valid_until', 'rule.parameters', 'rule.rule_definition',
'rule.source_version_no', 'rule.source_sha256',
'source.title as source_title', 'source.diploma as source_diploma', 'source.article as source_article',
'source.url as source_url', 'source.validation_status as source_validation_status',
'source.url as source_url', 'source.validation_status as source_validation_status', 'source.fiscal_acceptance',
'source.version_no as source_current_version', 'source.valid_from as source_valid_from',
'source.valid_until as source_valid_until',
])
@@ -110,6 +113,7 @@ final class SimulationTaxParameterResolver
'article' => $row->source_article,
'url' => $row->source_url,
'validation_status' => $row->source_validation_status,
'fiscal_acceptance' => is_array($row->fiscal_acceptance) ? $row->fiscal_acceptance : json_decode($row->fiscal_acceptance ?? 'null', true),
'version_no' => (int) $row->source_current_version,
'valid_from' => (string) $row->source_valid_from,
'valid_until' => $row->source_valid_until === null ? null : (string) $row->source_valid_until,
@@ -117,7 +121,11 @@ final class SimulationTaxParameterResolver
];
})->all());
return $this->resolveAvailableParameters($versions, $component, $periodStart, $periodEnd, $context);
$parameters = $this->resolveAvailableParameters($versions, $component, $periodStart, $periodEnd, $context);
$resolvedIva = array_filter($parameters, fn (array $parameter): bool => $parameter['parameter_code'] === 'IVA_GENERAL_RATE' && $parameter['status'] === 'resolved');
return $diagnostics ? ['parameters' => $parameters, 'diagnostics' => $resolvedIva !== [] ? [] : (new SimulationIvaDiagnostics)->explain($versions, $periodStart, $periodEnd, $context)] : $parameters;
}
/**
@@ -126,15 +134,31 @@ final class SimulationTaxParameterResolver
* @return list<array<string,mixed>>
*/
public function resolveAvailableParameters(array $versions, string $component, string $periodStart, string $periodEnd, array $context): array
{
return $this->resolveWithStatus($versions, $component, $periodStart, $periodEnd, $context, 'published');
}
/** Explicit draft preview. Never used by the published-parameter API. */
public function resolveDraftForQa(array $versions, string $component, string $periodStart, string $periodEnd, array $context): array
{
abort_unless(IsolatedQaDatabase::allowsSchemaChanges(), 404);
return $this->resolveWithStatus($versions, $component, $periodStart, $periodEnd, $context, 'draft');
}
private function resolveWithStatus(array $versions, string $component, string $periodStart, string $periodEnd, array $context, string $requiredStatus): array
{
$start = $this->date($periodStart);
$end = $this->date($periodEnd);
if ($end < $start || $start->diff($end)->days > 365) {
throw new InvalidArgumentException('Reveja o período de aplicação indicado.');
}
$groups = [];
foreach ($versions as $version) {
if (($version['component'] ?? null) !== $component
|| ($version['status'] ?? null) !== 'published'
|| ($version['source']['validation_status'] ?? null) !== 'validated'
|| ($version['status'] ?? null) !== $requiredStatus
|| ! SimulationFiscalSourceAcceptance::eligible($version['source'] ?? [], $version['source_sha256'] ?? null)
|| (int) ($version['source_version_no'] ?? 0) !== (int) ($version['source']['version_no'] ?? -1)
|| ! $this->overlaps($version, $start, $end)
|| ! $this->covers($version['source'], $start) || ! $this->covers($version['source'], $end)
@@ -232,7 +256,7 @@ final class SimulationTaxParameterResolver
|| ($version['component'] ?? null) !== $component
|| ($version['scope_key'] ?? null) !== $scopeKey
|| ! $this->covers($version, $date)
|| ($version['source']['validation_status'] ?? null) !== 'validated'
|| ! SimulationFiscalSourceAcceptance::eligible($version['source'] ?? [], $version['source_sha256'] ?? null)
|| (int) ($version['source_version_no'] ?? 0) !== (int) ($version['source']['version_no'] ?? -1)
|| ! $this->covers($version['source'], $date)
|| ! preg_match('/^[a-f0-9]{64}$/D', (string) ($version['source_sha256'] ?? ''))) {
@@ -315,7 +339,7 @@ final class SimulationTaxParameterResolver
{
$parsed = DateTimeImmutable::createFromFormat('!Y-m-d', $date);
if ($parsed === false || $parsed->format('Y-m-d') !== $date) {
throw new InvalidArgumentException('Effective date must use YYYY-MM-DD.');
throw new InvalidArgumentException('Indique uma data de aplicação válida no formato ano-mês-dia.');
}
return $parsed;
+14 -14
View File
@@ -95,16 +95,16 @@ final class SimulationTaxRuleExpression
private function topologicalOrder(array $definition): array
{
if (($definition['language'] ?? null) !== self::LANGUAGE || ! is_array($definition['nodes'] ?? null) || ! is_string($definition['result'] ?? null)) {
throw new InvalidArgumentException('Unsupported expression definition.');
throw new InvalidArgumentException('O formato da expressão não é suportado.');
}
$nodes = $definition['nodes'];
if ($nodes === [] || count($nodes) > 100) {
throw new InvalidArgumentException('Expression must contain between 1 and 100 nodes.');
throw new InvalidArgumentException('A expressão deve conter entre 1 e 100 elementos.');
}
foreach ($nodes as $id => $node) {
if (! is_string($id) || ! preg_match('/^[a-z][a-z0-9_]{0,47}$/', $id) || ! is_array($node) || ! is_string($node['op'] ?? null)) {
throw new InvalidArgumentException('Invalid expression node.');
throw new InvalidArgumentException('Reveja o elemento indicado na expressão.');
}
$op = $node['op'];
$allowedFields = match ($op) {
@@ -112,40 +112,40 @@ final class SimulationTaxRuleExpression
'literal' => ['op', 'value'],
'add', 'subtract', 'multiply', 'divide', 'min', 'max' => ['op', 'args'],
'round' => ['op', 'args', 'scale', 'mode'],
default => throw new InvalidArgumentException('Expression operation is not allowed.'),
default => throw new InvalidArgumentException('Esta operação não é suportada na expressão.'),
};
if (array_diff(array_keys($node), $allowedFields) !== [] || array_diff($allowedFields, array_keys($node)) !== []) {
throw new InvalidArgumentException('Expression node has missing or unsupported fields.');
throw new InvalidArgumentException('Há campos em falta ou não suportados num elemento da expressão.');
}
if (in_array($op, ['input', 'parameter'], true) && (! is_string($node['key']) || ! preg_match('/^[a-z][a-z0-9_.-]{0,79}$/', $node['key']))) {
throw new InvalidArgumentException('Invalid input or parameter key.');
throw new InvalidArgumentException('Reveja o identificador da entrada ou do parâmetro.');
}
if ($op === 'literal') {
$this->number($node['value']);
}
if (isset($node['args'])) {
if (! is_array($node['args'])) {
throw new InvalidArgumentException('Invalid expression arguments.');
throw new InvalidArgumentException('Reveja os argumentos da expressão.');
}
$count = count($node['args']);
$validCount = $op === 'round' ? $count === 1 : ($op === 'subtract' || $op === 'divide' ? $count === 2 : $count >= 2 && $count <= 20);
if (! $validCount || count(array_filter($node['args'], 'is_string')) !== $count) {
throw new InvalidArgumentException('Invalid expression arguments.');
throw new InvalidArgumentException('Reveja os argumentos da expressão.');
}
}
if ($op === 'round' && (! is_int($node['scale']) || $node['scale'] < 0 || $node['scale'] > 12 || ! isset(self::ROUNDING[$node['mode']]))) {
throw new InvalidArgumentException('Invalid explicit rounding configuration.');
throw new InvalidArgumentException('Reveja a configuração de arredondamento.');
}
}
if (! array_key_exists($definition['result'], $nodes)) {
throw new InvalidArgumentException('Expression result references an unknown node.');
throw new InvalidArgumentException('O resultado da expressão refere um elemento inexistente.');
}
$state = [];
$order = [];
$visit = function (string $id) use (&$visit, &$state, &$order, $nodes): void {
if (($state[$id] ?? 0) === 1) {
throw new InvalidArgumentException('Expression dependency graph contains a cycle.');
throw new InvalidArgumentException('A expressão contém uma dependência circular.');
}
if (($state[$id] ?? 0) === 2) {
return;
@@ -153,7 +153,7 @@ final class SimulationTaxRuleExpression
$state[$id] = 1;
foreach ($nodes[$id]['args'] ?? [] as $dependency) {
if (! is_string($dependency) || ! array_key_exists($dependency, $nodes)) {
throw new InvalidArgumentException('Expression references an unknown node.');
throw new InvalidArgumentException('A expressão refere um elemento inexistente.');
}
$visit($dependency);
}
@@ -191,13 +191,13 @@ final class SimulationTaxRuleExpression
if ($op === 'min' || $op === 'max') {
return array_reduce(array_slice($args, 1), static fn (BigRational $carry, BigRational $value): BigRational => ($op === 'min' ? $carry->compareTo($value) <= 0 : $carry->compareTo($value) >= 0) ? $carry : $value, $args[0]);
}
throw new InvalidArgumentException('Expression operation is not allowed.');
throw new InvalidArgumentException('Esta operação não é suportada na expressão.');
}
private function number(mixed $value): BigRational
{
if ((! is_string($value) && ! is_int($value)) || ! preg_match('/^-?\d+(?:\.\d+)?$/', (string) $value)) {
throw new InvalidArgumentException('Numbers must be plain decimal strings or integers.');
throw new InvalidArgumentException('Introduza valores numéricos decimais ou inteiros.');
}
return BigRational::of((string) $value);
+9 -8
View File
@@ -65,6 +65,8 @@ final class SimulationTaxRuleGovernance
return DB::transaction(function () use ($actor, $id, $version, $action, $evidence): SimulationTaxRuleVersion {
$this->setContext($actor);
// Serialize public admission with publication/suspension, without widening RLS.
DB::select('select pg_advisory_xact_lock(261021, hashtext(?))', [$id]);
$rule = SimulationTaxRuleVersion::query()->lockForUpdate()->findOrFail($id);
$this->matchVersion($rule, $version);
$next = ['version_no' => $version + 1];
@@ -78,15 +80,12 @@ final class SimulationTaxRuleGovernance
break;
case 'review':
case 'reject':
abort_unless((int) $rule->author_id !== $actor->id, 403, 'O autor não pode rever a própria regra.');
abort_unless($rule->status === 'submitted', 422, 'A regra tem de estar submetida para revisão.');
$this->sourceStillValid($rule);
$next += ['status' => $action === 'review' ? 'reviewed' : 'rejected', 'reviewer_id' => $actor->id,
'review_evidence' => $evidence, 'reviewed_at' => now()];
break;
case 'approve':
abort_unless((int) $rule->author_id !== $actor->id && (int) $rule->reviewer_id !== $actor->id, 403,
'Autor e revisor não podem aprovar a mesma regra.');
abort_unless($rule->status === 'reviewed', 422, 'A regra tem de estar revista antes da aprovação.');
$this->sourceStillValid($rule);
$next += ['status' => 'approved', 'approver_id' => $actor->id,
@@ -94,9 +93,6 @@ final class SimulationTaxRuleGovernance
break;
case 'publish':
abort_unless($rule->status === 'approved', 422, 'A regra tem de estar aprovada antes da publicação.');
abort_unless((int) $rule->author_id !== $actor->id && (int) $rule->reviewer_id !== $actor->id
&& (int) $rule->approver_id !== $actor->id, 403,
'Autor, revisor e aprovador não podem publicar a mesma regra.');
$this->sourceStillValid($rule);
$this->assertNoPublishedOverlap($rule);
$next += ['status' => 'published', 'publisher_id' => $actor->id,
@@ -155,7 +151,11 @@ final class SimulationTaxRuleGovernance
$source = InvestmentLegalSource::query()->whereKey($data['legal_source_id'])->firstOrFail();
abort_unless(preg_match('/^[a-f0-9]{64}$/D', (string) $data['source_sha256']) === 1, 422,
'Indique o SHA-256 do exemplar oficial efectivamente revisto.');
abort_unless($source->validation_status === 'validated', 422, 'A fonte jurídica ainda não foi validada.');
abort_unless(SimulationFiscalSourceAcceptance::eligible($source->attributesToArray(), $data['source_sha256']), 422,
'A fonte ainda não está aceite para este cadastro fiscal.');
abort_if(is_array($source->fiscal_acceptance)
&& ($source->fiscal_acceptance['sha256'] ?? null) !== $data['source_sha256'], 422,
'O hash da regra deve corresponder ao exemplar registado na aceitação da fonte fiscal.');
abort_unless($source->valid_from <= $data['valid_from']
&& ($source->valid_until === null || ($data['valid_until'] !== null && $source->valid_until >= $data['valid_until']))
&& ($data['valid_until'] !== null || $source->valid_until === null), 422,
@@ -167,7 +167,8 @@ final class SimulationTaxRuleGovernance
private function sourceStillValid(SimulationTaxRuleVersion $rule): void
{
$source = InvestmentLegalSource::query()->lockForUpdate()->find($rule->legal_source_id);
abort_unless($source !== null && $source->validation_status === 'validated'
abort_unless($source !== null && SimulationFiscalSourceAcceptance::eligible($source->attributesToArray(), $rule->source_sha256)
&& (! is_array($source->fiscal_acceptance) || ($source->fiscal_acceptance['sha256'] ?? null) === $rule->source_sha256)
&& (int) $source->version_no === (int) $rule->source_version_no
&& $source->valid_from <= $rule->valid_from->format('Y-m-d')
&& ($source->valid_until === null || ($rule->valid_until !== null && $source->valid_until >= $rule->valid_until->format('Y-m-d')))
+5 -4
View File
@@ -250,7 +250,8 @@ class VirtualDesk
* @return array<string,mixed> */
public function validateFields(array $payload, array $definition, bool $submitting): array
{
$rules = ['payload' => ['required', 'array:'.implode(',', [...array_keys($definition['fields']), 'country', 'declared_origin_country_code'])],
$rules = ['payload' => ['required', 'array:'.implode(',', [...array_keys($definition['fields']), 'country', 'declared_origin_country_code', 'context_reuse'])],
'payload.context_reuse' => ['nullable', 'string', 'max:10000'],
'payload.country' => ['nullable', 'string', 'max:100', app(CountryCatalogue::class)->legacyCountryRule()],
'payload.declared_origin_country_code' => ['nullable', 'string', app(CountryCatalogue::class)->rule()]];
foreach ($definition['fields'] as $key => $field) {
@@ -388,7 +389,7 @@ class VirtualDesk
abort_if($this->owns($actor, $case, false), 403, 'O proponente não pode analisar ou decidir o seu próprio pedido.');
abort_unless($actor->can('manage-portal') || (int) $request->analyst_id === $actor->id, 403);
if (in_array($data['status'], ['accepted', 'rejected', 'closed'], true)) {
abort_unless(app(ScopedPortalAdministration::class)->deskDecision($actor,$request), 403);
abort_unless(app(ScopedPortalAdministration::class)->deskDecision($actor, $request), 403);
}
abort_unless(in_array($data['status'], config('virtual-desk.transitions.'.$request->status, []), true), 409, 'Transição inválida.');
}
@@ -412,13 +413,13 @@ class VirtualDesk
return DB::transaction(function () use ($actor, $id, $data) {
$case = $this->case($actor, $id, false, true);
abort_unless($this->owns($actor, $case) || app(ScopedPortalAdministration::class)->deskCaseDecision($actor,$case->id), 403);
abort_unless($this->owns($actor, $case) || app(ScopedPortalAdministration::class)->deskCaseDecision($actor, $case->id), 403);
abort_unless((int) $case->version === (int) $data['version'], 409);
$workflow = json_decode($case->workflow, true, 512, JSON_THROW_ON_ERROR);
$current = $workflow[$data['stage']];
$decision = in_array($data['status'], ['completed', 'not_applicable'], true);
if ($decision) {
abort_unless(app(ScopedPortalAdministration::class)->deskCaseDecision($actor,$case->id) && ! $this->owns($actor, $case, false), 403, 'Conclusão documental exige revisão interna por outra pessoa autorizada.');
abort_unless(app(ScopedPortalAdministration::class)->deskCaseDecision($actor, $case->id) && ! $this->owns($actor, $case, false), 403, 'Conclusão documental exige revisão interna por outra pessoa autorizada.');
abort_unless(trim($data['evidence'] ?? '') !== '', 422, 'Identifique a evidência documental.');
foreach ($current['dependencies'] as $dependency) {
abort_unless(in_array($workflow[$dependency]['status'], ['completed', 'not_applicable'], true) || (($data['exception'] ?? false) && ($data['document_id'] ?? null)), 422, 'Dependência '.$dependency.' pendente; uma exceção exige evidência própria.');
+24 -1
View File
@@ -1,6 +1,7 @@
<?php
use App\Http\Middleware\AuditProfileDecision;
use App\Http\Middleware\EnsureHomologationAccess;
use App\Http\Middleware\EnsureSimulationRecoveryCookie;
use App\Http\Middleware\HandleAppearance;
use App\Http\Middleware\HandleInertiaRequests;
@@ -30,7 +31,7 @@ return Application::configure(basePath: dirname(__DIR__))
$middleware->encryptCookies(except: ['appearance', 'sidebar_state', EnsureSimulationRecoveryCookie::NAME]);
$middleware->web(append: [
\App\Http\Middleware\EnsureHomologationAccess::class,
EnsureHomologationAccess::class,
PortalProfileSession::class,
ProtectPrivilegedIdentity::class,
AuditProfileDecision::class,
@@ -68,6 +69,28 @@ return Application::configure(basePath: dirname(__DIR__))
fn (Request $request) => $request->is('api/*') || $request->expectsJson(),
);
$exceptions->respond(function (Response $response, Throwable $exception, Request $request): Response {
if (($request->is('api/*') || $request->expectsJson()) && $response->getStatusCode() >= 400) {
$body = json_decode((string) $response->getContent(), true);
$genericMessages = [
'Not Found' => 'O registo não foi encontrado ou não está acessível nesta conta.',
'Not found.' => 'O registo não foi encontrado ou não está acessível nesta conta.',
'Forbidden' => 'Esta conta não tem acesso a esta operação.',
'Forbidden.' => 'Esta conta não tem acesso a esta operação.',
'Unauthenticated.' => 'Entre na sua conta para continuar esta operação.',
'CSRF token mismatch.' => 'A sessão expirou. Entre novamente antes de repetir a operação.',
'Too Many Attempts.' => 'Foram feitas várias tentativas. Aguarde um pouco e tente novamente.',
'Server Error' => 'Não foi possível concluir a operação. Tente novamente.',
];
if (is_array($body) && is_string($body['message'] ?? null) && isset($genericMessages[$body['message']])) {
$body['message'] = $genericMessages[$body['message']];
$response->setContent(json_encode($body, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
} elseif (is_array($body) && is_string($body['message'] ?? null)
&& $response->getStatusCode() === 404 && str_starts_with($body['message'], 'The route ')
&& str_ends_with($body['message'], ' could not be found.')) {
$body['message'] = $genericMessages['Not Found'];
$response->setContent(json_encode($body, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
}
}
if ($request->is('api/*')) {
$response->headers->set(
'X-Correlation-ID',
+1 -1
View File
@@ -78,7 +78,7 @@ return [
|
*/
'locale' => env('APP_LOCALE', 'en'),
'locale' => env('APP_LOCALE', 'pt'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
+1
View File
@@ -2,6 +2,7 @@
return [
// Explicit opt-in after authenticated installation; never infer activation from schema/ready.
'private_preparation' => env('PRIVATE_INVESTMENT_PREPARATION_ENABLED', false),
'enabled' => env('INVESTOR_PATH_ENABLED', false),
'definition_id' => '61000919-0000-4000-8000-000000000001',
];
+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'),
+8
View File
@@ -0,0 +1,8 @@
<?php
return [
// Publication and consumer exposure are separate Tempo decisions.
'enabled' => env('INVESTMOZ_IVA_CONSUMER_ENABLED', false),
'rule_id' => env('INVESTMOZ_IVA_CONSUMER_RULE_ID'),
'rule_version' => (int) env('INVESTMOZ_IVA_CONSUMER_RULE_VERSION', 0),
];
+3 -2
View File
@@ -1,8 +1,9 @@
<?php
return [
// These lists are deliberately empty by default. A portal manager and MFA are
// also required; governance roles never activate rules in the calculation engine.
// Optional specific assignments. Existing authorised administration can maintain
// fiscal parameters with MFA, without four people or four email designations.
// Rule publication never activates the calculation consumer by itself.
'editors' => array_values(array_filter(array_map('strtolower', array_map('trim', explode(',', (string) env('INVESTMOZ_TAX_RULE_EDITORS', '')))))),
'reviewers' => array_values(array_filter(array_map('strtolower', array_map('trim', explode(',', (string) env('INVESTMOZ_TAX_RULE_REVIEWERS', '')))))),
'approvers' => array_values(array_filter(array_map('strtolower', array_map('trim', explode(',', (string) env('INVESTMOZ_TAX_RULE_APPROVERS', '')))))),
@@ -0,0 +1,68 @@
<?php
use App\Support\IsolatedQaDatabase;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
if (! IsolatedQaDatabase::allowsSchemaChanges()) {
throw new LogicException('Ensaie esta correcção apenas numa base QA isolada. A aplicação no destino exige decisão, backup/restauro e acção autenticada.');
}
// Only fiscal rule maintenance changes. Opportunity/zone workflow checks remain intact.
DB::statement('ALTER TABLE simulation_tax_rule_versions DROP CONSTRAINT tax_rule_stage_evidence_check, DROP CONSTRAINT tax_rule_approval_check, DROP CONSTRAINT tax_rule_publication_check');
DB::statement("ALTER TABLE simulation_tax_rule_versions
ADD CONSTRAINT tax_rule_stage_evidence_check CHECK(status NOT IN ('reviewed','rejected','approved','published','suspended','archived') OR (reviewer_id IS NOT NULL AND submitted_at IS NOT NULL AND reviewed_at IS NOT NULL AND length(trim(review_evidence))>=20)),
ADD CONSTRAINT tax_rule_approval_check CHECK(status NOT IN ('approved','published','suspended','archived') OR (approver_id IS NOT NULL AND approved_at IS NOT NULL AND length(trim(approval_evidence))>=20)),
ADD CONSTRAINT tax_rule_publication_check CHECK(status NOT IN ('published','suspended','archived') OR (publisher_id IS NOT NULL AND published_at IS NOT NULL AND length(trim(publication_evidence))>=20))");
DB::statement('ALTER TABLE investment_legal_sources ADD COLUMN fiscal_acceptance jsonb NULL');
DB::statement("ALTER TABLE investment_legal_sources ADD CONSTRAINT investment_legal_source_fiscal_acceptance_check CHECK (
fiscal_acceptance IS NULL OR COALESCE((
fiscal_acceptance->>'scope'='fiscal_parameters' AND
fiscal_acceptance->>'basis'='Tempo-official-sources-20261008' AND
fiscal_acceptance->>'responsible_user_id'=owner_id::text AND
fiscal_acceptance->>'sha256' ~ '^[a-f0-9]{64}$' AND
fiscal_acceptance->>'legal_opinion'='false' AND fiscal_acceptance->>'independent_review'='false' AND
length(trim(fiscal_acceptance->>'citation'))>=10 AND
length(trim(fiscal_acceptance->>'official_origin'))>=10
), false))");
DB::statement('DROP POLICY simulation_tax_rule_versions_public_read ON simulation_tax_rule_versions');
DB::statement("CREATE POLICY simulation_tax_rule_versions_public_read ON simulation_tax_rule_versions FOR SELECT USING (
status='published' AND EXISTS (SELECT 1 FROM investment_legal_sources source
WHERE source.id=simulation_tax_rule_versions.legal_source_id
AND (source.validation_status='validated' OR (source.validation_status='pending'
AND source.fiscal_acceptance->>'scope'='fiscal_parameters'
AND source.fiscal_acceptance->>'basis'='Tempo-official-sources-20261008'
AND source.fiscal_acceptance->>'sha256'=simulation_tax_rule_versions.source_sha256))
AND source.version_no=simulation_tax_rule_versions.source_version_no
AND source.valid_from<=simulation_tax_rule_versions.valid_from
AND (source.valid_until IS NULL OR (simulation_tax_rule_versions.valid_until IS NOT NULL AND source.valid_until>=simulation_tax_rule_versions.valid_until))
AND (simulation_tax_rule_versions.valid_until IS NOT NULL OR source.valid_until IS NULL)))");
}
public function down(): void
{
if (! IsolatedQaDatabase::allowsSchemaChanges()) {
throw new LogicException('Reversão permitida apenas em QA isolado.');
}
$used = DB::table('investment_legal_sources')->whereNotNull('fiscal_acceptance')->exists()
|| DB::table('simulation_tax_rule_versions')->whereRaw('reviewer_id=author_id OR approver_id=author_id OR approver_id=reviewer_id OR publisher_id=author_id OR publisher_id=reviewer_id OR publisher_id=approver_id')->exists();
if ($used) {
throw new LogicException('Reversão recusada: conservar os cadastros e históricos legítimos; desactivar novas execuções em vez de apagar registos.');
}
DB::statement('ALTER TABLE simulation_tax_rule_versions DROP CONSTRAINT tax_rule_stage_evidence_check, DROP CONSTRAINT tax_rule_approval_check, DROP CONSTRAINT tax_rule_publication_check');
DB::statement("ALTER TABLE simulation_tax_rule_versions
ADD CONSTRAINT tax_rule_stage_evidence_check CHECK(status NOT IN ('reviewed','rejected','approved','published','suspended','archived') OR (reviewer_id IS NOT NULL AND reviewer_id<>author_id AND submitted_at IS NOT NULL AND reviewed_at IS NOT NULL AND length(trim(review_evidence))>=20)),
ADD CONSTRAINT tax_rule_approval_check CHECK(status NOT IN ('approved','published','suspended','archived') OR (approver_id IS NOT NULL AND approver_id<>author_id AND approver_id<>reviewer_id AND approved_at IS NOT NULL AND length(trim(approval_evidence))>=20)),
ADD CONSTRAINT tax_rule_publication_check CHECK(status NOT IN ('published','suspended','archived') OR (publisher_id IS NOT NULL AND publisher_id<>author_id AND publisher_id<>reviewer_id AND publisher_id<>approver_id AND published_at IS NOT NULL AND length(trim(publication_evidence))>=20))");
DB::statement('DROP POLICY simulation_tax_rule_versions_public_read ON simulation_tax_rule_versions');
(require __DIR__.'/2026_10_03_190000_allow_public_read_of_published_tax_parameters.php')->up();
DB::statement('ALTER TABLE investment_legal_sources DROP CONSTRAINT investment_legal_source_fiscal_acceptance_check');
DB::statement('ALTER TABLE investment_legal_sources DROP COLUMN fiscal_acceptance');
}
};
+38
View File
@@ -0,0 +1,38 @@
{
"rule_code": "IVA-GERAL-BASE-LIQUIDA",
"rule_version": 1,
"component": "IVA",
"scope_key": "mz.internal-general-net.mzn",
"valid_from": "2023-01-01",
"valid_until": null,
"source_sha256": "ea1b0dc6f5cf6e595f6d9a19e7042f398999e0087caef1785f6e8fac571b5f53",
"rule_definition": {
"language": "investmoz.tax.v1",
"nodes": {
"base": {"op": "input", "key": "base_tributavel"},
"rate": {"op": "parameter", "key": "iva_general_rate"},
"hundred": {"op": "literal", "value": "100"},
"product": {"op": "multiply", "args": ["base", "rate"]},
"intermediate": {"op": "divide", "args": ["product", "hundred"]}
},
"result": "intermediate"
},
"parameters": {
"IVA_GENERAL_RATE": {
"value": "16",
"unit": "percent",
"conditions": {"operation": "internal_general_net", "currency": "MZN"}
}
},
"rounding_policy": {
"status": "not_applied",
"precision": "exact_decimal",
"method": "none",
"ties": "not_applicable_without_rounding",
"aggregation": "single_declared_taxable_base",
"purpose": "operation_tax_not_payment",
"evidence": "Orientação expressa Tempo: calcular este lote IVA sem arredondamento automático; ficha v1.2",
"coverage_start": "2026-01-01",
"coverage_end": "2026-12-31"
}
}
+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,11 @@ 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.
Conferência de integração de 22/10/2026 (requisitos documentais MASTER-9f6dd1d9-0001/0003 e reutilização MASTER-02f44baf-0002): 8286d750 servido, acesso real de Nala restituído removendo dependência indevida da reserva técnica do host; lista nominal, MFA e âmbito preservados. Ver secção «Entrada única — integração e conferência real» do manual. Não encerrar o fluxo integral das oportunidades: ausência de Publicador activo e publicação promocional ainda por executor privado são saldos explícitos; não condicionam os acessos já aprovados. Não foram criadas contas, matrizes ou papéis adicionais.
### Divulgação promocional corrente — QA, 22/10/2026
Evidência complementar aos requisitos existentes MASTER-9f6dd1d9-0001/0003 (gestão documental), MASTER-02f44baf-0002 (reutilização) e MASTER-6e51cfcd-0001 (oportunidades): secção «Divulgação promocional pela equipa — implementação QA» do manual e `OpportunityPromotionTest`. Por decisão expressa da Tempo, a divulgação de resumos Em preparação tem prévia, confirmação, actualização, retirada e auditoria na interface, independente da aprovação de execução e do workflow de oportunidade completa. Reutilizados os perfis operacionais e o âmbito existentes; zero novos papéis/aprovadores. Removida a dependência do executor privado para esta operação corrente em QA. Origem dos controlos de versão/token: escolha técnica de integridade. Não alterar os critérios originais nem dar por concluídas a instalação no activo, a disponibilidade das contas ou as variantes não demonstradas. Mario conserva a designação aprovada e pode ser seleccionado por nome quando a conta/requisitos estiverem disponíveis. Geometrias excluídas.
@@ -76,3 +76,5 @@
"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\portal-documents\5c860c48-e8cb-4d8d-ab9b-2e3ea7036ce2";"5c860c48-e8cb-4d8d-ab9b-2e3ea7036ce2";"1834284";"EA1B0DC6F5CF6E595F6D9A19E7042F398999E0087CAEF1785F6E8FAC571B5F53";"5c860c48-e8cb-4d8d-ab9b-2e3ea7036ce2";"";"Private stored copy already represented by its document record";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\portal-documents\64c46765-96b9-4f43-afc6-f5b730c010c4";"64c46765-96b9-4f43-afc6-f5b730c010c4";"440018";"CCF2A8DEECA820F2DD076B4B2141B1EFE3F6A806568C3A587CA59A8E92F4C251";"64c46765-96b9-4f43-afc6-f5b730c010c4";"";"Private stored copy already represented by its document record";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
"Cópia privada ou derivado";"C:\laragon\www\portal-investidor\storage\app\private\portal-documents\b0ea969a-30ec-477b-8b8b-b7af82a11bfe";"b0ea969a-30ec-477b-8b8b-b7af82a11bfe";"67067";"5C69B6F189A1D3F5307E0CF7DD8224554705158A682637D3105AFAD615DB26E2";"b0ea969a-30ec-477b-8b8b-b7af82a11bfe";"C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928\original.pdf + C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928-v2\original.pdf + C:\laragon\www\portal-investidor\storage\app\private\portal-documents\b0ea969a-30ec-477b-8b8b-b7af82a11bfe";"Private stored copy already represented by its document record";"Acessível em armazenamento privado";"Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão."
"Formulario fornecido Tempo";"\\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\portal do investidor\Notas\Novo Modelo A.docx";"Novo Modelo A.docx";"532008";"62C198E90D8C6792F7D5CCEFC6386DF2E4B6089573067D3A6E1EAE7FC2B9572B";"IVA Modelo A - exemplar Tempo";"";"Original privado na biblioteca QA; sem publicacao ou activacao";"Acessivel / importado QA / privado / scan limpo";"Imagem incorporada lida visualmente; edicao/vigencia nao certificada. Correspondencia na reconciliacao Modelo A, AUD-SIM-INT02. Variante de bytes preservada, nao duplicado por nome."
"Formulario fornecido Tempo";"\\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\portal do investidor\Notas\Novo Modelo A v1.docx";"Novo Modelo A v1.docx";"532100";"63D45FA80DB17A60B43619D55A25813229D670D14C758273F86316285DB8E02B";"IVA Modelo A - exemplar Tempo";"";"Original privado na biblioteca QA; sem publicacao ou activacao";"Acessivel / importado QA / privado / scan limpo";"Imagem incorporada lida visualmente; edicao/vigencia nao certificada. Correspondencia na reconciliacao Modelo A, AUD-SIM-INT02. Variante de bytes preservada, nao duplicado por nome."
1 RecordType Path FileName SizeBytes SHA256 TitleOrSubject DuplicateGroup Disposition Status Notes
76 Cópia privada ou derivado C:\laragon\www\portal-investidor\storage\app\private\portal-documents\5c860c48-e8cb-4d8d-ab9b-2e3ea7036ce2 5c860c48-e8cb-4d8d-ab9b-2e3ea7036ce2 1834284 EA1B0DC6F5CF6E595F6D9A19E7042F398999E0087CAEF1785F6E8FAC571B5F53 5c860c48-e8cb-4d8d-ab9b-2e3ea7036ce2 Private stored copy already represented by its document record Acessível em armazenamento privado Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão.
77 Cópia privada ou derivado C:\laragon\www\portal-investidor\storage\app\private\portal-documents\64c46765-96b9-4f43-afc6-f5b730c010c4 64c46765-96b9-4f43-afc6-f5b730c010c4 440018 CCF2A8DEECA820F2DD076B4B2141B1EFE3F6A806568C3A587CA59A8E92F4C251 64c46765-96b9-4f43-afc6-f5b730c010c4 Private stored copy already represented by its document record Acessível em armazenamento privado Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão.
78 Cópia privada ou derivado C:\laragon\www\portal-investidor\storage\app\private\portal-documents\b0ea969a-30ec-477b-8b8b-b7af82a11bfe b0ea969a-30ec-477b-8b8b-b7af82a11bfe 67067 5C69B6F189A1D3F5307E0CF7DD8224554705158A682637D3105AFAD615DB26E2 b0ea969a-30ec-477b-8b8b-b7af82a11bfe C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928\original.pdf + C:\laragon\www\portal-investidor\storage\app\private\geo-ine-review-20260928-v2\original.pdf + C:\laragon\www\portal-investidor\storage\app\private\portal-documents\b0ea969a-30ec-477b-8b8b-b7af82a11bfe Private stored copy already represented by its document record Acessível em armazenamento privado Inventariado por caminho, tamanho e SHA-256; não publicado nem aberto nesta revisão.
79 Formulario fornecido Tempo \\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\portal do investidor\Notas\Novo Modelo A.docx Novo Modelo A.docx 532008 62C198E90D8C6792F7D5CCEFC6386DF2E4B6089573067D3A6E1EAE7FC2B9572B IVA Modelo A - exemplar Tempo Original privado na biblioteca QA; sem publicacao ou activacao Acessivel / importado QA / privado / scan limpo Imagem incorporada lida visualmente; edicao/vigencia nao certificada. Correspondencia na reconciliacao Modelo A, AUD-SIM-INT02. Variante de bytes preservada, nao duplicado por nome.
80 Formulario fornecido Tempo \\Mac\Home\Documents\TEMPO\Portal Tempo\Servicos Tempo\portal do investidor\Notas\Novo Modelo A v1.docx Novo Modelo A v1.docx 532100 63D45FA80DB17A60B43619D55A25813229D670D14C758273F86316285DB8E02B IVA Modelo A - exemplar Tempo Original privado na biblioteca QA; sem publicacao ou activacao Acessivel / importado QA / privado / scan limpo Imagem incorporada lida visualmente; edicao/vigencia nao certificada. Correspondencia na reconciliacao Modelo A, AUD-SIM-INT02. Variante de bytes preservada, nao duplicado por nome.
@@ -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/`
@@ -0,0 +1,100 @@
# IVA-GERAL-BASE-LIQUIDA — ficha documental v1.2
21/10/2026. AUD-SIM-INT02 na matriz existente. Proposta inicial `0fafd348`; v1 em `325d58b6`, v1.1 em `37085b40`. Essas versões ficam preservadas em Git. Esta versão aplica a orientação posterior expressa da Tempo: **prosseguir neste lote IVA com precisão decimal exacta, sem introduzir arredondamento automático; separar valor calculado de apresentação monetária**. Retira a falta de uma política genérica de arredondamento como condição bloqueante do cálculo. Não estende a decisão a todos os impostos.
**Estado:** cadastro e cálculo experimental em QA, regra em rascunho, sem publicação/activação ou integração em `main`. Não equivale a conclusão do simulador fiscal, emissão de factura, declaração ou determinação de saldo a pagar. A Tempo declarou não ter conferido a ficha local nem validado juridicamente as conclusões transcritas; não se lhe atribui essa validação.
## Âmbito e contrato
Um único percurso: base tributável líquida já identificada × taxa geral /100, em MZN explícito, para operação interna abrangida pela taxa geral. Janela demonstrada do lote:01/01/2026–31/12/2026, delimitação técnica que não cria caducidade legal em31/12. Não decide automaticamente incidência, sujeito passivo, localização, isenção ou regime especial. Contexto declarado é rotulado como tal; uma rubrica orçamental não se transforma automaticamente em base fiscal.
Excluídos: extracção de preço com IVA incluído, taxas reduzidas, isenções/listas, importações, digital/não residentes, dedução/crédito/reembolso, incentivos e outros impostos. Permanecem no inventário documental existente, sem omissão nem alegação de cobertura deste lote.
| Campo | Estado actual |
|---|---|
| Regra | `IVA-GERAL-BASE-LIQUIDA`, versão1, estado `draft`; cadastro explícito apenas em QA. |
| Taxa / unidade | `16` / `percent`, vínculo Lei22/2022, art.17(1), exemplar/hash abaixo. |
| Vigência cadastrada | Desde01/01/2023, sem termo artificial; cobertura ensaiada2026 separada da vigência. |
| Entradas | Base decimal como texto, MZN explícito, data de exigibilidade declarada, contexto `internal_general_net`. Zero explícito é válido; ausência não é zero. |
| Valor calculado | Decimal exacto obtido pelo avaliador racional existente; sem conversão para float, truncagem ou redução para duas casas. |
| Apresentação | Valor decimal preservado como texto e moeda separados. Duas casas da unidade monetária não impõem arredondamento ao cálculo. Ecrã/CSV/dossier reproduzem o valor guardado. |
| Arredondamento | `not_applied`; nenhum nó `round` neste percurso. Não se inventa precisão, empate ou método fiscal. |
| Indeterminação efectiva | Fonte/versão/contexto incompatível, parâmetro ausente ou pedido fora da cobertura demonstrada. A falta de arredondamento não torna indeterminado o resultado deste percurso. |
| Limites do resultado | Cálculo QA sobre regra em rascunho e enquadramento declarado; não é obrigação validada nem saldo a pagar. |
## Fontes reutilizadas e disposições documentadas
Fontes oficiais aceites pela decisão Tempo de08/10/2026; sem nova certificação genérica de proveniência. A identificação de âmbito, período e dispositivo é aplicação da fonte aceite. Reutilizar [análise primária](primary-text-review-2025-2026.md), [registo de fontes](official-source-register.md) e [reforma fiscal](fiscal-reform-2026-review.md). A aceitação da fonte não activa regra nem valida juridicamente uma operação concreta.
| Exemplar | Identificação e integridade |
|---|---|
| [Lei13/2016 / CIVA republicado](source-archive/Lei-13-2016-CIVA-BR-156-10-suplemento.pdf) | BR156,10.º suplemento,30/12/2016;32páginas,2.127.554bytes; SHA256 `B90DC7C14ECEA2E378C4A62E8E5A47D54A5644C457BAEB8B8053B92FD2A5710B`. Leitura visual dirigida PDF6–8,13–14,17–18; entrada em vigor no art.4/PDF5. Não se afirma análise integral de anexos. |
| [Lei22/2022 republicada](source-archive/Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf) | BR250,2.º suplemento;18páginas,1.834.284bytes; SHA256 `EA1B0DC6F5CF6E595F6D9A19E7042F398999E0087CAEF1785F6E8FAC571B5F53`. Art.17(1),PDF3/BR2252-(27), taxa16%; art.6 da Lei,PDF4, eficácia01/01/2023. |
| [BR248, suplemento1/2025](source-archive/BR-248-I-Suplemento-1-2025.pdf) |21páginas,440.018bytes; SHA256 `CCF2A8DEECA820F2DD076B4B2141B1EFE3F6A806568C3A587CA59A8E92F4C251`. Lei10/2025,PDF12–15; eficácia01/01/2026. |
| [BR16/2008 / Decreto7/2008](source-archive/BR-16-I-Serie-2008-04-16.pdf) |16/04/2008;28páginas,2.267.865bytes; SHA256 `FC373FF5637EDF75EC6A9595DEC8763A8503D4EE786E3B7D8F1C742EAE9D80EE`. Regulamento IVA,PDF1–2; página2 conferida visualmente. O BR inclui outros impostos, que não são fundamento deste lote. |
| [BR89/2017 / rectificação CIVA](source-archive/BR-89-I-Serie-2017-06-08.pdf) | Aviso relativo a art.6(5)c/d,14(2)c e anexos de isenção; análise anterior reutilizada. Não encerra consolidação das listas de isenção. |
| Disposição exacta | Conteúdo documentado / consequência |
|---|---|
| CIVA art.1(1)(a), PDF6, BR1426-(264) | Transmissões de bens e prestações de serviços onerosas no território nacional, nos termos do art.6, por sujeito passivo nessa qualidade. Exigir identificação desse contexto; importações ficam fora. |
| Art.2(1)(a), PDF6 | Qualidade do sujeito passivo ligada às condições de residência/estabelecimento e actividade independente previstas. Perfil de investidor não prova sujeição. |
| Arts.3(1),4(1), PDF6–7 | Transferência onerosa de bens corpóreos e noção geral de serviço. Casos assimilados/especiais não entram implicitamente na fórmula. |
| Art.6(1), PDF7, BR1426-(265) | Para bens, localização no início do transporte/expedição ou quando postos à disposição, sem transporte. Serviços seguem art.6(3) e excepções subsequentes; não basta localização do prestador para todos os serviços. |
| Art.15(1), PDF13, BR1426-(271) | Base geral: contraprestação obtida ou a obter, sem prejuízo das bases especiais do n.º2. Não equivale a qualquer receita de planeamento. |
| Art.15(5)(a)/(b), PDF14, BR1426-(272) | Inclusão de outros impostos/direitos/taxas, excepto o próprio IVA, e despesas acessórias debitadas, nas condições previstas. |
| Art.15(6)(a)–(d), PDF14 | Exclusões condicionadas: indemnizações judiciais indicadas, descontos/abatimentos/bónus, pagamentos em nome/conta de terceiros registados adequadamente e embalagens retornáveis. Não classificar só pelo nome da rubrica. |
| Art.17(2), PDF14; art.7(1)(a)/(b), PDF8 | Taxa vigente na exigibilidade; facto gerador/exigibilidade diferem por operação e condições. A criação da simulação não substitui a data fiscal. Adiantamentos e prestações continuadas requerem enquadramento próprio. |
| Art.27(5)(c)/(d), PDF17, BR1426-(275) | Separação do preço líquido/elementos da base, taxa e montante do imposto. Lei10/2025 preserva essas alíneas com «[…]», alterando o n.º10. |
| Regulamento, arts.4/5, Decreto7/2008,PDF1–2 | Remete matéria colectável para arts.15/16 e apuramento devido para arts.18–22 do Código. IVA da operação não equivale ao saldo após deduções. |
Cadeia temporal documentada: Lei13/2016 efectiva em01/01/2017; a taxa histórica17% do exemplar não é usada para2026. Lei22/2022 altera art.17(1) para16% desde01/01/2023 e acrescenta art.15(10) para taxa reduzida, mantendo os números gerais consultados. Lei10/2025, efectiva01/01/2026, não lista arts.15/17 entre os alterados. Esta conclusão documental não é atribuída como validação jurídica ao utilizador e não certifica todos os regimes/excepções.
## Precisão do cálculo e exigências contextuais
Art.34 do CIVA, Lei10/2025/PDF14, regula extracção de base de valores com imposto incluído, com divisor116 e arredondamento para unidade mais próxima. Art.9(1)(b) do regulamento/Decreto7/2008,PDF2/p.100, trata do valor do meio de pagamento por cheque ou vale de correio e arredondamento por excesso para meticais nesse exemplar histórico. **Nenhum é aplicado como regra geral de arredondamento deste cálculo sobre base líquida.** A vigência consolidada e exigências de formatos de pagamento/declaração serão determinadas quando esses consumidores forem abrangidos.
A [comunicação oficial de30/09/2026](https://portaldogoverno.gov.mz/governo-introduz-alteracoes-nos-regulamentos-fiscais-para-reforcar-eficiencia-tributaria/) anuncia alterações regulamentares; não fornece articulado publicado e eficácia por disposição. A pesquisa dirigida em fontes oficiais não identificou uma exigência geral que obrigue a arredondar este percurso. Ausência de correspondência não prova inexistência de instruções específicas. Não há questão pendente à Tempo como pré-condição deste cálculo. Se um futuro formato exigir precisão, apresentar uma única questão contextual, identificando o formato, precisão, método, empates e agregação; não generalizar a outros cálculos.
Nota de rastreabilidade, sem reabrir pontos: o exemplar BR54 de06/04/2017 consultado anteriormente não continha a alteração regulamentar procurada. O [catálogo INM](https://www.inm.gov.mz/pt-br/produtos?page=124) identifica Decreto8/2017 no BR50 de30/03/2017. O articulado e alterações pertinentes continuam a ser correspondências documentais a completar quando necessárias ao consumidor; não são bloqueio genérico do cálculo exacto deste lote.
## Exemplos e provas dirigidas
Pressupostos explícitos: operação interna onerosa à taxa geral, base já apurada, sem isenção/redução/base especial, data coberta2026 e MZN. Não são classificações jurídicas de operações reais.
| Base | IVA calculado exacto | Apresentação / significado |
|---|---|---|
|100|16|`16` e moeda `MZN`, separados; não é saldo a pagar. |
|0,03|0,0048|Conservar todas as casas no armazenamento, ecrã e exportação. Não substituir por0,00. |
|0|0|Zero explicitamente declarado, distinto de entrada ausente. |
|9007199254740993,0003|1441151880758558,880048|Preservar sem converter para float JavaScript/PHP. |
|100 + transporte10 − desconto5|16,8 sobre base105|Apenas se verificadas as condições de inclusão/exclusão do art.15. |
Os testes do novo consumidor cobrem registo repetido sem duplicação, rascunho excluído da API publicada, resultado exacto, snapshot/relatório conservados após edição da regra e recuperação noutra sessão de autenticação, acesso negado a outra conta, moeda obrigatória, incompatibilidade da fonte, cobertura e recusa fora de QA. A nova sessão provada é API/autenticação, não alegação de novo ensaio browser. A conta PostgreSQL QA ultrapassa RLS: recusas demonstram a política da aplicação, não nova prova de RLS.
## Implementação, limites e integração
Reutilizados `SimulationTaxRuleGovernance`, `SimulationTaxParameterResolver`, `SimulationTaxRuleExpression` e o armazenamento existente de simulações, fontes, histórico e relatórios. Não há motor novo, migration ou alteração da base activa. `fiscal:qa-register-iva` exige alvo QA isolado, editor e referência de fonte já existente; confere hash e cadastra apenas rascunho, de forma idempotente. Não corre em deploy/importação. O artefacto versionado é `database/qa/iva-general-net-v1.json`.
Pré-visualização administrativa `POST /api/v2/investment/admin/tax-rules/{id}/qa-preview`, com `Idempotency-Key`, exige os controlos administrativos existentes e alvo QA isolado. Não muda a API de parâmetros publicados para aceitar rascunhos. O snapshot congela regra, fonte/versão/hash, contexto declarado, data, passos racionais, decimal calculado, apresentação sem arredondamento e limitações. Alteração da regra não reescreve o histórico. Uma nova entrada produz nova execução; um snapshot fiscal não é convertido por edição numa simulação de planeamento.
O cadastro persistido usa clone `simulator_fiscal_delta_20261021_test` e referência/actores sintéticos explicitamente identificados; não representa revisão jurídica por uma pessoa real nem atribuição persistente de papéis da Tempo. O recibo privado regista IDs, estado draft, zero regras publicadas e hash do relatório. A conta técnica e os ficheiros de prova privados não são material de deploy.
Planeamento personalizado mantém identificação própria. O resultado deste lote é apresentado como cálculo QA com regra em rascunho, não estimativa percentual introduzida pelo utilizador nem regra fiscal activada. As provas aceites de13f68f1b/valores/CSV/paginação não são repetidas. A entrega anterior permanece independente e concluída; não integra esta branch automaticamente.
## Estimativa e revisão
As15–26h anteriores eram condicionais e incluíam o trabalho de cadastro e integração que agora avançou em QA. O esclarecimento de uma política genérica de arredondamento deixa de integrar o caminho crítico deste lote. O saldo deve usar os resultados técnicos registados na adenda do manual existente, distinguindo trabalho já executado, revisão e qualquer consumidor futuro de pagamento/declaração. Não são horas medidas, promessa de activação nem estimativa do simulador fiscal integral. Publicação, activação e integração em `main` continuam separadas e não foram executadas.
## Revisão consolidada de83d2f97d
**Controlo de manutenção corrigido emQA,21/10/2026:** por decisão expressa da Tempo, a mesma administração autorizada pode cadastrar a fonte e concluir as etapas da regra, comMFA, responsável, evidência e histórico. A obrigação técnica de quatro pessoas não tinha fundamento aprovado identificado para este cadastro fiscal. Foi retirada apenas deste fluxo; o novo fundamento de aceitação da fonte oficial não é parecer jurídico nem revisão independente. Âmbito, exclusões, pressupostos e identificação do resultado como estimativa sobre base declarada mantêm-se. O manual consolidado contém a origem da condição, as6provas dirigidas/86asserções e o procedimento actualizado; não se reensaiou o cálculo nem se publicou ou activou a regra real nesta correcçãoQA.
**Actualização técnica de21/10/2026 — delta2ced7b79 implementado em QA:** o consumidor por versão publicada está ligado ao simulador normal, com activação desligada por omissão, selecção da regra no servidor e gravação/recuperação/relatório pelo histórico existente. O mesmo manual regista a demonstração pelo Edge, indeterminação sem declaração de âmbito, repetição sem duplicação, suspensão antes do processamento da fila, conclusão com versão congelada e recusa de novas execuções. Oito testes dirigidos/75asserções e dois testes frontend novos passaram, além deTypeScript/build/Pint. Mantêm-se integralmente o âmbito2026, exclusões, fontes e precisão desta ficha; não se repete a fundamentação jurídica nem se lhe atribui validação da Tempo. O procedimento único foi actualizado para integração, publicação da versão real e activação, decisões ainda não executadas na homologação. A fixture publicada exclusivamente no clone QA foi suspensa após o ensaio; os históricos foram preservados. As notas abaixo sobre ausência do consumidor descrevem o estado anterior à implementação deste delta.
**Disponibilização no simulador existente:** a proposta única «Consumidor IVA no simulador da homologação», no mesmo manual, delimita o código ainda necessário e as três operações distintas: integração com consumidor desligado, publicação da versão pela governação existente e activação da ligação à versão autorizada. O percurso QA aceite não equivale a esse consumidor público. Mantêm-se âmbito2026, exclusões, precisão exacta e históricos; suspensão impede novas admissões e preserva execuções já admitidas. Esta proposta não executa nenhuma das três operações nem amplia a cobertura fiscal.
O [mesmo manual de rastreabilidade](../unification/traceability-and-manual.md) consolida âmbito/período, operações excluídas/indeterminadas e os procedimentos concretos de integração, publicação, activação e desactivação, preservando históricos. Identifica também que83d2f97d não contém um consumidor fiscal público: a publicação torna parâmetros elegíveis na API existente, mas não transforma o percurso QA em cálculo público activado.
A única lacuna adicional encontrada foi o percurso completo pela interface. Acrescentou-se a páginaQA protegida `/management/investment/tax-rules/{id}/qa-review`, ligada às APIs existentes de cálculo/gravação, recuperação e relatório imutável. **Exemplo observado no Edge:** base1000,03MZN e exigibilidade declarada21/10/2026 → IVA calculado160,0048 → execução`fa6d17f2-5f47-4cb6-b8cb-40850c12505f` gravada → recarga da página → recuperação do mesmoID no histórico → relatório`d13e2ad5-3ba8-4d01-9d30-5815496da774`, mesmo valor/contexto/proveniência e hash igual ao resultado guardado. Uma execução persistida; regra continua emdraft, zero publicadas. Não prova aplicabilidade fiscal a qualquer operação nem saldo a pagar.
A ferramenta perdeu inicialmente a ligação ao navegador e permaneceu sem inventário apesar das reactivações; essa limitação ficou registada, sem a atribuir ao simulador. A ligação foi depois restabelecida e concluiu-se apenas a parte não observada, com autenticação/segundo factor normais, sem contornar guardas. As provas aceites de83d2f97d são reutilizadas;1teste novo de rota/17asserções,TypeScript/build e a observação browser cobrem o delta. Não se atribui ao utilizador conferência local ou validação jurídica. Não há nova preparação genérica identificada para este recorte; integração e activação continuam separadas.
@@ -0,0 +1,281 @@
{
"base": "59ab4fe95635",
"branch": "codex/simulator-integrated-review",
"scope": "QA only; no main merge or remote change",
"source_files": [
{
"path": "config/app.php",
"sha256": "a01f552093570f8744f7b874392824c15b5a6b49d61759273a658e8660a1d240"
},
{
"path": "docs/investmoz/unification/traceability-and-manual.md",
"sha256": "347c340bcc4f5adc7965bd66cef1f357d98b6c271fdcd35206a3a086e59ad34d"
},
{
"path": "docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv",
"sha256": "a3473a391634c32fa8c53ecb2528b4de08f145ec0284b50f3bbf89aa97e5cf24"
},
{
"path": "docs/investmoz/v2/auditoria-portal/backlog.md",
"sha256": "1294586bfd452e6430aa1fcb71a512cbf64177e529a416ac28dfab954839664b"
},
{
"path": "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv",
"sha256": "f57861a36e310a892d55a16be6d06fecd48763defe11011036df8a3a33a57d6d"
},
{
"path": "lang/pt.json",
"sha256": "c21fa0b63a3729e001f7d4ba891a23890bcfcfd718137d99a7c6c18d3be1a235"
},
{
"path": "lang/pt/auth.php",
"sha256": "d32cc31b75d4056ef561baf501d9cbf71ec136c7f63bef635c0c3e31b9a2357d"
},
{
"path": "lang/pt/validation.php",
"sha256": "bb95fe0f683cd6d8a6d0475dc322c71bce58197b952220e70ac74f5e97cdcf60"
},
{
"path": "resources/js/components/appearance-tabs.tsx",
"sha256": "d53f48b0883713bf786fdf4b7877e03638bcf2e707b64ea6b8559501bfc8c4c7"
},
{
"path": "resources/js/components/delete-user.tsx",
"sha256": "2f94b4dc6edf20bf9e00940d4d7e7b5da31bdf560f26b84efd65672bcee585b8"
},
{
"path": "resources/js/components/manage-passkeys.tsx",
"sha256": "8f3e8970d3b82deba721ac27e57718e2e7dcecc67b1e78cb6404c475c9f9c62e"
},
{
"path": "resources/js/components/manage-two-factor.tsx",
"sha256": "35fea615cf3a0454448057b0ef3137e24c21df4de30a58fce04cb847eb89eba1"
},
{
"path": "resources/js/components/passkey-item.tsx",
"sha256": "a307c5ef209e24f3f913f77e355a4d04a1433f59f520ad45ad40112111ab4677"
},
{
"path": "resources/js/components/passkey-register.tsx",
"sha256": "e7cb9d853eb943fd0899bc8530378eddbf941f9334a77bcbb7693d2e29d99470"
},
{
"path": "resources/js/components/passkey-verify.tsx",
"sha256": "6db387920437ab39cd734a2af9bec7e64ae6469c0f1b368d536b275e57cbea37"
},
{
"path": "resources/js/components/simulation-fiscal-status.tsx",
"sha256": "540dc6addbec0be79114526f7d27f53942291b0c7be05a2608a515498970d3e0"
},
{
"path": "resources/js/components/simulation-form.tsx",
"sha256": "1b2d3a917e354eef7ccdb47201fb7e515ea646d45099beb9b15798c80e1dd2db"
},
{
"path": "resources/js/components/two-factor-recovery-codes.tsx",
"sha256": "9384ae3987a58ae89018f2a997dec7faebec4f51f516ebbdc2b337c8e757f00f"
},
{
"path": "resources/js/components/two-factor-setup-modal.tsx",
"sha256": "8586833e10310dbca8a097df80b86f40638f874170b7668fa476f0d7017a311b"
},
{
"path": "resources/js/components/ui/dialog.tsx",
"sha256": "abeb6df2254f0bf5f73de1c9b5aeb4e667afe8ac0319e8b9aa099a612cc70b8d"
},
{
"path": "resources/js/components/ui/sheet.tsx",
"sha256": "a525cc98beaf974e0dfe6cda1c5da1101798b6843ea355a758b3c5df5235f506"
},
{
"path": "resources/js/components/user-menu-content.tsx",
"sha256": "9cb79c2a999bb363bd9d4fc1869b672c474b088eff23fca6e9f588ecae00d3ce"
},
{
"path": "resources/js/hooks/use-two-factor-auth.ts",
"sha256": "16d9434379531f1994096e4d1fcfd778b2c829602cce5731d602a25ea9c0073f"
},
{
"path": "resources/js/layouts/settings/layout.tsx",
"sha256": "78ea66c1eb6cad907e23de9c81a4d309a127f251355f04771f72eb5689609a4e"
},
{
"path": "resources/js/lib/project-dossier-document.ts",
"sha256": "d0a66ca759e8710c1a3900bcd27cc7f0506587c782ab798c49703396c1fb31d6"
},
{
"path": "resources/js/lib/project-dossier-report.ts",
"sha256": "6c5f52362a6218048f321b41211bfd328fa1e19ea4ab45fd1424932130482c8d"
},
{
"path": "resources/js/lib/simulation-form.ts",
"sha256": "40df1a9b6939b1910d12045835e981d6e47d4c1d534775631ac2f6b655a5e2bd"
},
{
"path": "resources/js/pages/auth/forgot-password.tsx",
"sha256": "6cdedca491d5a05dad0083fb637cd618d0b290bd6b48b98e7c6163dc0d740166"
},
{
"path": "resources/js/pages/auth/login.tsx",
"sha256": "8468308dacd26bd94ee41683684dcb7719386a1a53196ed90496ca406b1b94da"
},
{
"path": "resources/js/pages/auth/reset-password.tsx",
"sha256": "f3561726ae5d7ee682d968a295bbf60ec9f8cbabba1721c33f48bafd80d1b20a"
},
{
"path": "resources/js/pages/auth/two-factor-challenge.tsx",
"sha256": "0fd14dff2f0a572f4b99905d0fb152baa476eaa55a221998b67da85fa4787610"
},
{
"path": "resources/js/pages/auth/verify-email.tsx",
"sha256": "ffc3220d0ee6cc243e8e08ea46f8b1637794626fa45d7aeaa77f54203692864d"
},
{
"path": "resources/js/pages/portal/services.tsx",
"sha256": "9369add7c012c1e8a9e217b46be55e2927382bb8f18b90973d3b1f4199442d9b"
},
{
"path": "resources/js/pages/settings/appearance.tsx",
"sha256": "638367104837b028aeeb49b4cad1d2c9a44a92d1b9aebe71c31995605e0479b4"
},
{
"path": "resources/js/pages/settings/profile.tsx",
"sha256": "c599b1ca523ece69c3f5d3fe480a9f2ce4c323e97afb42095f61f2ddb7d4ba7b"
},
{
"path": "resources/js/pages/settings/security.tsx",
"sha256": "39264620329657af975f990a50d657c42186ead57458cd83040aa07f9cd78444"
},
{
"path": "tests/frontend/project-dossier-report.test.mjs",
"sha256": "fcdcabfc659f2f4ca0d5a51e592719d53d51420271c7cbe350176b48cef3fc97"
},
{
"path": "tests/frontend/simulation-form.test.mjs",
"sha256": "294a1b996d8131771ebaca85d2ff0e44bb6afd62e460073b7535486f13ae3a6b"
}
],
"evidence": [
{
"path": "outputs/simulator-integrated-review/effective-parameter-inventory.json",
"sha256": "aeec1251ef5dbbf7ae547af9f1ebcd2575aaa6c251ef65b37c6c31b8cf371acc"
},
{
"path": "outputs/simulator-integrated-review/independent-controls-proof.json",
"sha256": "47ad49b2136fd74c801b7a5d057c891732d820fb6a5002ccf08c71a2ffabf575"
},
{
"path": "outputs/simulator-integrated-review/visible-language-inventory.json",
"sha256": "5f1f50236ae492c8428a9528af350900e4f2c44c387fe8f23ebce0e4f1b14b3a"
},
{
"path": "outputs/simulator-integrated-review/dynamic-language-inventory.json",
"sha256": "fe3246f570c333c2c59e9fe2acb0ec01078a158a46e53d37087ec59d419578a5"
},
{
"path": "outputs/simulator-integrated-review/portuguese-validation-proof.json",
"sha256": "b7a454cdc1e934faada40b683ff2b7a6edce73aebfd22ba34e5da17a1854b0a1"
},
{
"path": "outputs/simulator-integrated-review/http-reports-proof.json",
"sha256": "9d37fbee89f7dc556ff9496f6b0980e1c9626790eb50a41a499cf23e2f27fe5f"
},
{
"path": "outputs/simulator-integrated-review/date-field-error.jpg",
"sha256": "6a792c09c61857a0a064a56d3d61b47fc2adc1d6c8bcf627161f6fa77c7b099d"
},
{
"path": "outputs/simulator-integrated-review/stale-result.jpg",
"sha256": "84b37c41ba47d44f52979e83dbc3426184f5cbfee6d38e7c7d4caa3be225320b"
},
{
"path": "outputs/simulator-integrated-review/tax-lookup-error.jpg",
"sha256": "d3f366690fa3acbda45bc021ade90dc003cea7dc0517c4d2932935548dbb40d0"
},
{
"path": "outputs/simulator-integrated-review/positivo.docx",
"sha256": "61b1ea80476922aa1be8c29f9a757b389c3ecc8f0e75f414a29eddd5b37ecefd"
},
{
"path": "outputs/simulator-integrated-review/positivo-model.json",
"sha256": "fc8df79e7ce81e4a475b99f5ea07763c1cee140bc9dcb24550ed26ce412e4e0d"
},
{
"path": "outputs/simulator-integrated-review/checked-positivo/positivo.pdf",
"sha256": "60d655b33fcc27dbbaae40a0739c1ebeb9b366df9b00621a6536feecd60da452"
},
{
"path": "outputs/simulator-integrated-review/negativo.docx",
"sha256": "f266cff1a1f9f63449703cc5ef51a7f4d2d7bf94230be70ae382bc89af8139bd"
},
{
"path": "outputs/simulator-integrated-review/negativo-model.json",
"sha256": "6aff37f66695e1d2a7441bf7b361f7980bc10f6951f915713f55a01e5b00da48"
},
{
"path": "outputs/simulator-integrated-review/checked-negativo/negativo.pdf",
"sha256": "3a2577194bc6c9e7234b650e69f3795f6cef6aef41d62658839c0790989a8f5c"
},
{
"path": "outputs/simulator-integrated-review/nulo.docx",
"sha256": "efb83aa9044e35515880e40641d6bea52c6ea6f81289c3abc9d3bdbc76a9ba26"
},
{
"path": "outputs/simulator-integrated-review/nulo-model.json",
"sha256": "94bbc6cf72cc613ebcd5ab9e9c074220b2f50dfb5b03959e92a7c84032a86750"
},
{
"path": "outputs/simulator-integrated-review/checked-nulo/nulo.pdf",
"sha256": "c93e989a7af230e56f5d01766a4e386161e6fbbfa99341d5bfa0569bafe9e18e"
},
{
"path": "outputs/simulator-integrated-review/incompleto.docx",
"sha256": "026f0ff1877ed5f23359d220e3223a319c213e742ff9704d81812fc2ed3ae20e"
},
{
"path": "outputs/simulator-integrated-review/incompleto-model.json",
"sha256": "748b9cecde2acc99d7e2bbfd16188bf9f9ac0ab411ad850ba687c6e1f2e36cf5"
},
{
"path": "outputs/simulator-integrated-review/checked-incompleto/incompleto.pdf",
"sha256": "0daac41c80ae131df6d66b7b542353a61a804d02d2e77769b9e5cc2bc9cfbc88"
},
{
"path": "outputs/simulator-integrated-review/completed-calculation.jpg",
"sha256": "7180ff0f536e2e377c2a28b85574ec7c7fefcce5eed28ed08f07628b16870293"
}
],
"checks": {
"targeted_frontend": 34,
"typescript": "passed",
"php_lint": "passed",
"build": "passed",
"reports_pages": {
"positivo": 6,
"negativo": 6,
"nulo": 6,
"incompleto": 5
},
"tax_lookup_retry": "503 then successful lookup; IVA; 2026-01-01 through 2026-12-31; indeterminate parameters",
"arithmetic_ui": "100*16/100*(1-25/100)=12; base change removes result",
"build_manifest_sha256": "af96a5f5682dad7a741d0ee8b302d8dc7f4d08a1806bc000265bd2602e5bfc61",
"terminal_status_ui": "200*16/100*(1-25/100)=24; completed notice visible; awaiting-calculation notice absent"
},
"remaining_hours": [
6,
10
],
"limitations": [
"No published fiscal rule versions in QA; no legal tax validation",
"Authenticated cross-dossier and new-session frontend delta still pending",
"Parameter-version change and additional business-plan/cash-flow controls pending",
"PDF examples are DOCX rendering; native Edge print and final UI download comparison pending",
"Dynamic backend/catalogue messages and directed save/export error checks remain open"
],
"deployment": {
"authorised_now": false,
"migrations": [],
"locale_on_future_deploy": "APP_LOCALE=pt; preserve existing secrets"
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,93 @@
# Reconciliação do IVA com o Novo Modelo A
21/10/2026. Referência AUD-SIM-INT02, sem nova matriz/backlog. Preparação progressiva em QA; não é apuramento completo, submissão ou aceitação pela AT. A componente16% existente continua independente das restantes componentes por cadastrar/desenvolver. A leitura e aceitação documental da Tempo não é apresentada como validação jurídica independente.
## Exemplar e preservação
Exemplar exacto: **Novo Modelo A.docx**, 532008bytes, SHA256 **62c198e90d8c6792f7d5ccefc6386df2e4b6089573067d3a6e1eae7fc2b9572b**. Localizado na pasta de notas do portal indicada no inventário privado desta execução, não substituído por formulário genérico ou pela página da AT. O quadro1–8 é imagem incorporada: word/media/image2.png, SHA2567ae7f02efca96323087d0dfa5f3078b4ae724318edd5b6c4802c863d840bdf66, conferida visualmente sobre fundo branco para tornar legível a transparência. Os blocos seguintes e respectivas instruções foram extraídos do OOXML. Também se conferiram visualmente as três páginas da conversão de leitura em LibreOffice: quadros1–8 na primeira, créditos/quadros9–13 na segunda e apenas o rodapé de distribuição na terceira. A conversão apresenta deslocações de texto; serve para leitura, não é versão corrigida ou reprodução fiel para entrega. Nenhum DOCX original foi alterado, nem se afirmou edição oficial vigente pela simples leitura.
Também inventariado e conservado **Novo Modelo A v1.docx**, 532100bytes, SHA25663d45fa80db17a60b43619d55a25813229d670d14c758273f86316285db8e02b. A mesma imagem é byte a byte igual; a extracção textual difere na segmentação de palavras. São originais distintos, não duplicados por bytes, conservados sem renomear uma revisão como diploma novo. A criação OOXML em21/10/2026 não é data de aprovação ou vigência do formulário.
Biblioteca QA existente, clone simulator_fiscal_delta_20261021_test: recurso33/documento fbfa820d-29d5-4bf8-9f81-534931d80cef (original) e recurso34/documento cf8b799e-44de-4497-b2e3-a5c468f2fe0e (v1). Pré-visualização, hash, conflitos de identidade/bytes, importação privada idempotente, revisão/auditoria e scan reais reutilizados; ambos limpos com o hash analisado igual ao original. **Não publicados**; consulta privada e estado documental não activam cálculo. Actor166 é fixture QA já existente, configurada apenas neste processo de ensaio da administração da biblioteca, sem atribuir função real da Tempo ou alterar permissões persistentes. Nenhuma importação no destino activo. A aceitação oficial da Tempo08/10 é preservada; os estados genéricos do importador não a revogam nem certificam vigência.
## Correspondência dos quadros
| Quadro/bloco no exemplar | Finalidade e dados existentes | Delta/limite |
|---|---|---|
|1 — Tipo de declaração|Inicial/substituição; não corresponde a criar versão de uma projecção|Não simular uma declaração efectiva; futura ligação a declaração anterior separada de versões de planeamento|
|2 — Período/prazo|Meses da projecção disponíveis; não prova de entrega dentro/fora do prazo|Calendário fiscal e entrega efectiva por integrar; nunca marcar submissão pela data da simulação|
|3 — NUIT/sucursal/Área Fiscal|Identificação guardada no dossier quando disponível|Reutilização com confirmação específica; não exigir NUIT para planear nem inferir enquadramento fiscal|
|4 — Nome/designação|Identificação privada existente|Não copiar para documento público; tratar como dado efectivo só se confirmado|
|5 — Actividade/CAE|Actividade/sector existentes; não garantem CAE ou taxa|Correspondência CAE e classificação por operação, sem converter sector em incidência|
|6 — Domicílio fiscal|Localização e contactos do dossier|Domicílio do projecto não prova domicílio fiscal; recolher apenas diferença/confirmar contexto|
|7 — Inexistência de operações|Zero de vendas é conhecido, mas aquisições podem existir|Não assinalar inexistência por vendas zero ou campos vazios; precisa de todas as operações activas/passivas|
|8 — Apuramento|Campos01–18; componente01/02 disponível no âmbito actual|Demais bases, imposto suportado/dedutível e regularizações continuam indeterminados|
|Utilização de créditos anteriores|Campos19/20; o texto remete novamente para quadro8|Registar remissão ambígua; créditos efectivos e comunicados distintos de estimativas/pedidos|
|9 — Substituições|Campo21 e referências17/18 da actual/substituída|Guardar a relação entre declarações efectivas; uma nova simulação não substitui declaração|
|10 — Entrega ao Estado|Campos22–24, meios de pagamento|Sem pagamento ou guia gerada; caixa prevista não prova liquidação|
|11 — Recuperação|Campos25–27, reporte e pedido|Separar crédito, pedido, reconhecimento e recebimento; não gerar reembolso automático|
|12 — Autenticação|Data/nome/assinatura do sujeito passivo|Autenticação do portal não assina nem submete Modelo A|
|13 — Serviços|Entrada/inserção/receita/funcionário/recebedor|Reservado à AT; não preencher ou imitar aceitação administrativa|
## Correspondência campo a campo
Os rótulos de finalidade são explicativos; não corrigem a imagem nem substituem o original. Fórmulas impressas estão mapeadas, não activadas por este registo. Ausência de componente não equivale a zero.
|Campo do Modelo A|Finalidade|Origem dos dados|Cálculo|Fundamento|Já implementado/em falta|
|---|---|---|---|---|---|
|01|Base das operações à taxa geral|Vendas mensais já projectadas, declaradas sem IVA e no âmbito geral|Base líquida do mês|CIVA arts.15 e 17; Lei22/2022; regra publicada existente|Implementado no âmbito declarado|
|02|IVA à taxa geral|Resultado mensal congelado no servidor|Campo01 × taxa da versão publicada /100|CIVA art.17(1); mesma regra/versionamento do consumidor|Implementado; componente do apuramento|
|03|Base de operações específicas à taxa reduzida|Operações separadas e enquadradas; ainda não recolhidas|Base das operações elegíveis|Exemplar e CIVA art.17-A, aditado pela Lei22/2022; enquadramento a cadastrar|Em falta; não assumir zero|
|04|IVA à taxa reduzida|Campo03 e futura versão fundamentada|Campo03 × taxa reduzida aplicável; não activa pelo exemplar|CIVA art.17-A; âmbito/versão temporal a integrar|Em falta; não assumir zero|
|05|Vendas isentas com direito à dedução|Vendas classificadas por operação e fundamento|Soma das bases classificadas; sem taxa presumida|Exemplar; CIVA arts.9–14 e 18–20 a consolidar por operação|Em falta; não assumir zero|
|06|Operações da alínea b), n.º1, art.18, conforme exemplar|Operações classificadas com referência verificada|Soma separada; não inferir do nome do sector|Remissão do exemplar ao art.18(1)(b); cotejo do âmbito necessário|Em falta; não assumir zero|
|07|Isentas sem direito à dedução|Vendas classificadas e uso das aquisições|Soma das bases elegíveis|Exemplar; incidência/isenções e exclusões de dedução a fundamentar|Em falta; não assumir zero|
|08|IVA dedutível — imobilizado|Aquisições/facturas de activos, não o valor global do investimento|Apenas imposto elegível documentado|CIVA arts.18–21; art.18(2), Lei10/2025: documentos legais e fornecedor inscrito|Em falta; não assumir zero|
|09|IVA dedutível — existências|Compras projectadas reutilizáveis; falta IVA, documento e elegibilidade|IVA elegível das existências, não compras × taxa geral|CIVA arts.18–21; condições/exclusões, afectação e período|Em falta; não assumir zero|
|10|IVA dedutível — outros bens/serviços|Aquisições discriminadas; despesas agregadas não provam IVA|IVA elegível; excluir operações sem direito|CIVA arts.18–21; art.19(2), Lei10/2025, inclui exclusão das operações art.17-A|Em falta; não assumir zero|
|11|IVA dedutível — importações|Documento aduaneiro legal e afectação; ainda não recolhidos|Imposto aduaneiro elegível, não taxa sobre orçamento|CIVA art.18(2), Lei10/2025; demais condições/exclusões a cadastrar|Em falta; não assumir zero|
|12|Regularizações a favor do sujeito passivo|Ajustamentos individualizados, documento e motivo|Soma de regularizações elegíveis|Exemplar tem remissão interna 11/12 divergente das caixas12/13; fundamento por motivo por conferir|Em falta; não assumir zero|
|13|Regularizações a favor do Estado|Ajustamentos individualizados, documento e motivo|Soma separada das regularizações elegíveis|Mesma divergência; não corrigir o original nem inventar incidência|Em falta; não assumir zero|
|14|Soma das bases do quadro de apuramento|Campos01,03,05,06,07 completos|01+03+05+06+07|Fórmula impressa no exemplar; não é apenas vendas tributáveis|Estrutura mapeada; total indeterminado|
|15|Total a favor do sujeito passivo|Campos08–12 completos|08+09+10+11+12|Fórmula do exemplar; depende de deduções/regularizações fundamentadas|Estrutura mapeada; total indeterminado|
|16|Total a favor do Estado|Campos02,04,13 completos|02+04+13|Fórmula impressa no exemplar; 02 sozinho não equivale a16|Estrutura mapeada; total indeterminado|
|17|Diferença devedora antes de créditos anteriores|Totais15/16 completos|16−15 quando16>15|Instrução do exemplar; não é automaticamente pagamento|Indeterminado; sem consolidação completa|
|18|Diferença credora antes de créditos anteriores|Totais15/16 completos|15−16 quando15>16|Instrução do exemplar; não constitui reembolso reconhecido|Indeterminado; sem consolidação completa|
|19|Excesso a reportar do período anterior|Histórico fiscal anterior confirmado, separado de estimativas|Crédito anterior utilizável; sem copiar crédito estimado|Quadro de utilização de créditos do exemplar; condições/prazo por conferir|Em falta; não assumir zero|
|20|Créditos comunicados pelos serviços|Comunicação efectiva da Administração Fiscal|Crédito efectivamente comunicado e utilizável|Exemplar; não converter pedido ou crédito estimado em comunicação|Em falta; não assumir zero|
|21|Diferencial de declaração de substituição|Valores17/18 da declaração actual e da substituída|Casos impressos no quadro9, com sinal devedor/credor|Exemplar; ligação às declarações efectivas e regras de substituição em falta|Em falta; não assumir zero|
|22|IVA a entregar no contexto da declaração|Apuramento e créditos/substituição completos|Não derivar apenas do campo02|Quadro10 do exemplar; procedimento aplicável por fundamentar|Em falta; não assumir zero|
|23|Juros compensatórios|Factos e períodos que legalmente os determinem|Sem taxa, mora ou obrigação presumidas|Quadro10; fundamento específico não analisado neste lote|Em falta; não assumir zero|
|24|Importância a pagar|Campos22/23 completos|22+23|Fórmula do exemplar; não é ordem nem execução de pagamento|Em falta; não assumir zero|
|25|Crédito de imposto do quadro11|Apuramento completo e tratamento dos créditos anteriores|Montante recuperável no contexto próprio, por fundamentar|Caixa25 confirmada visualmente no bloco de recuperação; condições por consolidar|Em falta; não assumir zero|
|26|Reporte para período seguinte|Crédito utilizável e opção no quadro11|Separado de pedido e recebimento|Quadro11; cotejo de condições/prazos e articulação19/20 necessário|Em falta; não assumir zero|
|27|Pedido de reembolso|Crédito elegível e pedido, sem presumir reconhecimento/caixa|Pedido distinto de aprovação e recebimento|Exemplar cita art.20; Lei10/2025 altera reembolsos no art.21. Divergência registada.|Em falta; não assumir zero|
## Cotejo documental para o período2026
Reutilizados os exemplares oficiais aceites e arquivados em source-archive, a fichaIVA existente e a análise primária; não se pede nova autenticação genérica das fontes. Leitura dirigida agora: Lei22/2022, art.2/aditamento17-A, PDF4/BR2252-(28), e art.6, eficácia01/01/2023; Lei10/2025 no BR248/suplemento1, PDF12–15, especialmente arts.18,19,21 na PDF13/BR2180-(13), arts.32/34 na PDF14. Páginas13 e14 de2025 e página4 de2022 renderizadas para confronto. Não se afirma revisão integral de todo o CIVA ou de todas as alterações/regulamentos.
- Campo01/02: taxa geral e base líquida já fundamentadas pela ficha existente; não reabrir a prova do motor.
- Campo03/04: o art.17-A aditado pela Lei22/2022 identifica serviços médicos/sanitários de estabelecimentos privados indicados; ensino por estabelecimentos privados integrados e reconhecidos; formação profissional por entidades privadas; lições pessoais escolares/superiores. Esta enumeração não classifica automaticamente uma empresa ou toda a sua facturação. Falta cadastro/integração do âmbito temporal e classificação por linha; não se publica uma regra5% com base apenas no formulário.
- Aquisições: Lei10/2025, art.18(2), exige os documentos legais indicados e inscrição do fornecedor; art.18(6) trata proporcionalidade em certas bases reduzidas. Art.19(2) exclui o direito relativo às operações do art.17-A e às indicadas no art.15(6)(c). Portanto, o valor das compras ou o IVA suportado não podem ser automaticamente dedutíveis. Arts.18(1),19(1),20,21 e22, exclusões, afectação, pro rata e momento de dedução precisam de consolidação para cada futuro lote; os trechos[...] da lei alteradora não substituem o texto-base.
- Reembolso: a nota do exemplar cita art.20. A Lei10/2025 altera no art.21 condições e prazos de reembolso/crédito, incluindo números7/8/10/13/15. Registar a divergência; não transplantar prazos ou limites isolados para regras de elegibilidade. Crédito não é recebimento garantido.
- Preços com IVA incluído: art.34 na redacção de2025 prevê método específico para apurar a base e referência a arredondamento nesse contexto. Não é regra universal para o cálculo líquido, nem autorização para aplicar16% ao bruto. O consumidor actual continua a preservar o bruto e a não o calcular. A extensão requer implementação própria e casos dirigidos; não reabre como bloqueio o arredondamento já resolvido para base líquida.
- Declaração/período: art.32 alterado em2025 distingue situações/prazos. Não se decide entrega fora/dentro do prazo por ser uma projecção futura. O exemplar instrui terminar no quadro8 em certos casos; essa instrução não está activa como regra de software.
**Divergências preservadas:** linha de regularizações diz campo11/12 mas caixas12/13; utilização de créditos cita campos18/19, enquanto as caixas desse bloco são19/20; nota de reembolso cita art.20 e as alterações de2025 tratam a matéria no art.21; remissões de quadro8 são ambíguas; a repetição25 foi um artefacto da extracção textual, não uma duplicação de campo no formulário, cuja caixa25 foi confirmada visualmente. Não se fez correcção silenciosa nem se apresentou uma transcrição com ambiguidades como fórmula legal inequívoca.
Pesquisa oficial21/10/2026: [página AT Modelo A](https://edeclaracao.at.gov.mz/formularios/iva/view/formulario_iva_ma.aspx) tem resultado indexado com apuramento no quadro5, diferente do quadro8 deste exemplar. A abertura directa expirou; não se descarregou formulário substituto nem se certificou qual é a edição vigente. [INM BR248/2025](https://inm.gov.mz/pt-br/content/sum%C3%A1rio-suplemento-n%C2%BA-1-de-291225-pag-2180-1-20-br-n%C2%BA-248-boletim-da-rep%C3%BAblica-i-serie-p%C3%A1g-1) identifica a Lei10/2025. O [comunicado governamental de30/09/2026](https://portaldogoverno.gov.mz/governo-introduz-alteracoes-nos-regulamentos-fiscais-para-reforcar-eficiencia-tributaria/) anuncia alterações regulamentares; não fornece por si o articulado para cadastrar novos critérios. A edição exacta aplicável do formulário, correspondência da Lei3/2025 referida na Lei10/2025 e regulamentos pertinentes permanecem por localizar/consolidar no lote respectivo. Nenhuma destas pendências é convertida em bloqueio genérico do cálculo líquido16% já aceite.
Inventário complementar sem utilização automática: Downloads/IVA-2025.pdf(83páginas, consolidação com leis listadas na capa, origem oficial do exemplar ainda não identificada), iva.pdf(8páginas sem texto extraível nas duas primeiras) e alteracao CIVA2023(18páginas, BR250/2022, já corresponde ao acervo arquivado) foram localizados. Não se usa a consolidação de programação ou uma cópia sem identificação como substituto do BR. O inventário global anterior e as fontes não analisadas permanecem conservados.
## Delta entregue e execução progressiva restante
**Entregue nesta candidataQA:** correspondência consultável dos27campos, quadros documentados, resumo mensal rotulado como simulação parcial01/02, notas no relatório sem reescrever os snapshots aceites, reutilização das compras/encomendas activas (por produto, calendário ou constante) mantendo zero distinto de falta e base/IVA/dedução separados e indeterminados. Não transforma custo das vendas, depreciação, pagamento de fornecedor, investimento global ou compra no imposto dedutível. Sem taxa nova no formulário, novo motor, migration, regra publicada ou activação.
**Próximo lote implementável delimitado:** classificação fiscal por operação de venda e aquisição, com pressuposto ou dado efectivo identificados; tipos de documento, datas, fornecedor/inscrição quando aplicável, utilização e exclusões; separar net/IVA/gross sem repetir quantidade/preço. Reutilizar planos como fontes, pedir apenas a classificação e o que falta; registar ligações/versionamento do dado de origem, não copiar e divergir. Dados efectivos não substituem previsões históricas nem herdam automaticamente as declarações de âmbito da projecção. No cadastro, configurar versões/âmbitos/condições da taxa5% e das deduções apenas após consolidar os dispositivos pertinentes; testes de operações excluídas e entradas insuficientes antes de publicar. Depois ligar totais14–18 através do avaliador existente, propagando indeterminação até todas as parcelas estarem conhecidas ou explicitamente sem operações. Num lote posterior, créditos anteriores/substituições/pedido e liquidação, preservando comunicação/reconhecimento/caixa separados. A publicação/activação são operações expressas distintas da integração do código.
Estimativa de saldo, não horas medidas nem compromisso de cobertura integral: **cadastro/consolidação8–16h** (classificação5%, deduções do primeiro âmbito, exemplos e correspondências) e **código/aceitação20–36h** (dados por operação8–14h; ligação a versões/dedução e totais8–14h; histórico/relatório/coerência4–8h), **28–52h** para o próximo lote delimitado. Créditos anteriores, substituições, pagamento/reembolso e cobertura integral ficam fora desta estimativa e exigem delimitação própria; não reutilizar as antigas15–26h como estimativa desta ampliação. Perguntas concretas ainda abertas: edição/norma de aprovação do formulário para o período e dispositivos consolidados pertinentes; não pedir à Tempo nova validação da credibilidade dos BR aceites.
Critérios do lote seguinte: divisão de uma mesma venda entre enquadramentos sem duplicação; aquisições elegíveis/não elegíveis/mistas, documento ausente e fornecedor não inscrito; zero diferente de falta; não deduzir17-A; data relevante por componente; totais nulos com parcelas indeterminadas; alteração de origem cria versão e invalida dependentes, sem reescrever efectivos/previstos anteriores; relatório mostra período/fonte/regra e não gera declaração aceita, guia ou promessa de reembolso. Suspensão e históricos reutilizam o mecanismo existente.
Provas deste delta:3testes frontend novos (prioridade dos calendários de compras, zero/falta e ausência de dedução presumida, adenda sem mutar resultados antigos); TypeScript/build; importação privada/hash/scan e repetição idempotente. Conferência Edge apenas da nova correspondência, compras e relatório recuperado, sem calcular novamente nem repetir CSV/PDF aceites. Provas privadas em outputs/modelo-a-20261021. MatrizAUD-SIM-INT02, inventário documental e recibo existentes actualizados; main e homologação intactos.
@@ -1,5 +1,15 @@
# Governação de versões de regras fiscais
## Correcção de controlo — decisão expressa da Tempo,21/10/2026
A manutenção dos parâmetros fiscais reutiliza a administração já autorizada, com autenticação/MFA, fonte, versão, responsável e histórico. Uma mesma pessoa pode concluir `submit` → `review` → `approve` → `publish`; esses estados preservam o percurso técnico e os eventos, sem afirmar independência pessoal ou parecer jurídico. As quatro listas específicas de permissões permanecem opcionais. A utilização do simulador pelo investidor conserva as permissões e os limites próprios, separados da manutenção.
A obrigação de actores distintos foi encontrada no serviço fiscal e nosCHECKs da migration de03/10/2026, incorporados em `e65cb269` (06/10/2026), e descrita como implementação em `simulator-evolution-progress.md`. Não foi localizada uma decisão expressa da Tempo ou requisito aplicável que a imponha ao cadastro fiscal. A origem identificada é escolha de implementação; a autoria ou integração do commit não demonstra aprovação dessa regra organizacional. A decisão actual da Tempo manda removê-la neste fluxo, sem alterar a independência acordada nas oportunidades, perfis ou outros processos.
A correcçãoQA retira apenas as comparações de identidade entre autor/revisor/aprovador/publicador dos controlos fiscais; mantém campos obrigatórios, evidência, datas, etapas, versão optimista, fonte, bloqueios, RLS e eventos imutáveis. Para cadastrar uma fonte oficial neste percurso existe `POST /api/v2/investment/admin/tax-rule-sources`, sob a mesma administração autenticada. Recebe os metadados existentes mais `sha256`, `citation` e `official_origin`; regista a aceitação da fonte oficial segundo a decisão Tempo de08/10/2026, o responsável real e a indicação explícita de que não é parecer jurídico nem revisão independente. O hash da regra tem de coincidir com o exemplar cadastrado. A fonte permanece pending no estado geral: a aceitação fiscal não a valida para outros módulos. A projecção pública e o resolvedor fiscal reconhecem esse fundamento e o hash correspondente, sem alterar a fórmula. A fonte não activa nem publica uma regra automaticamente. A validação normal de fontes para os outros processos mantém o seu controlo; não se alteraram os workflows de oportunidades/zonas/perfis.
Migration de correcção: `2026_10_21_130000_correct_fiscal_maintenance_actor_constraints.php`, protegida contra execução fora deQA isolado. Acrescenta metadados de aceitação fiscal à fonte e corrige osCHECKs que impediam o actor único. Foi ensaiada apenas emQA, com6testes/86asserções do controlo alterado, sem cálculo, build ou repetição dos testes aritméticos/exportadores. O manual consolidado regista provas e procedimento actualizado das três operações. Os estados históricos abaixo descrevem o incremento original e não reabrem as provas já aceites da candidata226fd308.
## Âmbito deste incremento
Foi criado um fluxo administrativo para registar uma versão declarativa de regra e a sua referência jurídica. O fluxo existe separado do motor de cálculo. Nenhum endpoint de simulação consulta `simulation_tax_rule_versions`; `published` significa apenas publicada no registo interno de governação, não activa no produto nem aprovada para aconselhamento fiscal.
@@ -1,5 +1,20 @@
# 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. |
| Divulgação promocional — candidata QA | **Gerir oportunidades e histórico → Divulgação promocional** (também na ficha) → preencher apenas título/resumo/texto, fonte/edição/data-base e abrangência a divulgar → **Preparar prévia de divulgação** → rever o conteúdo público → motivo e confirmação → **Confirmar divulgação do resumo**. Para actualizar: reabrir → alterar o resumo → **Rever actualização do resumo** → confirmar. Para retirar: **Preparar retirada do resumo → Confirmar retirada do resumo**. O estado público é **Em preparação**; não conclui o workflow nem autoriza execução. Não inclui contactos ou anexos privados. Esta acção está preparada em QA e não foi instalada na homologação nesta passagem. |
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.
@@ -1543,3 +1558,731 @@ Gestão: editar e publicar pelos ecrãs existentes da biblioteca/editorial; o re
### Library integration evidence supplement - 20/10/2026
Functional code remains 3258a25f7659. Existing QA database legacy_trio_20261015_test and its original local storage were selected by an isolated loopback harness; no import, publication or application route override. Normal /sectores/turismo rendered the source-derived summary and image /documentos/25/view (HTTP 200; 900px natural width). Keyboard Enter on Ler o guia opened normal /turismo; the article links to library resource /documentos/10 (source SHA256 8cab862be245382dd4c1fd5fcb423c47aed7be3a22e0a7f8d5521510cc36bc72). Thus the real-library integration limitation of the earlier copied preview is resolved. URLs: http://127.0.0.1:18572/sectores/turismo and /turismo. Evidence: outputs/educational-guides-20261020/real-library-receipt.json, real-library-sector.jpg and real-library-guide.jpg. No dossier path/context change; previous accepted dossier proof retained, not repeated. Tempo visual review remains pending before main integration. AI excluded. This supplement changes evidence/documentation only, not functional code.
## Revisão integrada do simulador — diagnóstico inicial, 20/10/2026
Branch `codex/simulator-integrated-review`, base `e69ab1499fe99196db240c90d149b7ee46292576`. Pedido expresso da Tempo: cálculos e isolamento primeiro; continuidade e resultados a seguir; português transversal. Apenas QA, sem regras fiscais presumidas, activação de parâmetros, alterações reais ou integração em main.
O código separa cálculo aritmético exploratório (`SimulationExecution`/`RunSimulation`, `custom_planning`, `legal_interpretation=false`, moeda não definida), resolução fiscal versionada (`SimulationTaxParameterResolver`, vigência/condições, estados indeterminados) e planeamento financeiro (`financial-calculators`, `business-financial-plan`, `monthly-cashflow`, ponte fiscal). Não os apresentar como um único motor fiscal integralmente validado. A ausência de moeda no cenário aritmético é explícita no contrato; falta conferir a explicação nos seus consumidores, não atribuir MZN por presunção.
| ID | Prioridade / problema ou verificação | Causa / evidência | Acção e prova nova necessária | Estado |
| --- | --- | --- | --- | --- |
| AUD-SIM-INT01 | P0 — data fiscal inválida interrompe validação | `getFiscalPeriodRange` chamava `toISOString` sobre data inválida | Guarda `Number.isFinite`; datas 2026-99-99, 2026-00-01, dia zero, 30 Fevereiro e vazio devolvem null | Corrigido em QA; testes dirigidos passaram |
| AUD-SIM-INT02 | P0 — fiabilidade e dados fiscais | Resolutor existente conserva período, condições, unidade e versão; sem correspondência devolve valor null | Conferir acervo e parâmetros efectivos QA; cruzar casos independentes, distinguir desconhecido/zero; não activar por publicação | Verificação delimitada pendente, não erro comprovado |
| AUD-SIM-INT03 | P0 — isolamento e recuperação | Âmbito cliente por case/draft, cookies anónimos e RLS/políticas/testes existentes | Reconciliar provas recentes de contexto e conta; verificar só lacunas de nova sessão/projectos diferentes sem repetir os casos aceites | Não há fuga comprovada; verificação pendente |
| AUD-SIM-INT04 | P1 — invalidar e conservar resultados | Fingerprints e versões existentes; resultados/snapshots separados do formulário | Conferir alteração de entradas/parâmetros, histórico imutável e destino dossier correcto | Verificação pendente |
| AUD-SIM-INT05 | P1 — unidades, moeda e cálculos independentes | Aritmética SQL numeric; ferramentas com montantes exactos; falta reconciliação integral do acervo actual | Livro de controlo independente para resultados positivos/negativos/zero/incompletos, sem pressupor incentivos por localização | Verificação pendente |
| AUD-SIM-INT06 | P1 — relatórios e recuperação de exportações | Relatórios/snapshots e exportações já existem; provas anteriores em simulations/ e manual | Conferir exemplos efectivamente gerados, coerência e paginação; reutilizar exemplos aceites compatíveis | Exemplos desta revisão ainda não entregues |
| AUD-SIM-INT07 | P1 — mensagens e operações | APIs com idempotência e concorrência; mensagens e falhas por consumidor carecem de inventário final | Validar/calcular/guardar/exportar com confirmação e dados preservados; conferir erros concretos | Verificação pendente |
| AUD-SIM-INT08 | P1 — português transversal | Exportação apresentava nominal/effective; páginas de segurança e spinner em inglês | Tipo de taxa traduzido na saída, confirmação de palavra-passe/segurança e indicador de carregamento traduzidos; códigos internos preservados | Primeira correcção QA; restante inventário aberto |
Inventário de idioma observado: segurança/confirmação, perfil, reposição de palavra-passe, confirmação de email, MFA, chaves de acesso e recuperação. Pesquisa textual em menus, consumidores de investimento, gestão e componentes comuns também devolve identificadores internos; essas ocorrências são candidatos a inspecção, não erros de tradução automaticamente demonstrados. Não traduzir chaves, contratos/API, dados históricos nem nomes de fontes.
Provas reutilizáveis identificadas: `SimulationExecutionTest`, `SimulationDraftContinuityTest`, `SimulationFormDraftTest`, testes de regras/resolução, contratos e fingerprints frontend; relatórios e capturas `docs/investmoz/simulations/` e a secção recente de apresentação no manual. Não correr RefreshDatabase em bases com acervo. As provas documentadas de Outubro antigo não certificam por si só o estado de todas as versões actuais.
Esforço inicial para este recorte: 18–30 h: dados/cálculos 6–10; isolamento/continuidade 5–8; relatórios/erros 4–7; idioma 3–5. Estimativa de engenharia, não tempo já executado nem desenvolvimento fiscal novo. Reestimar após a reconciliação das provas e parâmetros efectivos; fontes/catálogos realmente ausentes ficam separados e não impedem cálculos independentes que disponham dos dados necessários. Nenhuma nova função organizacional exigida.
Correcções iniciais verificadas: 38 testes frontend dirigidos passaram, TypeScript passou. Sem ensaio remoto, SQL de alteração, novas migrations ou mudança do motor fiscal. A revisão integral permanece aberta. Roteiro Tempo, após completar as verificações: escolher contexto → preencher/rever dados → calcular → guardar/recuperar → alterar e recalcular → comparar e exportar. Os relatórios finais e a matriz problema/causa/correcção/prova/pendência serão complementados nesta mesma secção, sem outro backlog.
### Simulador integrado — saldo da verificação QA, 20/10/2026
Continuação de `59ab4fe95635`, exclusivamente na branch `codex/simulator-integrated-review`, baseada no editorial `e69ab149`. Não houve integração em main, instalação remota, migrations, importação ou activação de parâmetros. A base utilizada foi `opportunity_search_20261020_test`, porta 55441, com guarda de isolamento. Os exemplos são sintéticos e explicitamente identificados.
| Item | Resultado e causa/correcção | Prova e saldo concreto |
| --- | --- | --- |
| AUD-SIM-INT01 | Data pontual inválida passa a produzir erro em `fiscal_reference_date`, sem período alternativo. | Testes de vazio, data impossível e ano incompatível; navegador mostrou mensagem junto ao campo, `aria-invalid` e descrição associada. Encerrado este delta. |
| AUD-SIM-INT02 | Inventário efectivo: zero versões de regras fiscais na base QA. As seis fontes territoriais existentes não fornecem taxas ao motor. | Consulta IVA/2026 devolveu indeterminado, nunca zero. Fórmula executada é apenas base × taxa/100 × (1−redução/100), `portal-arithmetic-v1`, valores declarados, sem moeda, regra fiscal ou interpretação legal. A data de execução não é período de vigência fiscal. Fontes oficiais mantêm a aceitação; não se pediu nova autenticação da proveniência. Parâmetros fiscais ausentes ficam indisponíveis, não estimados pela aplicação. |
| AUD-SIM-INT03 | Remontagem do formulário por conta/contexto/rascunho para não reutilizar estado de outro âmbito. Recuperação do rascunho QA observada após reload. | Provas backend anteriores de isolamento mantidas; falta a conferência positiva do delta frontend com dois dossiers e nova sessão autenticada. Reload não foi contado como nova sessão. |
| AUD-SIM-INT04 | Alterar uma entrada retira o resultado actual e mostra aviso de recálculo; não escreve sobre snapshots históricos. | Navegador calculou 12 e, ao alterar base 100→200, retirou o resultado e mostrou o aviso. Versões/hashes backend preservados. Falta concluir a conferência positiva do histórico/contexto no consumidor autenticado e da mudança de versão de parâmetros. |
| AUD-SIM-INT05 | Controlos independentes por livro de caixa, Decimal e recorrência mensal/bissecção para o crédito. | Saldo mensal +100/−100/0; excedente de fundos −200; equilíbrio 11 unidades; crédito sem juros 100 por mês; nominal 12%/ano, 1000/12 meses: 88,85 na primeira prestação; campo vazio recusado. Cenário personalizado 100×16%×75%=12 confirmado no servidor. São controlos matemáticos, não validação de taxas legais. Restam controlos delimitados de cash-flow/plano de negócio. |
| AUD-SIM-INT06 | Resumo do relatório passou a apresentar os resultados financeiros do mesmo modelo de detalhe. Secção sem anexos ficou compacta, eliminando uma página quase vazia. | Quatro exemplos completos pelo exportador real: positivo, negativo, nulo (6 páginas cada), incompleto (5). Todas as páginas DOCX renderizadas conferidas; testes comparam resumo/detalhe. PDF disponibilizado é a renderização do DOCX, não prova da impressão nativa do Edge. Falta essa impressão e a comparação final ecrã/download pela interface. |
| AUD-SIM-INT07 | Removidos requisitos de residência/documentação que não entram no cenário percentual personalizado; mantida validação específica de base/taxa/redução. Consulta fiscal tem nova tentativa e ignora respostas abortadas. Estado de espera é substituído pelo estado terminal do cálculo. | Cenário personalizado calculou sem esses campos; erro 503 sintético na consulta mostrou mensagem e Tentar novamente; repetição preservou IVA/2026 e devolveu indeterminado por falta de parâmetro. Falhas de gravação/exportação ainda carecem de conferência dirigida dos consumidores afectados. |
| AUD-SIM-INT08 | Traduzidos textos de autenticação, perfil, preferências, MFA/chaves, diálogos, notificações e validações comuns, preservando códigos e mecanismos. | Inventário de 4519 textos/atributos JSX literais em todos os TSX; três candidatos restantes são português com a palavra continue. Isto não cobre todas as mensagens dinâmicas do backend ou do catálogo. Inglês explicitamente seleccionável no formulário bilingue e títulos de fontes preservados. Falta classificar o inventário dinâmico e conferir mensagens residuais em execução. |
Operação de idioma para uma eventual disponibilização: usar `APP_LOCALE=pt` na configuração nominal. O fallback de `config/app.php` passou a pt, mas não substitui um valor explícito do ambiente; não copiar `.env` QA. Foram adicionados `lang/pt/auth.php`, `lang/pt/validation.php` e `lang/pt.json`; as regras comuns cobertas não equivalem ao catálogo completo de validações Laravel.
Complemento do inventário: os 77 candidatos de literais TS/TSX restantes foram classificados como classes CSS internas ou traduções explicitamente seleccionáveis (incluindo menus, formulário bilingue e assistência, cuja activação não foi alterada). Foram ainda corrigidos o breadcrumb de segurança e as três mensagens de falha no carregamento de QR/chave/códigos. O fecho desse levantamento estático não certifica todas as mensagens produzidas por backend, conteúdo e serviços em execução; esse é o saldo de idioma ainda aberto.
Provas novas: 34 testes dirigidos de formulário/relatório passaram; TypeScript, build e lint PHP dos ficheiros de idioma/configuração passaram. Mantidas as provas anteriores não afectadas de contratos, APIs, isolamento e versões; nenhum RefreshDatabase executado sobre o acervo QA. Artefactos locais: `outputs/simulator-integrated-review/` (inventário efectivo, controlos independentes, modelos dos quatro relatórios, DOCX/HTML e `checked-*` com PDF/páginas conferidas). As pastas anteriores `render-*`/`final-*` são intermédias e não são as versões entregues.
Exemplos para revisão por HTTP na aplicação QA local, sem `file:`: `http://127.0.0.1:18570/qa/relatorios/simulador/positivo.pdf`, `negativo.pdf`, `nulo.pdf`, `incompleto.pdf`, e os mesmos nomes com extensão `.docx`. Estes endereços são uma whitelist do router privado de QA, exclusivamente loopback; não são rotas públicas de produção. Ficheiros não sintéticos e configuração privada não são servidos por esta whitelist.
**Estimativa restante: 6–10 h**, já descontado o trabalho acima e as provas aceites; não são as 18–30 h iniciais. Distribuição: continuidade autenticada/histórico e mudança de parâmetros 2–3 h; controlos adicionais delimitados 1–2 h; impressão Edge e comparação ecrã/download 1–2 h; mensagens dinâmicas/falhas de gravação/exportação e fecho 2–3 h. Não inclui cadastrar regras fiscais ausentes nem decidir taxas/incentivos sem fundamento. Falta de dados fiscais não bloqueia os controlos aritméticos disponíveis. A revisão integral continua aberta nos pontos precisos desta tabela.
### Continuação delimitada do simulador — controlo operacional e fontes
Base `a690c665fb4e`, mesma branch QA. Clone exclusivo `simulator_review_delta_20261020_test`, loopback 55441, aplicação `http://127.0.0.1:18576`. A base anterior e as contas reais foram preservadas. Duas contas e dois dossiers SINTÉTICOS QA foram preparados apenas neste clone; sem funções de gestão e sem transporte para o destino. O sinalizador local `INVESTOR_PATH_ENABLED=true` habilitou a capacidade já existente neste clone; não representa configuração aplicada remotamente.
| Item existente | Resultado desta continuação | Saldo |
| --- | --- | --- |
| AUD-SIM-INT01 | Prova anterior de data inválida preservada; código não alterado neste ponto. | Concluído o delta anterior; sem repetição. |
| AUD-SIM-INT02 | Confirmado zero de regras de qualquer estado na base QA original, com visibilidade administrativa sem ocultação por RLS; biblioteca contém 30 documentos. Inventariados 9901 caminhos do acervo antes da selecção: 32 candidatos por nome fiscal e três originais versionados no repositório, estes com hash actual conferido. Existem análises de textos e correspondências preparadas em `primary-text-review-2025-2026.md`, `official-source-register.md`, `fiscal-source-references-2026-10-06.csv` e comando de arquivo existente. Não se identificou um conjunto serializado de parâmetros fiscais reais nos seeders/configuração pesquisados. | Não confundir ausência do cadastro executável QA com ausência de fontes/trabalho documental. Leitura actual da homologação aguarda autenticação SSH local; não se afirmou que o destino também está vazio. Os binários do acervo montado não foram todos relidos: enumeração de caminhos não comprova conteúdo/versão. |
| AUD-SIM-INT03 | A e B recuperaram títulos próprios; B conservou a edição após erro/repetição. Logout e novo login normais, depois recuperação de A e B, conferidos no navegador. Recusa de outra conta: 403 nos dois dossiers e 404 no resultado, pelo serviço/política utilizados pelo controlador. | Concluída a conferência delimitada. O papel PostgreSQL deste clone é superutilizador: esta prova de recusa é da aplicação, não nova certificação de RLS. Provas anteriores de RLS não privilegiado mantidas. |
| AUD-SIM-INT04 | Corrigida a abertura de histórico que carregava mas não mostrava o resultado no percurso de pequeno negócio. Resultado seleccionado agora visível e explicitamente separado das entradas do dossier. Navegador abriu 24 e 12; três snapshots mantiveram hashes/versões originais após a utilização. | Versões sintéticas exclusivamente em memória do resolvedor: 7,5% até Junho e 9,25% a partir de Julho, fonte e código QA que não representam legislação. Versão histórica conservada; transição dentro do período fica indeterminada; versão da fonte incompatível deixa de resolver. Nada foi registado/publicado na base ou no destino. É prova do resolvedor, não de execução fiscal integrada. |
| AUD-SIM-INT05 | Livro independente de três meses: resultado antes de impostos 225 MZN, fundo de maneio 130, usos 730, caixa inicial após usos 470 e caixa final 638. Distintos reconhecimento/pagamento de juros, capital, compras, despesas e cobranças. Tesouraria com mês de imposto desconhecido mantém total indeterminado; caixa conhecida 210, sem somar crédito/pedido/reconhecimento de IVA como dinheiro. | Controlos passaram. Pressupostos completos no teste `simulator-independent-review.test.mjs`; não são previsões ou taxas legais. |
| AUD-SIM-INT06 | CSV e Word efectivamente descarregados pelos botões normais do dossier B, não por gerador autónomo. Prestação 88,85, juros 66,19 e saldo 100 coincidem com o ecrã. DOCX usa vírgula decimal; CSV/ecrã usam ponto. Word de seis páginas integralmente conferido após renderização, com pressupostos e campos desconhecidos explícitos. | Impressão nativa Edge aberta, aguardando gravação de `SINTETICO-QA-Edge-continuacao.pdf` pelo utilizador; o diálogo não é acessível à automação. Não contar o PDF renderizado do DOCX como essa prova. |
| AUD-SIM-INT07 | Corrigido Abrir cálculo: pequeno negócio abre a etapa financeira existente, em vez de procurar uma etapa fiscal ausente. Falha única 503 de gravação preservou edição; botão Guardar agora repete efectivamente a escrita, com fila serializada e rechecagem da impressão digital para evitar gravação redundante. Recuperação posterior confirmou a edição. | Exportação normal Word/CSV comprovada. Provas automáticas aceites de limites de anexos preservadas e regressão do exportador passou. A tentativa interactiva com anexo sintético de 16 MiB foi impedida pela permissão de URLs de ficheiro da extensão, antes de o ficheiro entrar na aplicação; não foi registada como falha do portal nem como prova de recusa pela interface. |
| AUD-SIM-INT08 | Mensagem dinâmica Not found observada foi tratada no respondedor comum: mensagens genéricas de ausência/acesso/sessão/tentativas/servidor em português, preservando HTTP e correlação. Traduzidas as mensagens de validação de parâmetros/expressões que o controlador de governação apresenta nos campos, sem alterar DSL, códigos ou limites. | Conferência do 404 por pedido real ao kernel passou; mensagens das validações revistas no código/controlador. Inventário estático anterior e traduções intencionalmente seleccionáveis mantidos. Não se certificou cada conteúdo dinâmico do catálogo nem uma sessão administrativa de governação. |
**Pressupostos do crédito de controlo:** capital 1000 MZN; prazo 12 meses; periodicidade mensal (12 prestações); taxa nominal anual hipotética 12%, dividida por 12, resultando em 1% por mês; sem carência, comissão inicial ou periódica. Montantes com duas casas decimais, juros/prestação com metade para cima; saldo residual ajustado na última prestação. Primeira prestação 88,85; última 88,84; juros totais 66,19; total pago 1066,19 MZN. O CSV contém o cronograma completo; o Word apresenta prestação inicial, juros e receita de cálculo. O controlo anterior de 100 por mês usa capital 1200 MZN, 12 meses, periodicidade mensal, taxa nominal anual zero, sem carência/comissões, mesmo arredondamento, total 1200. Estes pressupostos foram acrescentados às saídas e à prova; nenhum produto de crédito foi confirmado.
**Reconciliação fiscal:** os três originais já versionados (BR 248/2025, republicação Lei 22/2022 e BR 89/2017), as análises e relações documentais são trabalho reutilizável. Não pedir novamente documentos já disponíveis nem reabrir a credibilidade das fontes oficiais aceites pela Tempo em 08/10. As referências antigas a reautenticação genérica ficam ultrapassadas por essa decisão; continuam a ser tarefas distintas identificar artigo, âmbito, período e parâmetro. A biblioteca documental não é a tabela de parâmetros. O repositório contém governação, DSL e resolvedor temporal; `RunSimulation` executa a aritmética personalizada e não consome essas fórmulas fiscais. Mesmo encontrar parâmetros no destino não comprovaria cálculo fiscal integrado. Essa ligação funcional não foi implementada silenciosamente neste recorte.
Artefactos em `outputs/simulator-continuation-20261020/`: `interface-comparison.json`, `interface-financial.csv`, `interface-report.docx`, seis páginas em `docx-render/`, provas de A/B e nova sessão, `history-preservation-proof.json`, `fiscal-corpus-inventory.json` e `local-rule-visibility.json`. Credenciais, dump e fixtures privadas ficam excluídos do Git. O script `read-fiscal-remote.py` preparado só faz leitura administrativa e `BEGIN READ ONLY`/SELECT/ROLLBACK; ainda não foi executado por faltar autenticar a ligação. Nenhum teste ou dado sintético foi enviado à homologação.
**Esforço restante do recorte: 1–3 h após os acessos/ficheiro necessários**, para recolha remota, conferência do PDF nativo e fecho das limitações de interface. O tempo de espera do utilizador não é esforço de programação. Não são as 6–10 h anteriores nem as 18–30 h iniciais. Não inclui implementar o motor fiscal integrado ou activar parâmetros reais. As pendências de acesso e impressão não bloqueiam a revisão dos resultados QA já utilizáveis.
**Achado adicional de continuidade, identificado antes da correcção:** o componente de tesouraria mantinha os seus pressupostos apenas no estado React; não recebia nem devolvia snapshot ao rascunho. Foi acrescentado `cashflow_snapshot` versionado ao JSON económico existente e ao validador do pedido, sem migration. O componente recupera os campos e o modo de imposto; guarda apenas após edição e não converte vazios em zero. Snapshots desconhecidos/inválidos não geram resultados alternativos e mostram aviso. O relatório consolidado e o CSV do dossier passam a incluir pressupostos, valores conhecidos/indeterminados e meses; o resumo destaca a tesouraria quando existir snapshot. Dossiers antigos sem esse campo mantêm a apresentação anterior — as seis páginas do DOCX de B já conferidas não tinham esse campo e não foram reemitidas desnecessariamente.
Provas adicionais: teste HTTP com dois dossiers, logout lógico/nova sessão, recuperação exacta e recusa por outra conta passou (8 asserções), em transacção revertida no clone. Quatro controlos frontend dirigidos passaram, incluindo recuperação de mês vazio, valor inválido para correcção, relatório sem zero inventado e pressupostos completos do crédito. As 10 verificações do exportador afectado passaram; teste temporal sintético passou (7 asserções); erro HTTP em português passou (3). TypeScript/build e sintaxe PHP passaram. A conferência visual do novo componente de tesouraria com sessão real QA permanece por executar depois de fechar a impressão do Edge; não foi substituída por uma prova unitária.
**Saldo revisto após esse achado: 2–4 h de verificação delimitada**, já descontado o desenvolvimento concluído: leitura remota 0,5–1 h; PDF nativo 0,5–1 h; continuidade visual/saída da tesouraria recém-corrigida 0,5–1 h; consolidação das provas e limitações 0,5–1 h. Exclui espera por autenticação/ficheiro e a integração funcional futura das regras fiscais. Esta estimativa substitui 1–3 h acima por uma divergência concreta; não retoma as 6–10 h nem as 18–30 h originais.
### Retoma da conferência visual de tesouraria — 21/10/2026
[Retoma 21/10/2026] QA 18576, clone simulator_review_delta_20261020_test: sessão sintética renovada por login normal; gravação autenticada e recarga recuperaram saldo 123, receita 900, custos 600, calendário mensal 10/vazio/0, crédito IVA estimado 500. Caixa conhecida 3713 MZN; total indeterminado desde mês 2; crédito estimado excluído da caixa. Prova visual cashflow-recovered-20261021.png e snapshot .txt em outputs/simulator-continuation-20261020. Não certifica recuperação destes novos valores após outro login, CSV/DOCX ou PDF nativo. Ligação de controlo do navegador perdida durante espera de download; não atribuir falha à aplicação. Leitura remota aguarda identificação da ligação SSH; zero alterações no destino.
### Leitura fiscal remota e saída nativa — 21/10/2026
[Leitura SSH 21/10/2026 08:17 Maputo] Zero regras em simulation_tax_rule_versions na homologação, incluindo todos os estados por leitura administrativa sem ocultação RLS. Seis referências em investment_legal_sources; não são parâmetros executáveis. QA também tinha zero regras. BEGIN READ ONLY/SELECT/ROLLBACK; zero escritas. Prova homologation-fiscal-readonly.json. O git HEAD registado é do checkout consultado, não certifica a revisão do build servido. A ausência de cadastro não é ausência de fontes oficiais nem reabre a aceitação da Tempo de 08/10. Não implementa a ligação funcional ao motor fiscal.
[Exportação nativa 21/10/2026] Saída actual aberta pelo botão do portal: revisão 4, 12 meses, MZN, saldo inicial123/receita900/custos600, calendário10/vazio/0, caixa conhecida3713 e total indeterminado coincidem com o ecrã. Crédito estimado500 identificado como pressuposto, não somado à caixa. Prova portal-print-current-20261021.txt. Relatório de20/10/revisão2 preservado, sem o confundir com o actual. Downloads: sem novo CSV na pasta habitual ou nos temporários locais conferidos; isso não demonstra falha nem cancelamento. Histórico de downloads bloqueado pela política da ferramenta; caminho do ficheiro solicitado antes de repetir. PDF nativo aberto, mas paginação/legibilidade aguardam o ficheiro efectivamente guardado pelo Edge; conversão DOCX/PDF anterior não conta como essa prova. Perda da ligação e bloqueio de controlo durante impressão são limitações da ferramenta, sem falha do simulador demonstrada.
### Ficheiros de Transferências recuperados — 21/10/2026
[Fecho da verificação dos ficheiros 21/10/2026] Transferências Windows redireccionada para \\Mac\Home\Downloads; CSV concluído08:12:46, PDF nativo08:17:19. Sem repetir downloads. Os12 registos mensais do CSV coincidem exactamente com o snapshot do ecrã após correspondência de colunas; PDF revisão4 conserva valores mensais, MZN,12meses,123/900/600, crédito estimado500 não caixa e caixa conhecida3713. Os10 PNG do PDF nativo foram inspeccionados: defeito confirmado na linha repetida de rodapé, sobreposta ao corpo perto do topo de páginas, incluindo2,6e10. Conteúdo/valores conferidos; paginação/legibilidade não aceites até correcção e nova prova nativa. Não é conversão DOCX/PDF. Perda de ligação continua limitação da ferramenta e não causou falha do download; sobreposição é defeito distinto demonstrado no PDF. Provas: download-native-inspection-20261021.json, portal-cashflow-20261021.csv, portal-native-20261021.pdf e native-pdf-render/. A indicação anterior de ficheiros pendentes fica superada por esta prova. Sem alteração funcional nem escrita remota.
### Correcção do rodapé do PDF nativo em QA — 21/10/2026
[Correcção do rodapé nativo QA 21/10/2026] Origem: pedido expresso da Tempo nesta conversa; solução de implementação, sem regra organizacional nova. Causa em createProjectDossierPrintHtml (project-dossier-document.ts): footer position:fixed/bottom:-14mm sobrepunha a área fragmentada de impressão. Substituído na impressão por caixas @page @bottom-left/@bottom-right, dentro da margem inferior reservada20mm, com identificação/revisão e número de página/total. Rodapé normal de ecrã conservado; sem diminuir letras do corpo (10,5pt), tabelas, cabeçalhos ou origens (9pt), nem retirar conteúdo. Mesmo dossier B/revisão4 regenerado pelo botão do portal QA18576 e guardado no Edge, não por conversão DOCX. 10→10 páginas A4; comparação automática do corpo por página, descontando apenas hora de emissão/rodapé/contador, coincide integralmente. Inspecção visual dos10 PNG: sem sobreposição, cabeçalhos de tabela repetidos, quebras/linhas preservadas, última página íntegra e numeração1/10→10/10. Rodapés medidos inteiramente na margem inferior. Ecrã QA conferido após build; estilos screen e geradores DOCX/ZIP inalterados por comparação de fontes. 10 testes do exportador, TypeScript e build passaram; escape da revisão na nova string CSS verificado. Reutilizadas provas de valores e CSV aceites; não repetido download/aritmética. Provas: native-footer-fix-20261021.json; portal-native-fixed-20261021.pdf; native-footer-before-after-20261021.jpg; native-pdf-fixed-render/; screen-after-print-fix-20261021.png. Fechado exclusivamente o defeito de rodapé neste relatório QA; limitações e restantes critérios de AUD-SIM-INT06 mantêm âmbito próprio. Relato anterior de sobreposição fica superado neste delta pela comparação antes/depois. Branch codex/simulator-integrated-review separada de main, sem integração, escrita remota ou disponibilização activa; revisão prevista mantém-se pendente.
### Saldo consolidado dos oito itens — 21/10/2026
Reconciliação a pedido expresso da Tempo. O fecho do rodapé limita-se à apresentação deste relatório e baseia-se nas provas apresentadas; o utilizador declarou não ter conferido a imagem nem o PDF pelos caminhos locais. Não é fecho da revisão integral do simulador. Reutilizadas as provas aceites, sem repetir10 páginas, valores,CSV ou consulta SSH.
| Item existente | Estado no recorte | Saldo / limite |
|---|---|---|
| AUD-SIM-INT01 | Concluído no recorte | Nenhum delta de validação de data por demonstrar; prova anterior preservada. |
| AUD-SIM-INT02 | Parcial | Leitura remota concluída: zero regras de todos os estados na homologação, tal como QA; seis referências jurídicas não são parâmetros executáveis. Falta cadastro fundamentado por componente/âmbito/período e ligação funcional ao motor fiscal. Fontes e análises existentes reutilizáveis; aceitação oficial da Tempo de08/10 preservada. Não activar taxas nem importar/publicar por esta reconciliação. |
| AUD-SIM-INT03 | Concluído no recorte | A/B, logout/novo login, recuperação e recusa pela aplicação demonstrados anteriormente. A tesouraria tem prova API de nova sessão e prova visual de recuperação por recarga. Não atribuir ao navegador nova sessão dos valores de tesouraria, nem nova certificação RLS; limites expressos conservados. |
| AUD-SIM-INT04 | Concluído no recorte demonstrado | Histórico24/12 visível, hashes preservados e versões temporais sintéticas do resolvedor conferidas. Execução fiscal integrada com parâmetros reais pertence ao saldo de02 e não foi demonstrada por estes testes. |
| AUD-SIM-INT05 | Concluído nos controlos delimitados | Controlos independentes de caixa/plano/crédito, zero/desconhecido e moedas preservados. Não são validação de taxas legais nem certificação de toda a contabilidade. |
| AUD-SIM-INT06 | Concluído na apresentação e comparação deste relatório | Rodapé do PDF nativo fechado em QA segundo provas apresentadas;10 páginas/valores/CSV não repetidos. Tempo declarou que não abriu imagem/PDF pelos caminhos locais: registar aceitação das provas, não revisão visual directa do utilizador. Outras provas de relatório conservadas; falhas interactivas de exportação/anexos mantêm saldo em07. |
| AUD-SIM-INT07 | Parcial | Gravação503→nova tentativa→recuperação e exportações normais demonstradas. Faltam falhas dirigidas de geração Word/ZIP/PDF e retoma; teste interactivo de limite de anexo não ocorreu por bloqueio da ferramenta antes do envio. Nova leitura do código: três catch apresentam cause.message para Error, podendo mostrar texto técnico sem tradução. Sem nova falha de execução alegada. |
| AUD-SIM-INT08 | Parcial | Inventário estático,404 real e traduções de validação preservados. Classificar/apresentar em português os erros dinâmicos das bibliotecas de exportação (cause.message, três consumidores) e conferir estados residuais/governação delimitados; não certificar todos os textos dinâmicos do catálogo. |
**Estimativa restante deste recorte:1–2h de engenharia**, excluindo espera por interacção necessária da ferramenta: 07/08 agrupados0,75–1,5h para tratamento e conferência dirigida de erros dinâmicos/retoma/anexos; 0,25–0,5h para reconciliação final das provas. Não somar o mesmo erro duas vezes por aparecer em07e08. Substitui o saldo anterior2–4h porque leitura remota, apresentação nativa e comparação da tesouraria já foram executadas. Não inclui cadastrar/aplicar regras fiscais reais, integrar fórmulas no motor ou certificar todo o catálogo. O esforço para02 integral permanece por dimensionar por componente, âmbito, período e correspondência documental; não há estimativa de conclusão integral obtida apenas deste1–2h.
Próxima execução delimitada: corrigir a apresentação de excepções brutas nos três exportadores, conferir falha/retoma e os limites interactivos que faltam, preservando os relatórios/CSV já aceites. Branch codex/simulator-integrated-review mantida separada de main até à revisão consolidada. Nenhuma nova concessão, regra fiscal, migration ou escrita remota.
### Candidata consolidada da revisão do simulador — 21/10/2026
Origem: pedido expresso da Tempo para fechar apenas falhas dirigidas de exportação/anexos, apresentação das mensagens e consolidação. As 1–2 h anteriores respeitavam a esse recorte, não à implementação fiscal integral. A branch `codex/simulator-integrated-review` permanece separada de `main`; não integrar antes da revisão consolidada porque a cadeia existente disponibiliza automaticamente a revisão integrada.
**Separação funcional preservada.** Os cálculos de planeamento usam valores declarados pelo utilizador, com origem e pressupostos identificados. O relatório não os transforma em resultados fiscais. Falta de imposto/parâmetro continua indeterminada, não zero; crédito/pedido/reconhecimento de IVA não se torna dinheiro recebido. A correcção de falhas de exportação e do rodapé não encerra a componente fiscal.
**Implementação deste delta.** O exportador apresenta mensagens em português, identifica a operação e a referência de diagnóstico e mantém as selecções para nova tentativa pelo mesmo botão. A excepção original é registada na consola técnica local com operação/referência; não há novo envio ao servidor, telemetria nem registo de conteúdo do dossier/anexos. A consola é um diagnóstico local, não uma auditoria persistente no servidor. Uma guarda síncrona recusa outra execução enquanto a primeira está activa, mesmo antes da actualização do estado React; após erro a guarda é libertada. Preparação dos anexos é atómica: falhas/limites não substituem a lista existente nem as legendas. A selecção repetida do mesmo nome com os mesmos bytes mantém uma entrada; bytes diferentes com o mesmo nome e nomes diferentes continuam distintos. Esta identidade é escolha de implementação local, não regra de cadastro/publicação documental. Os limites de quantidade/tamanho já existentes não foram alterados.
**Provas reutilizadas.** Data inválida; A/B e nova sessão; recusa de outra conta; histórico24/12 e hashes; resolvedor temporal sintético; controlos financeiros independentes; Word/CSV e respectivos valores; PDF nativo10 páginas/rodapé/última página; inventários/documentos e leitura SSH sem escrita. A Tempo aceitou o fecho do rodapé segundo as provas apresentadas e declarou que não abriu a imagem/PDF pelos caminhos locais. Não lhe atribuir inspecção visual directa. Os testes temporais sintéticos não representam legislação. A prova de recusa do clone com superutilizador é da aplicação; não é uma nova certificação de RLS. Não repetir as provas aceites apenas para gerar uma nova lista de resultados.
**Ensaio dirigido isolado.** `http://127.0.0.1:18576/qa/export-recovery` usa o componente real e os geradores reais, com uma falha única de biblioteca injectada no módulo de ensaio, sem alteração das rotas de produção. Os controlos de falha e os ficheiros sintéticos ficam em `outputs/`, fora do Git e do pacote. Os contadores referem-se a chamadas/generações; para ZIP contam a geração interna de Word e a preparação do arquivo, não duas transferências. No ensaio de nova tentativa de PDF é omitida apenas a impressão automática, para observar a janela com conteúdo e não repetir a prova nativa aceite. Nenhuma dessas injecções entra na candidata funcional.
**Procedimento de integração após revisão.** Fixar o commit candidato e manifesto de fontes; obter o `main` remoto actualizado e reconciliar apenas os deltas dos programadores que afectem estas fontes/consumidores. Rever o diff e conferir no ambiente QA a revisão exacta a integrar; repetir apenas verificações afectadas por conflitos ou alterações posteriores. Este delta não contém migrations, cadastro de regras, importação, publicação de documentos, contas, permissões ou alterações de MFA/configuração. Após decisão da Tempo, integrar pelo processo Git existente; confirmar o recibo da cadeia e a revisão efectivamente servida em `/build/revision.json`, com consulta pública e login privado conferidos nos consumidores afectados. Uma falha deve conservar a release anterior; não editar a pasta servida para contornar a cadeia. A reversão usa o mecanismo de release anterior de código/build, preservando base, anexos, configuração e dados posteriores; não repor um dump antigo. Se uma futura entrega incluir regras/dados activos, exigir o procedimento específico já aprovado de backup/restauro e acção autenticada; esse trabalho não está incluído aqui.
### Diagnóstico fiscal separado da candidata — 21/10/2026
| Camada | Existente e comprovado | Saldo efectivo |
|---|---|---|
| Fontes | Inventário anterior de9901 caminhos e32 candidatos por nome; três originais versionados: BR248/I/Suplemento1/2025, republicação Lei22/2022 no BR250/2.ºSuplemento e BR89/2017. Análises em `primary-text-review-2025-2026.md`, `official-source-register.md`, `fiscal-reform-2026-review.md` e referências em `fiscal-source-references-2026-10-06.csv`. | Reutilizar conteúdo, hashes, versões e citações já disponíveis; não pedir novamente esses documentos. A enumeração de caminhos não equivale à leitura de todos os binários. Distinguir fontes já lidas de fontes só inventariadas e alterações/anexos ainda sem correspondência. |
| Parâmetros executáveis | Leitura administrativa de todos os estados confirmou zero linhas em `simulation_tax_rule_versions` tanto na QA como na homologação. A homologação tem seis referências em `investment_legal_sources`, ligadas a estruturas territoriais; não são taxas/fórmulas prontas. | Cadastrar por componente: código/versão, diploma/artigo/versão/hash, âmbito e condições, início/fim de aplicação, valor/unidade, bases/limites/exclusões, fórmula e arredondamento fundamentados. Os campos necessários variam por regra; não preencher lacunas por zero ou pela taxa mais recente. |
| Governação e consulta | Modelo versionado, controlador de transições, API de resolução por componente/período/contexto, selecção de publicados e compatibilidade de versão da fonte; resolução temporal e indeterminação já ensaiadas. | Reutilizar estes serviços; conferir as correspondências documentais e a operação com parâmetros reais quando estes existirem. Não criar aprovações, nomeações ou uma nova exigência organizacional por escolha técnica. A Tempo define os papéis e as regras. |
| Expressões | `SimulationTaxRuleExpression` já valida e avalia a DSL `investmoz.tax.v1` com aritmética racional, arredondamento e valores em falta. | Não reconstruir o avaliador. Verificar se cada regra fundamentada cabe na linguagem existente e estender apenas operações concretamente necessárias. |
| Execução fiscal integrada | `RunSimulation` executa `base × taxa/100 × (1−redução/100)`, identifica `custom_planning`, `legal_interpretation=false`, `ruleset_version=null` e moeda não indicada nesse cenário. Consulta/DSL não são consumidas por este fluxo como motor fiscal. | Desenvolver orquestração explícita de componente/período/âmbito → regras elegíveis → entradas e parâmetros → avaliação → resultado e explicação com fontes/versões imutáveis. Tratar ambiguidade/transição/ausência como indeterminação. Manter o cenário de planeamento separado, sem o relabelar como imposto calculado. |
| Resultado, dossier e relatório | Snapshots, histórico e exportadores existentes podem transportar resultados e proveniência; relatórios actuais identificam pressupostos/estimativas. | Integrar o resultado fiscal com identificação própria de regra, período, unidade/moeda, base, passos, arredondamento e lacunas. Guardar reprodução histórica e demonstrar casos de referência por regra, isolamento e não reaplicação de uma versão actual a um resultado antigo. |
**Componentes a parametrizar, não taxas inventadas.** O acervo já analisado identifica famílias de alterações para ICE (Lei7/2025/tabela), ISPC (Lei9/2025), IVA (republicação Lei22/2022 e Lei10/2025, além da consolidação/rectificação aplicável), IRPS (Lei11/2025) e IRPC (Lei12/2025). A API também contempla INSS, retenções e direitos aduaneiros. Isto é inventário de domínios, não prova de cobertura documental completa nem autorização para aplicar uma taxa genérica. Para ICE/aduaneiro são relevantes código/unidade/tabela e período; para IVA, operação, incidência/exclusões/dedução e estado de reembolso; para ISPC, elegibilidade/limites e obrigações; para IRPS/IRPC, categoria, residência/estabelecimento, base, retenções e regimes específicos. Prazos, taxas, anexos e incentivos só entram com correspondência documental aplicável.
A aceitação das fontes oficiais determinada pela Tempo em08/10 prevalece sobre pedidos históricos genéricos de reconfirmação de autenticidade ou de um fiscalista adicional. Identificar o artigo, âmbito, período, alterações/revogação e parâmetros é aplicação/rastreabilidade da fonte aceite, não segunda validação da sua credibilidade. A biblioteca documental, a situação jurídica e a utilização em cálculo são estados independentes; importar ou publicar documentos nunca activa cálculo fiscal.
**Desenvolvimento fiscal necessário:** correspondência documental e cadastro versionado; adaptação concreta de expressões quando necessária; orquestração do cálculo; contrato de resultado/proveniência/histórico; ligação à interface/relatórios e casos de referência por componente/período. Nenhuma dessas frentes é encerrada por corrigir crédito, orçamento, tesouraria, falhas de exportação ou rodapé. O esforço integral deve ser dimensionado por regras/componentes e lacunas documentais identificadas; não cabe na estimativa de1–2h do recorte de exportação.
### Fecho do recorte de exportações e saldo da candidata — 21/10/2026
| Item existente | Saldo consolidado da candidata | Prova / limite |
|---|---|---|
| AUD-SIM-INT01 | Concluído no recorte | Validação de data: prova anterior reutilizada. |
| AUD-SIM-INT02 | Parcial; implementação fiscal integral aberta | Zero regras remotas/QA; fontes, cadastro e integração discriminados no diagnóstico separado. |
| AUD-SIM-INT03 | Concluído nas provas delimitadas | A/B/nova sessão e recusas anteriores; tesouraria com API/nova sessão e UI/recarga, sem alegar uma nova prova browser da mesma sessão. Limite RLS mantido. |
| AUD-SIM-INT04 | Concluído no recorte demonstrado | Histórico/snapshots e resolvedor sintético reutilizados; integração real fiscal permanece em02. |
| AUD-SIM-INT05 | Concluído nos controlos delimitados | Controlos independentes e provas financeiras aceites reutilizados; sem validação legal de impostos. |
| AUD-SIM-INT06 | Concluído na apresentação/comparação deste relatório | Rodapé nativo aceito segundo provas; sem repetir10páginas/valores/CSV; sem atribuir inspecção local ao utilizador. |
| AUD-SIM-INT07 | Concluído no recorte dirigido QA |3falhas de geração/retoma, anexo16MB recusado com lista conservada, re-selecção sem duplicação e ZIP íntegro; injecção sintética fora da entrega. |
| AUD-SIM-INT08 | Concluído nos consumidores afectados | Mensagens portuguesas/diagnóstico técnico separado; limites de catálogo/governação mantidos, sem certificação global. |
Esforço restante do recorte de engenharia de exportações:0h; revisão consolidada e eventual integração continuam pendentes. As1–2h anteriores foram estimativa deste recorte; não eram estimativa da implementação fiscal integral. Não fechar a revisão integral do simulador nem AUD-SIM-INT02 por este incremento. Word/PDF/ZIP foram ensaiados no componente real com falhas sintéticas de geração de um invólucro QA privado; ficheiros de67bytes e16MiB seleccionados manualmente pelo utilizador, estados observados directamente pelo Codex. A primeira recusa encontrou lista vazia e não foi usada como prova de preservação; a sequência posterior demonstrou lista válida→recusa16MB mantendo1→re-selecção válida mantendo1. ZIP real verificado por bytes, entrada única, título e secção. Leitura transitória falhada e concorrência demonstradas nos testes dirigidos, não por avaria real do sistema de ficheiros. A limitação anterior da selecção automática não foi contornada: a selecção normal foi manual. Os detalhes técnicos permanecem em diagnóstico local; não foi criado serviço de recolha remoto.
Provas actuais em outputs/simulator-continuation-20261020/: export-word-error-20261021.*, export-pdf-error-20261021.txt, export-pdf-retry-20261021.txt, export-zip-error-20261021.*, export-zip-retry-20261021.txt, attachment-limit-preserved-20261021.*, attachment-duplicate-retry-20261021.*, export-zip-content-proof-20261021.json e export-recovery-candidate-20261021.json. O procedimento de integração e diagnóstico fiscal imediatamente anteriores integram esta mesma candidata e o mesmo registo; sem matriz/backlog adicional.
### Identificação da entrega e conferência do limite — 21/10/2026
A entrega funcional `13f68f1b` identifica-se como **melhorias de planeamento financeiro, recuperação, exportação e idioma**. Está pronta para revisão de integração, não representa a conclusão do simulador fiscal. Não houve integração em `main` nem disponibilização activa. A Tempo confirmou o fecho em QA segundo o relatório e declarou não ter conferido o registo local; não atribuir essa conferência ao utilizador.
Conferência somente por leitura, sem repetir ensaios: em `c22003a3`, anterior à candidata, `project-dossier-report.ts` já definia `MAX_DOSSIER_ATTACHMENT_BYTES = 15 * 1024 * 1024` (15.728.640 bytes), quantidade10 e total50MiB. A candidata conserva esses valores. O ficheiro recusado tem16.777.216 bytes (16MiB), superior ao limite preexistente de15MiB; não foi introduzido limite inferior. A interface histórica escreve «MB», mas a implementação usa unidades binárias. A origem é uma escolha de implementação já existente; esta leitura não inventa uma aprovação organizacional específica para esse número. O limite2MiB de incorporação de texto é separado do limite por anexo: conteúdos maiores podem seguir como originais no ZIP, dentro dos15MiB. A nova preparação/deduplicação chama a mesma validação por anexo e não transforma2MiB num bloqueio de selecção.
### Proposta do primeiro lote fiscal delimitado — 21/10/2026
**Proposta para revisão, sem cadastro, publicação ou activação nesta passagem.** Um único percurso inicial de IVA geral sobre base líquida já identificada, em MZN, para operações internas abrangidas pela taxa geral. Janela de cobertura/ensaio do lote:01/01/2026–31/12/2026. Esta janela não cria uma data de entrada em vigor ou caducidade legal: os metadados das regras devem reproduzir as datas dos diplomas aplicáveis. Não criar artificialmente uma expiração em31/12/2026. A cobertura do primeiro lote é escolha de implementação proposta, não uma nova restrição organizacional da Tempo.
**Regra candidata, ainda não cadastrada:** `IVA-GERAL-BASE-LIQUIDA`, identificador provisório para a fórmula base tributável × taxa geral/100. A análise primária já existente identifica16% no art.17(1) da republicação da Lei22/2022, com efeitos desde01/01/2023, e documenta que a Lei10/2025 não altera esse artigo. Reutilizar essa conclusão e as citações/versões existentes, sem nova validação genérica da credibilidade da fonte. O lote não decide automaticamente se uma pessoa/operação está sujeita, isenta, reduzida ou excluída. A aplicação à operação exige um âmbito identificado e evidência/declaração de entrada rotulada; se o âmbito não estiver determinado, a saída é indeterminada. Um cálculo por fórmula com fonte verificada e âmbito apenas declarado deve ser identificado dessa forma, sem o apresentar como obrigação fiscal validada.
| Elemento | Existente e reutilizável | Trabalho do lote |
|---|---|---|
| Fonte da taxa | `source-archive/Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf`, art.17(1); análise em `primary-text-review-2025-2026.md` e adenda de taxa reconciliada em `fiscal-reform-2026-review.md`. | Recuperar exemplar/hash/edição/citação já registados e ligar ao cadastro. A taxa identificada não está tecnicamente cadastrada como parâmetro executável. |
| Cadeia para2026 | `source-archive/BR-248-I-Suplemento-1-2025.pdf`, alterações IVA/Lei10/2025 e vigência documentadas nas análises; aviso BR89/2017 e referências da consolidação disponíveis. | Aplicar as correspondências de âmbito/período que afectem esta regra. Não declarar que todos os binários do acervo foram analisados. Os diplomas/anexos sem correspondência continuam no inventário existente. Não reconstruir uma consolidação completa de todos os regimes para implementar apenas esta regra. |
| Parâmetros e entradas | Valor nominal16% já fundamentado no acervo; modelo aceita valor/unidade/condições; resolução temporal existente. | Cadastrar código/versão, unidade`percent`, condições de operação e vínculo documental. Determinar base tributável, âmbito concreto, período e moeda de entrada. Não presumir MZN nem base fiscal pela simples existência de um valor de planeamento. |
| Arredondamento e unidade de apuramento | O avaliador já suporta operações racionais e arredondamentos explícitos. | Mapear a política aplicável à liquidação sobre base líquida, a precisão e se o arredondamento incide por linha/documento/apuramento. Não transportar automaticamente a regra do art.34 para este percurso, nem assumir HALF_UP/duas casas como regra fiscal. Até esse ponto estar fundamentado, não publicar a regra nem apresentar um total fiscal final arredondado; conservar resultado exacto/intermédio e lacuna explícita. |
| Períodos/versões | O resolvedor verifica período, âmbito e compatibilidade da versão da fonte. | Preservar a vigência efectiva do diploma; limitar a cobertura demonstrada deste lote a2026. Pedido fora dessa cobertura identifica «fora da cobertura demonstrada do lote», sem afirmar que a lei deixou de vigorar ou usar zero como substituto. |
**Fora deste lote:** extracção de IVA de preço já incluído (art.34), taxas reduzidas, isenções/listas de bens, importações/serviços digitais/não residentes, dedução/crédito/reembolso, incentivos territoriais, ISPC, IRPS, IRPC, ICE, INSS, retenções e direitos aduaneiros. Estes domínios permanecem no acervo/registo existente e não são omitidos por ficarem fora desta primeira implementação. Não confundir consulta documental disponível com disponibilidade de cálculo.
**Cadastro e aplicação documental — estimativa4–8h.** Reutilizar as análises/fontes, preparar a ficha nominal da regra e parâmetro, transcrever vínculos/versionamento/condições e política de arredondamento aplicável, rever fronteiras temporais e casos de referência e cadastrar como rascunho apenas em QA pelo mecanismo existente quando a execução do lote for autorizada. O intervalo inclui a aplicação documental ao percurso delimitado; se a política ou o âmbito permanecerem ambíguos no acervo, registar a lacuna e parar apenas esse resultado, sem criar um valor. A espera por um elemento documental concreto não está incluída. As fontes oficiais continuam aceites pela decisão da Tempo; não se introduz novo fiscalista ou nova certificação genérica como condição.
**Desenvolvimento de código e aceitação técnica — estimativa12–20h.** Reutilizar `SimulationTaxRuleGovernance`, `SimulationTaxParameterResolver` e `SimulationTaxRuleExpression`; não reconstruir cadastro, transições, DSL ou resolvedor. Acrescentar a orquestração explícita do percurso fiscal: componente/período/âmbito→resolução existente→parâmetros e entradas→avaliação existente→resultado com explicação e proveniência. Separar o cenário fiscal do `custom_planning` de `RunSimulation`. Ligar o resultado ao dossier, histórico e exportadores existentes, com regra/versão/fonte/período/moeda/entradas/passos/arredondamento/estado de validação do âmbito. Acrescentar tratamento de lacunas e casos de referência dirigidos. Não se prevê um novo motor ou uma migration por princípio; confirmar necessidade a partir do contrato do resultado, sem alargar o schema preventivamente.
**Total de engenharia proposto16–28h para este lote**, sem dupla contagem entre cadastro e código e sem incluir espera, revisão consolidada, activação no destino ou implementação fiscal integral. Pressupõe que o acervo disponível permita fundamentar âmbito e arredondamento deste percurso e que os mecanismos existentes continuem compatíveis. Divergência concreta é relatada antes de alargar o lote. As1–2h anteriores pertenciam exclusivamente ao recorte de exportação já concluído.
Critérios de aceitação deste primeiro lote:
1. Ficha e parâmetro correspondem ao exemplar/artigo/versão/hash e vigência aplicáveis; condições e arredondamento têm fundamento identificado. Não publicar enquanto faltarem estes elementos. O cadastro em rascunho não é activação.
2. No clone QA isolado, resolver a regra para contexto/período cobertos e recusar aplicação ambígua ou fora de cobertura, com lacuna expressa, sem zero/taxa de outra versão como substituto. Transição dentro de um período e fonte incompatível mantêm o comportamento temporal já existente.
3. Casos independentes sobre bases que gerem resultados exactos e casos fraccionários/fronteiras da política de arredondamento correspondem à regra cadastrada; unidades/moeda não são inferidas. Zero explícito e ausência de valor continuam distintos.
4. O resultado fiscal identifica regra/versão/fonte/artigo/período, entradas, passos, unidade/moeda, arredondamento e âmbito comprovado ou apenas declarado. A expressão «verificado» não se estende a uma operação cuja sujeição foi apenas declarada. Planeamento manual permanece disponível e claramente distinto.
5. Guardar/recuperar o resultado no dossier e numa nova sessão mantém o snapshot original; mudar regra/entrada não reescreve o histórico. Outra conta não obtém entradas ou resultados privados. Reutilizar os mecanismos e provas de isolamento, ensaiando apenas o novo consumidor fiscal.
6. Ecrã e relatório apresentam o mesmo resultado/proveniência/lacunas e distinguem o cenário fiscal de estimativas do utilizador. Reutilizar o exportador e a prova aceite de paginação; conferir só as novas linhas fiscais e os deltas de layout se existirem, sem repetir os cálculos de planeamento/CSV aceites.
7. Publicação de regra, exposição do percurso e eventual disponibilização no destino são acções separadas, pelos mecanismos e papéis existentes definidos pela Tempo. Nenhum parâmetro é activado por importação de documento ou integração deste documento de proposta em Git.
Esta proposta fica ligada a AUD-SIM-INT02 na matriz existente. Não cria uma matriz ou backlog alternativos. A apresentação da proposta não autoriza, por si, a integração de13f68f1b em`main`.
### Actualização posterior — integração autorizada e ficha fiscal v1
A Tempo autorizou posteriormente a integração de `13f68f1b`. O commit foi integrado em `main` e a revisão servida na homologação confirmou esse identificador em21/10/2026. As menções anteriores a integração pendente descrevem o estado histórico da candidata. A entrega continua identificada como melhorias de planeamento financeiro, recuperação, exportação e idioma, sem encerrar a implementação fiscal.
A [ficha documental IVA-GERAL-BASE-LIQUIDA v1](../simulations/ficha-IVA-GERAL-BASE-LIQUIDA-v1.md) desenvolve separadamente a proposta `0fafd348`: fundamento da taxa e período, limites do art.34, exemplar digitalizado recuperado do inventário, parâmetros por cadastrar, lacunas de enquadramento/arredondamento, integração e critérios de aceitação. É rascunho documental sem cadastro ou activação. AUD-SIM-INT02 mantém-se parcial; os outros sete saldos aceites não são reabertos. Não foram repetidas provas aceites nem alterados cálculos, dados ou regras. A branch fiscal permanece separada de `main`.
**Adenda fiscal v1.1 — 21/10/2026:** a mesma ficha regista a leitura visual dirigida do CIVA/Lei13/2016, arts.1–4,6–7,15,17 e27, e a consulta do regulamento/Decreto7/2008. Enquadramento geral da base documentado, com composição, exclusões, localização e data de exigibilidade; não constitui classificação automática de operações. O arredondamento do art.34 refere-se à extracção de base com IVA incluído e o art.9(1)(b) do regulamento ao meio de pagamento cheque/vale; nenhum foi convertido em regra geral de arredondamento sobre base líquida. Permanecem por esclarecer precisão/modo/empates/agregação e a correspondência regulamentar pertinente para2026. A ficha identifica a questão documental concreta e exemplos aritméticos, sem inventar norma. Estimativa restante condicional: cadastro/aplicação documental3–6h + código/aceitação12–20h =15–26h, excluindo espera, revisão e activação. Fontes locais arquivadas com hash coincidente; não se atribui conferência ao utilizador. Nenhuma regra activada; AUD-SIM-INT02 continua parcial e a entrega13f68f1b mantém-se independente.
### Decisão posterior da Tempo e execução fiscal QA v1.2 — 21/10/2026
**Estado actual; substitui a condição bloqueante e a estimativa da adenda v1.1.** A Tempo determinou prosseguir neste lote sem arredondamento automático, preservando precisão decimal e distinguindo valor calculado de apresentação monetária. A ausência de uma política genérica de arredondamento deixa de bloquear o cálculo do IVA sobre base líquida. Isto não é regra universal para todos os impostos. Exigências de precisão de um formato de pagamento/declaração serão identificadas nesse consumidor, sem as generalizar. A ficha existente foi consolidada emv1.2; versões anteriores permanecem emGit. Não há questão adicional à Tempo como pré-condição deste cálculo nem validação jurídica atribuída ao utilizador.
**Cadastro:** artefacto `database/qa/iva-general-net-v1.json`; comando explícito `fiscal:qa-register-iva {actor} {source}`, com guarda da base QA isolada, papel existente de editor e vínculo a fonte cadastrada. Confere SHA256 do exemplar e usa a governação existente para criar apenas `draft`. Repetição conserva o mesmoID/versão; divergência não sobrescreve o cadastro. Não corre em deploy, importação ou migration. O clone persistido é `simulator_fiscal_delta_20261021_test`,127.0.0.1:55441. A referência de fonte e os actores usados no ensaio são fixtures sintéticas identificadas; não constituem revisão jurídica por pessoa real nem atribuição persistente de papéis organizacionais.
**Cálculo e proveniência:** `SimulationFiscalQaPreview` usa o resolvedor temporal e avaliador existentes, com percurso explícito de rascunho QA que não altera a elegibilidade da API de parâmetros publicados. O resultado guarda contexto declarado, fonte/versão/hash, regra/versão/DSL, passos racionais e decimal exacto; apresentação guarda valor e moeda separadamente, com `rounding_applied=false`. Não há float, truncagem, `toFixed` ou nó`round` neste percurso. Exemplos dirigidos:0,03×16%=0,0048;9007199254740993,0003×16%=1441151880758558,880048; zero explícito conserva0. Fonte/versão incompatível e fora da cobertura2026 continuam indeterminados, sem taxa substituta/zero. A data declarada de exigibilidade alimenta a resolução, sem expiração legal artificial em31/12/2026.
**Histórico e relatórios:** armazenamento existente de simulações, fontes de resultado e relatórios; sem schema novo. Entradas declaradas e proveniência da regra são fontes separadas. Snapshot/relatório não são reavaliados quando a regra é editada. Nova execução usa novo registo; a mesma chave e parâmetros reutiliza a execução. O ensaio persistido actual tem simulação`842e05f9-2b6e-4853-8ff5-81650633f6c4`, relatório`49cac0bd-f49c-4613-a8f0-1ee5c285cf81`, hash`8ed8155a56fdf98ad26e2f01435644e062f27ca6b067ce3553415e974261c687`; resultado/relatório iguais e relatório anterior conservado. Zero regras publicadas. Ecrã e linhas do dossier/CSV distinguem cálculo exacto QA, contexto declarado, estimativas do utilizador e saldo a pagar, que não é calculado neste lote.
**Verificação deste incremento:**5testes feature/51asserções;7testes do resolvedor/25asserções;3testes frontend;TypeScript,build ePint aprovados. Os testes feature correm com transacções no clone dedicado `simulator_fiscal_checks_20261021_test`, separado do cadastro persistido. A primeira tentativa de base vazia encontrou uma migration antiga que exige instalação nominal; não se contornou essa guarda nem se alterou o activo: usou-se cópia do schema QA existente e transacções, sem novas migrations. Cobertura inclui não publicação, indeterminação efectiva, idempotência, precisão fraccionária/grande, moeda obrigatória, recusa de nó de arredondamento, recusa foraQA, snapshot após edição, recuperação por nova autenticação API e recusa por outra conta.
Conferência visual dirigida no Edge do componente React real renderizado emSSR e do HTML produzido por `createProjectDossierPrintHtml` apenas para as novas linhas fiscais:0.0048, base0.03, taxa16, MZN e rótulos legíveis, sem corte visível. O invólucro de inspecção suprimiu apenas a abertura automática da janela de impressão numa cópia privada do HTML; não alterou o exportador. Não é prova de novo PDF nativo guardado nem novo percurso browser de histórico no formulário completo. API/histórico/relatório imutável e os modelos usados pelo ecrã/CSV/dossier têm provas dirigidas. A conta técnica QA ultrapassaRLS; recusas demonstram política da aplicação, não nova certificaçãoRLS. Provas aceites das10páginas, valores, CSV e recuperação/exportações de13f68f1b reutilizadas, sem repetição.
Provas privadas em`outputs/simulator-continuation-20261020/`: `fiscal-qa-receipt-v2.json`, `fiscal-qa-tests.json`, `fiscal-qa-build.log`, `fiscal-qa-snapshot.json`, `fiscal-component-review.html` e `fiscal-report-review.html`. Página/servidor temporários de inspecção encerrados. O recibo versionado desta continuação recebe a mesma reconciliação; a matriz existente recebe apenas adenda emAUD-SIM-INT02. Não se cria backlog paralelo.
**Saldo e estimativa:** o recorte agora autorizado — cadastro versionado, precisão/proveniência, resolução temporal QA, histórico e dados de relatório — está implementado e verificado nos limites acima;0h de engenharia identificada restante nesse recorte, revisão não incluída. As15–26h eram estimativa condicional anterior que incluía trabalho agora executado, não horas medidas nem estimativa ainda integralmente por consumir. AUD-SIM-INT02 permanece parcial para disponibilização fiscal de produto e cobertura integral; os outros sete itens não são reabertos. Pagamento, declaração, classificação automática e outros impostos exigem âmbito/estimativa próprios. Não declarar o simulador fiscal concluído.
**Integração:** commit desta candidata fica apenas na branch `codex/simulator-integrated-review`, descendente de0fafd348, sem push/merge emmain. A autorização anterior de integração de13f68f1b não é reutilizada para esta candidata fiscal. Após revisão consolidada, uma decisão específica de integração continua separada de cadastro/publicação de regras. Esta preparação não muda o destino, papéis activos ou dados activos. Não retirar a guarda QA ou mudar o ambiente do activo para contornar a separação; preparar o consumidor de produto e a sua disponibilização apenas no âmbito que vier a ser autorizado.
### Revisão consolidada de83d2f97d — âmbito, demonstração e operação
**Pedido da Tempo:** consolidar a revisão, reutilizando provas aceites e demonstrando apenas o percurso completo pela interface que faltava. O exemplo aritmético não prova incidência nem aplicabilidade a qualquer operação; a Tempo não conferiu o ficheiro local nem validou juridicamente as conclusões transcritas. Esta revisão mantém essa distinção.
| Elemento da regra | Âmbito documentado / fronteira |
|---|---|
| Operação e base | Operação interna onerosa por sujeito passivo nessa qualidade, localização aplicável e base líquida já apurada: CIVA arts.1(1)(a),2,6 e15. Condições gerais e composição da base estão na ficha v1.2, com PDF/página/artigo. O código não classifica automaticamente uma pessoa ou operação. |
| Taxa e período |16% no art.17(1) da Lei22/2022 republicada, eficácia01/01/2023; data de exigibilidade relevante nos arts.7 e17(2). Lei10/2025 efectiva01/01/2026 não altera art.17 na cadeia lida. A janela demonstrada do lote é2026; não é caducidade legal em31/12/2026. Isto é conclusão documental nos limites da cadeia consultada, não validação jurídica atribuída ao utilizador. |
| Excluído do primeiro lote | Preço já com imposto incluído/art.34, taxas reduzidas, isenções/listas, bases especiais, importações, digital/não residentes, deduções, crédito/reembolso, incentivos e outros impostos. Não se transforma base orçamental em base tributável. |
| Indeterminado | Operação sem enquadramento identificado; fonte/versão/parâmetro/contexto incompatível ou período fora da cobertura demonstrada. O enquadramento apenas declarado permanece declarado. Ausência de arredondamento automático não torna indeterminado o valor exacto deste percurso. |
| Valor versus apresentação | Decimal exacto guardado e reproduzido como texto, sem float, truncagem ou arredondamento implícito; moeda separada. Não é valor a pagar ou saldo de declaração. Precisão de um formato de pagamento/declaração será determinada nesse contexto, sem regra universal. |
**Lacuna concreta encontrada na revisão:**83d2f97d disponibilizava API QA e componente/relatório, mas não um ecrã de introdução das entradas que permitisse observar a sequência inteira. Acrescentou-se apenas a página protegida `/management/investment/tax-rules/{id}/qa-review`, exclusiva do alvo QA isolado, sob os mesmos controlos administrativos, autenticação/MFA e consulta de governação. Chama a API QA já existente, grava no histórico existente, recupera sem recalcular e lê o relatório guardado pelo seuID/hash. Não cria motor, schema, fonte ou regra nova e não expõe um cálculo fiscal público.
**Exemplo completo efectivamente observado pela interface:** base`1000.03`, moedaMZN seleccionada explicitamente, exigibilidade declarada`2026-10-21` e declaração de operação interna à taxa geral sem excepção. Valor calculado`160.0048`, sem arredondamento; não prova de sujeição. Sequência observada: preencher → «Calcular e gravar no histórico QA» → execução`fa6d17f2-5f47-4cb6-b8cb-40850c12505f` concluída → recarregar a página, limpando o estado local/entradas → «Carregar histórico» → «Recuperar [mesmoID]» → «Consultar relatório guardado» → mesmo valor, base, moeda, data, regra/versão, fonte e contexto declarado. Relatório`d13e2ad5-3ba8-4d01-9d30-5815496da774`, SHA256`2e5e260f47ee5c849a34453fad248009e607ef8679dc56b7a1c58730f13e5c3b`, igual ao hash do resultado guardado. Cálculo e gravação pertencem à mesma operação transaccional existente; não se inventa um segundo botão de gravação. A conta de ensaio ficou com uma única execução persistida.
O ensaio correu no Edge, no clone`simulator_fiscal_delta_20261021_test`, porta local18579, com conta sintética própria e sessão/cookie separados. Durante a primeira autenticação a ligação da ferramenta perdeu-se. As duas reactivações comunicadas pelo utilizador não repuseram imediatamente navegadores no inventário, mesmo após reiniciar a sessão da ferramenta. **Limitação da ferramenta; não falha do simulador.** Mais tarde o inventário voltou a expor o Edge e retomou-se apenas o que faltava, por interacção DOM documentada. Foram observados início de sessão e segundo factor normais, entradas, cálculo/gravação, recarga, recuperação e relatório acima. A primeira edição de data não estava consolidada no controlo; a recusa não gerou execução no histórico. Conferiu-se histórico vazio antes da retoma, concluiu-se a edição da data por teclado e submeteu-se uma única execução concluída. Nenhuma guarda ou autenticação foi contornada. As provas anteriores de83d2f97d são reutilizadas; esta demonstração fecha somente a lacuna browser que faltava.
O novo ecrã passouTypeScript/build e1teste dirigido de rota/17asserções: redireccionamento para autenticação, página/props correctos para perfil autorizado, recusa de outro perfil e recusa foraQA. Mensagens de falha são portuguesas e o detalhe técnico fica na consola interna. O primeiro ensaio de rota antecedeu o build da página nova e encontrou ausência da entrada no manifest; após o build o teste passou. Não se reabriram os15testes aceites de83d2f97d, nem valores/CSV/paginação anteriores. **A única lacuna adicional desta revisão ficou demonstrada.** Provas privadas: `fiscal-ui-journey-receipt-20261021.json`, `fiscal-ui-complete-20261021.txt`, `fiscal-ui-complete-20261021.jpg` e `fiscal-ui-report-20261021.jpg`, na mesma pasta deoutputs. Conferência da BD somente por leitura para o novo exemplo: uma execução da conta, igualdade de hashes, regra emdraft, zero regras publicadas. Esta prova é de relatório guardado na interface; não se alega nova impressão/PDF nativo ou nova sessão browser, que não foram pedidos/repetidos.
#### Procedimento de integração — decisão de código
1. A Tempo revê83d2f97d e o delta desta revisão, identificando o commit exacto que autoriza integrar. A autorização anterior de13f68f1b não cobre estes commits. Esta consolidação inclui a demonstração browser concluída e as limitações expressas, sem a converter em validação de aplicabilidade fiscal.
2. Conferir a ancestralidade e o delta contra`origin/main`; a consulta remota desta revisão confirmoumain em13f68f1b. Se continuar ancestral da candidata, integrar por avanço directo após autorização; se houver alterações posteriores, resolver apenas os conflitos reais e verificar os deltas afectados. Não reexecutar o conjunto aceite sem motivo concreto.
3. Enviar a referência autorizada a`main` desencadeia disponibilização automática na homologação. Conferir a revisão efectivamente servida e a disponibilidade do portal. Não executar cadastro fiscal, transição de regra ou migration por fazer merge do código. Este delta não tem migration.
4. Guardar os identificadores da decisão, commit, build servido e resultado da disponibilização no mesmo registo. Para reversão de código, preservar o contrato de leitura dos snapshots e históricos; não eliminar tabelas/registos nem reescrever resultados antigos. As guardasQA permanecem activas neste consumidor.
#### Publicação e activação — decisões distintas da integração
1. Cadastro no destino exige autorização específica para escrita, backup/restauro confirmado e acção autenticada. Usar a API de governação existente com a referência real da fonte aceite e actores designados; não copiar as fixtures sintéticas QA para o destino nem mudar o ambiente do activo para executar o comandoQA.
2. A governação existente usa `POST /api/v2/investment/admin/tax-rules/{id}/{action}`, com`version_no` actual e`evidence`: `submit`→`review`→`approve`→`publish`, pelos papéis já configurados. As verificações actuais de autoria/revisão/aprovação/publicação, versão da fonte, vigência e sobreposição conservam-se; não se introduz nesta revisão um novo papel ou uma certificação genérica da fonte. Corrigir uma versão após revisão faz-se por nova versão e histórico, sem edição retroactiva de uma publicada.
3. **Efeito real da publicação:** a versão passa a ser elegível no resolvedor/API de parâmetros publicados. Isso pode disponibilizar o parâmetro aos consumidores dessa API; não é um simples rótulo documental. Ainda assim, não liga por si este cálculo QA ao motor público nem valida incidência de uma operação.
4. **Limite concreto de83d2f97d:** o consumidor novo é QA, exige regra em`draft` e guarda de base isolada. O job público de planeamento não consome automaticamente uma regra publicada como cálculo fiscal. Portanto, não há neste commit um interruptor de activação do lote em produção. Uma decisão futura de activação deve identificar o consumidor de produto, regra/versão, âmbito/período, comportamento de indeterminação e data de exposição. Não remover a guardaQA nem publicar a regra para simular que esse consumidor já existe. A publicação de documento, o cadastro, o merge e a activação não se confundem.
#### Desactivação e preservação histórica
- Para uma regra já publicada, a transição autenticada`.../{id}/suspend`, com versão corrente e evidência, altera o estado para`suspended`. O resolvedor existente selecciona apenas`published`, pelo que deixa de escolher essa versão para novas resoluções. Arquivo posterior usa`archive` quando aplicável. Não se ensaiaram estas transições sobre a regra real deste lote, que continua emrascunho; reutiliza-se o comportamento documentado/testado da governação existente.
- Suspender não apaga snapshots, relatórios, fontes ou eventos. A consulta de uma execução antiga lê o snapshot preservado, não volta a resolver com a taxa actual. Uma correcção ou substituição gera nova versão/execução com proveniência própria; nunca substitui silenciosamente o resultado antigo.
- Não existe transição genérica de`suspended` para`published` no fluxo actual. Uma retoma deve seguir uma versão elegível e as transições previstas, sem actualização directa de estado porSQL.
- Para parar este ensaio QA de rascunho basta encerrar o servidor de ensaio ou retirar a sua exposição no ambienteQA; não se publica/suspende artificialmente o rascunho para o desligar. Os registos persistidos mantêm-se para consulta. Qualquer futuro consumidor público deve definir a sua desactivação de novas execuções preservando essa leitura histórica.
**Saldo:**83d2f97d conserva o fecho técnico QA aceite; revisão consolidada com âmbito, exclusões, procedimento e percurso de interface demonstrados no recorte definido. Não há preparação técnica adicional identificada para fechar esta revisão; decisões de integração e publicação/activação permanecem distintas. AUD-SIM-INT02 continua parcial para produto fiscal integral; não declarar o simulador fiscal concluído. Sem merge, publicação, activação ou alteração do activo.13f68f1b mantém-se independente.
### Proposta única — consumidor IVA no simulador da homologação (21/10/2026)
**Conclusão da leitura do código em d186f942:** falta um delta de produto delimitado; integrar apenas83d2f97d/d186f942 e publicar uma regra não disponibiliza o percurso fiscal ao utilizador do simulador. A revisão QA anterior permanece concluída. Esta proposta identifica trabalho novo de ligação ao produto, sem reabrir as provas aceites, executar integração ou activar regras.
**Funcionalidade proposta:** na etapa fiscal do simulador existente, escolher «IVA geral sobre base líquida», indicar base tributável decimal, MZN, data de exigibilidade e declarar o contexto de operação interna à taxa geral sem excepções. Calcular e gravar pela execução existente; recuperar a execução e consultar/exportar o seu relatório preservado. Mostrar versão/fonte, contexto declarado e valor exacto separado da apresentação monetária. Não usar automaticamente a base orçamental nem a taxa introduzida no planeamento. A consulta do histórico mantém os requisitos actuais de conta e correio verificado; a execução/consulta individual mantém os controlos actuais de proprietário, organização ou token anónimo, conforme o percurso já suportado, sem prometer histórico de conta ao anónimo.
#### Delta exacto de código a implementar na branch separada
| Ponto actual verificado | Alteração delimitada / reutilização |
|---|---|
| `simulation-form.tsx::runSimulation` termina quando o modo não é `custom_planning`; a consulta de parâmetros não calcula. | Ligar o consumidor IVA à mesma etapa, validação, recuperação de falhas, idempotência, acompanhamento e histórico. Expor disponibilidade vinda do servidor; manter o planeamento personalizado identificado separadamente. |
| `StoreSimulationRequest`/`SimulationController::store` recebem base/taxa/redução. | Acrescentar contrato discriminado para o consumidor, com base decimal textual, moeda, data e contexto declarado. Reutilizar autorização de organização, identidade/token e limites existentes. O cliente não escolhe taxa, definição ou fonte executável; pedidos fora do contrato são recusados com mensagem clara e entradas preservadas. |
| `SimulationFiscalQaPreview`, `createFiscalQaPreview` e ramo de `RunSimulation` exigem QA isolado e regra emdraft. | Criar adaptador para regra publicada, reutilizando o resolvedor temporal e avaliador existentes e o núcleo decimal exacto. Conservar as guardas e o percurso QA. Resolver no servidor, conferir a versão autorizada e congelar regra/definição/fonte/parâmetro/contexto no snapshot; gravar através da mesma transacção, fila, idempotência e histórico. Não aceitar snapshot fiscal fornecido pelo cliente. |
| `resolvePublishedForPeriod` existe, mas a resposta de parâmetro não fornece um snapshot executável completo. | Recuperar no servidor a versão seleccionada pelo resolvedor e a definição correspondente, verificando elegibilidade publicada e correspondência temporal no mesmo limite transaccional da admissão. Não reconstruir o resolvedor nem inferir a regra pela taxa mais recente. |
| Não foi identificado interruptor deste consumidor nos serviços/configuração fiscal. | Acrescentar configuração específica, desligada por omissão, com consumidor, ID/versão autorizada e cobertura2026. Aplicar a verificação no servidor e expor o estado ao formulário. É escolha técnica proposta para executar as decisões separadas da Tempo, não novo papel organizacional. Não reutilizar o interruptor de outro módulo nem mudarAPP_ENV/guardasQA. |
| `fiscal-qa-preview.ts`, CSV e dossier reconhecem apenas `fiscal_rule_qa_preview`; rótulos dizemQA/draft. | Suportar explicitamente o novo tipo de snapshot publicado nos mesmos componentes/relatórios/exportações, preservando rótulos e leitura dos snapshots QA antigos. Garantir valor exacto textual e proveniência; nenhuma migração de resultados históricos ou arredondamento monetário silencioso. |
Modelos, políticas, governação, tabelas de execução/relatório e contrato genérico de resultados já existentes são reutilizados. Não se identifica necessidade de nova tabela fiscal ou migration para esta ligação; configuração e novo discriminador de cenário são o delta proposto. Novas entradas geram nova execução, sem converter uma execução fiscal antiga num cenário percentual editável.
#### As três operações da mesma proposta
1. **Integração do código.** Implementar o delta acima na branch fiscal, incluindo leitura histórica compatível e controlo desligado. A candidata a integrar terá de incluir esse delta: d186f942, isoladamente, ainda é QA. Após revisão da referência exacta pela Tempo, integrar emmain e confirmar a revisão servida pela disponibilização automática da homologação. Não cadastrar/publicar regra no arranque nem nas migrations. Resultado observável: código instalado, consumidor indisponível para novas execuções enquanto desligado; restantes percursos continuam disponíveis. Reversão por código compatível com os snapshots existentes, sem apagar dados.
2. **Publicação da versão da regra.** Com autorização de escrita no destino, backup/restauro confirmado e actores autenticados já designados, cadastrar a versão com a fonte real aceite, citações e metadados da ficha; não copiarUUIDs, utilizadores ou estados artificiais do QA. Usar os endpoints existentes de cadastro e `.../tax-rules/{id}/{action}`, com `version_no` e `evidence`, na sequência `submit` → `review` → `approve` → `publish`, pelos papéis existentes. Registar o ID/versão real publicado. Publicar torna o parâmetro elegível na API existente, inclusive para quem já a consulte; não habilita o novo consumidor. Não exigir nova certificação genérica da fonte oficial.
3. **Activação do consumidor.** Por decisão específica da Tempo, configurar a ligação ao ID/versão publicado autorizado e cobertura2026, habilitar o consumidor na homologação e aplicar a configuração efectiva do servidor. Confirmar estado disponível na etapa fiscal e executar somente a prova da ligação nova: entrada no simulador normal → versão publicada correcta → gravação → recuperação/relatório, mais recusa fora do âmbito e desligamento. Reutilizar as provas aceites de aritmética decimal, CSV, recuperação e paginação. Registar decisão, commit/build, regra/versão, configuração e momento da activação. Resultado entregue à Tempo: funcionalidade utilizável no portal, com esses limites; não apenas código instalado.
Estas operações são autorizáveis separadamente dentro desta única proposta. A autorização de13f68f1b não autoriza esta integração fiscal, publicação ou activação. Não foram executadas nesta análise. A sequência é código compatível desligado → versão publicada → consumidor ligado; não se atribui activação à mera publicação.
#### Âmbito, indeterminação e suspensão
Reutiliza-se integralmente o âmbito fundamentado da ficha v1.2: IVA geral16% sobre base líquida já identificada de operação interna, MZN, exigibilidade na cobertura demonstrada01/01/2026–31/12/2026. A taxa tem eficácia documental desde01/01/2023; o fim da cobertura2026 é limite do primeiro consumidor, não revogação/caducidade da norma. Enquadramento declarado é mostrado como declarado; o cálculo não certifica incidência. Fonte/versão/parâmetro/contexto insuficiente, conflito temporal ou falta de enquadramento produzem resultado indeterminado, nunca zero/taxa actual como substituto. Fora da cobertura ou consumidor desligado, explicar a indisponibilidade para nova execução e preservar as entradas.
Excluem-se preço com IVA incluído, taxas reduzidas, isenções/listas, bases especiais, importações, digital/não residentes, deduções/crédito/reembolso, incentivos, apuramento/declaração/pagamento e outros impostos. Não ampliar cobertura nem presumir arredondamento. Valor calculado exacto e apresentação monetária permanecem distintos.
**Suspensão operacional proposta:** desligar a configuração impede novas admissões pelo servidor e informa o formulário, sem bloquear recuperação, relatórios ou exportação de execuções já guardadas. **Suspensão da regra existente:** acção autenticada `suspend`, versão corrente e evidência; o resolvedor deixa de a seleccionar. Sem troca silenciosa por outra versão. A admissão deve fixar atomicamente a elegibilidade e o snapshot; execuções já admitidas conservam essa versão e podem concluir, identificadas pelo momento de admissão, mesmo se a fila terminar depois da suspensão. Novos pedidos ficam impedidos; repetição idempotente de pedido já admitido recupera a mesma execução. Este contrato de concorrência é escolha técnica explícita desta proposta, a verificar no delta, não comportamento já demonstrado pelo código QA.
Históricos não são recalculados, apagados ou renomeados como se tivessem usado a versão nova. A suspensão fica no registo de governação, sem alterar retroactivamente o conteúdo/hash do relatório. Retoma exige versão publicada elegível e ligação/activação autorizada; não existe transição directa de suspended parapublished no fluxo actual. Não eliminar a leitura de snapshots fiscais ao reverter código.
**Verificação nova estritamente necessária:** contrato público e permissões existentes; impossibilidade de executar desligado ou com versão não publicada; correspondência temporal/âmbito e precisão na ligação; cenário publicado na interface/histórico/relatório/exportação; suspensão versus admissão concorrente e repetição sem duplicação; preservação da leitura histórica e do modo personalizado. Não repetir as10páginas, valores/CSV ou ensaios QA aceites. Não se cria nova matriz/backlog; AUD-SIM-INT02 conserva o estado parcial do simulador fiscal integral e referencia esta delimitação. As fontes e provas existentes sustentam a proposta; não se atribui ao utilizador conferência local ou validação jurídica.
### Consumidor publicado implementado no simulador normal — fecho QA de21/10/2026
**Autorização:** instrução expressa da Tempo para implementar2ced7b79 em QA, sem nova proposta, integração emmain, publicação de regra real ou activação na homologação. O delta descrito acima está agora implementado. O fecho é da ligação deste único consumidor; mantém âmbito/exclusões da ficha v1.2 e não encerra o simulador fiscal integral.
**Percurso utilizável:** `/simulador-fiscal` → percurso habitual → etapa «Operação fiscal» → «IVA geral sobre base líquida» → base decimal textual, moeda explicitamente seleccionada, exigibilidade e declaração de contexto → «Calcular IVA e gravar». O resultado aparece no próprio simulador e no histórico existente. Pode recuperar-se sem recalcular; relatório, referência/hash e exportações usam o mesmo snapshot. Não é necessário acesso à administração fiscal para calcular. A consulta de parâmetros e o planeamento personalizado conservam os seus percursos e rótulos próprios. A base de orçamento e a taxa manual não são usadas como entradas implícitas deste IVA.
#### Implementação e fronteiras efectivas
- `StoreSimulationRequest` e `SimulationController::store` distinguem `fiscal_rule_published` do planeamento. O servidor recebe apenas entradas declaradas, não taxa/regra/versão/snapshot executável fornecidos pelo cliente; valida formato decimal textual, MZN, data e contexto. Falta de declaração de enquadramento grava indeterminação com valor nulo, nunca zero. Campos inválidos/período fora2026 são recusados com mensagens portuguesas e entradas preservadas.
- `SimulationPublishedFiscal` selecciona o ID/versão configurado no servidor, exige estado publicado e âmbito do lote, reutiliza `SimulationTaxParameterResolver` e `SimulationTaxRuleExpression`, e congela a proveniência antes da fila. `SimulationFiscalSnapshot` partilha a representação decimal com o adaptador QA, cujas guardas não foram removidas. Não há regra fiscal activada por importação ou arranque.
- `SimulationExecution` consulta a chave idempotente antes de verificar nova admissão. Assim, desligamento/suspensão não impedem recuperar o mesmo pedido já admitido. Nova chave exige consumidor ligado e versão publicada elegível; mudar entradas com a mesma chave dá conflito. A interface conserva a chave do pedido fiscal concluído para que repetir o mesmo cálculo não crie uma execução adicional. Novas entradas produzem nova chave/execução.
- Bloqueios consultivos transaccionais partilhados na admissão e exclusivos nas transições de governação/fonte serializam suspensão/revogação e admissão. Não se usa `SELECT FOR SHARE` na regra pública: poderia exigir uma política de alteração, contrariando as permissões existentes. A decisão de usar bloqueios consultivos é escolha de implementação, não nova exigência da Tempo. Não se alteraram políticas/grants nem se acrescentaram papéis de produto.
- `RunSimulation` processa o snapshot fiscal já admitido, sem resolver outra taxa/versão e sem reconsultar a activação. Guarda resultado, relatório e proveniência nos modelos existentes. Notas internas de revisão e identificadores dos funcionários ficam fora da proveniência devolvida pelo novo consumidor. Snapshots QA antigos conservam os seus rótulos/estado originais.
- O leitor de resultados do simulador foi adaptado ao contrato fiscal: o leitor aritmético anterior marcava o novo snapshot como inconsistente e desactivava exportação/conversão. Agora reconhece valor fiscal e indeterminação coerentes, e recusa divergência entre valor calculado e apresentação. CSV e dossier encaminham o novo tipo pelos componentes existentes; não se reescreveram os exportadores nem se repetiram provas aceites de paginação/valores.
- Configuração nova e desligada por omissão: `INVESTMOZ_IVA_CONSUMER_ENABLED=false`, `INVESTMOZ_IVA_CONSUMER_RULE_ID=` e `INVESTMOZ_IVA_CONSUMER_RULE_VERSION=0`, documentadas em `.env.example`. `/api/v1/fiscal-consumer` informa a disponibilidade sem cache; o servidor continua a exigir autorização na execução, independentemente do estado previamente visto no ecrã. A interface actualiza disponibilidade após uma tentativa, preservando entradas quando encontra suspensão. Configuração incompleta não escolhe outra regra automaticamente.
#### Provas novas, sem reabrir o conjunto aceite
Oito testes dirigidos deAPI/serviço,75asserções, passaram no clone isolado `simulator_fiscal_checks_20261021_test`, com transacções revertidas: ligação publicada, identidade/propriedade, token anónimo, idempotência/conflito, histórico/relatório/hash, contexto/fonte insuficientes, contrato/cobertura, suspensão/desligamento e trabalhador congelado, planeamento ainda disponível, configuração inválida e bloqueios de admissão. Um papel PostgreSQL temporário sem superutilizador/bypassRLS comprovou execução com a política pública de leitura, sem acesso aos eventos da governação ou alteração da regra; papel/grants de ensaio foram revertidos com a transacção. Uma segunda ligação comprovou que os bloqueios exclusivos da regra/fonte não atravessam a transacção de admissão. Dois testes frontend dirigidos ao novo tipo publicado passaram: encaminhamento coerente para ecrã/CSV/dossier e aceitação da indeterminação/recusa da apresentação divergente. TypeScript, build ePint passaram. Aviso do build sobre optimização opcional de fontes/fontaine não impediu a compilação.
**Observação no Edge, formulário normal:** porta18580, clone `simulator_fiscal_delta_20261021_test`, conta sintética de utilizador comum167, sem papéis fiscais administrativos. A fixture publicada é a versão sintética2 de `IVA-GERAL-BASE-LIQUIDA`, ID`01a15ddc-b0c5-727f-bb24-d806a69bd1f5`, para não substituir o rascunho QA anterior; fonte/actores sintéticos só neste clone. A governação existente foi usada na preparação da fixture. Não houve publicação na homologação nem declaração de aprovação jurídica da regra real.
1. Observou-se inicialmente o consumidor desligado e o botão indisponível. Habilitou-se somente a fixture local guardada contra qualquer outra base.
2. Base1000.03MZN, exigibilidade2026-10-21, sem declaração de contexto: execução`53663785-2e5d-4be2-8a05-c0c96a57896d`, resultado indeterminado, entradas conservadas e relatório guardado. A mesma base/data/moeda com declaração criou`5fa778f1-413e-4643-8c67-1b93ab290da7`, valor160.0048. Repetir o botão manteve essas duas execuções, sem terceira duplicada.
3. Reteve-se somente a fila QA dedicada `fiscal-consumer-qa-20261021`. A base1001.03 admitiu`be1e8399-ea95-4aa7-9ac0-7fa392932ac1` às09:52:11.782758UTC. Suspendeu-se a fixture pela governação às09:52:16UTC, ainda com essa execução emqueued e snapshot publicado congelado. O trabalhador real `queue:work database --queue=fiscal-consumer-qa-20261021 --once --tries=1 --timeout=30` concluiu-a depois da suspensão, com160.1648 e a mesma versão2. Nenhuma outra fila foi consumida.
4. Nova entrada1002.03 foi recusada: «A versão autorizada de IVA não está disponível para novas execuções. Os históricos foram preservados.» A base1002.03, data, moeda e declaração continuaram no ecrã; não houve quarta execução. Após recarga completa, recuperaram-se pelo histórico tanto o resultado calculado como o indeterminado. Ambos mostraram o snapshot original, sem alertas de inconsistência, com CSV disponível. O ficheiro não foi descarregado novamente; a implementação e as provas aceites do exportador foram reutilizadas.
5. Conferência por leitura: exactamente3execuções da conta, todascompleted; hashes de resultado/relatório iguais nas três; zero trabalhos pendentes na fila dedicada. O relatório da execução que concluiu após suspensão é`5e416a48-db07-449b-8eae-aa51a1538f04`, SHA256`997f026665e586c662ae574fd2ff7f62e0254819adb26aea920575e47e0490a3`. A fixture permanece suspensa e a configuração local foi reposta emdesligado; os históricos ficam acessíveis.
Provas privadas em `outputs/simulator-continuation-20261020`: `published-consumer-tests.txt`, `published-consumer-journey-receipt.json`, `published-consumer-calculated-ui.txt/.png`, `published-consumer-suspended-ui.txt`, `published-consumer-recovered-ui.txt/.png` e `published-consumer-indeterminate-recovered-ui.txt`. O primeiro acesso ao separador antecedeu o arranque do servidor local e apresentou ligação recusada; após arranque normal prosseguiu-se. Não é evidência de falha do simulador. Não se repetiram as10páginas do PDF nativo nem o conjunto aceite de83d2f97d/13f68f1b. A aritmética do exemplo demonstra a ligação e preservação do resultado, não a aplicabilidade fiscal a qualquer operação.
#### Procedimento único das três operações — agora executável após decisão
1. **Integrar código:** a Tempo identifica a referência exacta desta candidata na branch `codex/simulator-integrated-review`. Integrar emmain apenas após decisão; o envio desencadeia disponibilização automática na homologação. Manter `INVESTMOZ_IVA_CONSUMER_ENABLED=false`; não executar fixture, comandoQA ou cadastro fiscal no deploy. Confirmar build servido e aplicação; aplicar o procedimento habitual de recarga da configuração e reinício de trabalhadores persistentes, se existirem, antes de activar o novo tipo de trabalho. Não há migration neste delta. A leitura dos snapshots deve permanecer em qualquer versão de reversão; desligar o consumidor é preferível a voltar a um build que desconheça os históricos novos.
2. **Publicar versão real:** com autorização de escrita, backup/restauro confirmado e acção autenticada, reutilizar a fonte real aceite e os actores/papéis existentes. Cadastrar a versão fundamentada e usar `POST /api/v2/investment/admin/tax-rules/{id}/{action}`, com `version_no` e `evidence`, em `submit` → `review` → `approve` → `publish`. RegistarID e `rule_version` publicados. Não copiar a fixture/versão2/actoresUUID deste ensaio para homologação. Se a fonte já estiver cadastrada correctamente, reutilizá-la; não exigir nova certificação da credibilidade oficial. A publicação disponibiliza parâmetros naAPI existente, mas o consumidor continua desligado.
3. **Activar consumidor:** por decisão específica da Tempo, preencher `INVESTMOZ_IVA_CONSUMER_RULE_ID=<ID real autorizado>` e `INVESTMOZ_IVA_CONSUMER_RULE_VERSION=<rule_version autorizada>`; só então definir `INVESTMOZ_IVA_CONSUMER_ENABLED=true`. Aplicar a configuração pelo procedimento operacional existente (`php artisan config:cache` quando se usar cache de configuração; recarregar processos persistentes conforme o alojamento). Confirmar `/api/v1/fiscal-consumer` e a etapa fiscal do simulador, registando decisão, commit/build, ID/versão e instante de exposição. Não usar `version_no` de concorrência como se fosse `rule_version`. O servidor recusa outra versão em vez de escolher a mais recente silenciosamente.
**Suspender/desactivar:** repor o interruptor emfalse e aplicar a configuração impede novas admissões; para retirar a elegibilidade da própria regra usar a transição existente `suspend`, com versão corrente/evidência e actor autorizado. Pedidos já admitidos concluem pelo snapshot; repetição idempotente recupera-os. Histórico/relatório/exportação continuam acessíveis sem novo cálculo, publicação ou activação. Retoma segue uma versão publicada elegível e activação explícita, nuncaSQL directo sobre o estado suspenso. A janela2026 mantém-se um limite de cobertura, não data de caducidade legal.
**Saldo:** ligação funcional do primeiro consumidor fechada em QA, pronta para revisão e decisão sobre as três operações; sem outra preparação genérica identificada. AUD-SIM-INT02 permanece parcial para cobertura fiscal integral. As estimativas anteriores não constituem horas ainda por executar deste delta já concluído. Main local continua em13f68f1b; sem merge/push, publicação real ou activação na homologação. A Tempo não conferiu os ficheiros locais nem validou juridicamente as fontes nesta execução.
### Disponibilização autorizada pela Tempo —21/10/2026, execução em curso
A Tempo autorizou a implementação após o fecho da candidata226fd308 e a identificação das três operações. A integração foi executada por avanço directo de13f68f1b para `226fd308dd45d2805ef4678b20ca4398d7fef6c6`, seguida de envio aorigin/main. A disponibilização automática foi conferida através da camada HTTP autenticada: `/build/revision.json` serve essa revisão emmain, build de21/10/2026 às10:07:12.300278UTC, manifest SHA256 `d524c957dc95e92fcb6eb0d802b9df845743466bd51e398a485bd5ba3d7e3d8f`. `/api/v1/fiscal-consumer` responde200 com `enabled:false` e cobertura2026. A primeira leitura sem autenticação da camada HTTP foi recusada; a conferência seguinte utilizou o acesso já disponibilizado, sem divulgar credenciais. Não se repetiram provas aceites.
**Saldo das operações:** integração e disponibilização do código concluídas; publicação da versão real e activação ainda não executadas. A versão a cadastrar/publicar é `IVA-GERAL-BASE-LIQUIDA`, `rule_version:1`, da ficha documentalv1.2, vinculada ao exemplar da Lei22/2022 com hash `ea1b0dc6f5cf6e595f6d9a19e7042f398999e0087caef1785f6e8fac571b5f53`; oID real do destino ainda não foi obtido. Não usar a fixtureQA deversão2 ou os seus actores. A autorização de disponibilização não é apresentada como validação jurídica do utilizador.
**Dependência operacional concreta:** a sessão SSH anterior terminou comfalha de ligação; autenticação não interactiva no destino foi recusada. Pediu-se a reabertura do mecanismo existente `outputs/investmoz-git-delivery-20261020/resume-git-session.py`, com autenticação exclusivamente na consola local. Está preparado um inventário remoto somente por leitura para conferir os papéis/actores existentes, fonte, versão e estado do consumidor. Ainda não foi submetido, por falta da sessão autenticada. Backup/restauro e acção autenticada da governação serão confirmados antes de escrever dados fiscais ou alterar a activação. Não há nova pendência de fundamentação, nova ronda QA ou pedido de autorização para as operações já autorizadas.
Recibo privado da integração em `outputs/investmoz-iva-disponibilizacao-20261021/integration-receipt.json` no checkout principal; o recibo consolidado existente replica o estado. Nenhuma escrita fiscal, importação da fixture ou activação no destino foi executada nesta passagem. A continuação começa na autenticação remota e nas duas operações restantes, sem voltar a integrar ou preparar a candidata.
### Correcção QA da manutenção fiscal por um responsável — decisão Tempo de21/10/2026
**Origem e decisão:** a obrigação de quatro pessoas foi localizada em `SimulationTaxRuleGovernance::transition` e nos trêsCHECKs de `2026_10_03_170000_create_simulation_tax_rule_governance.php`, incorporados por `e65cb269` (06/10/2026, «varias alteracoes»). A nota de `simulator-evolution-progress.md` descreve «autor distinto de revisor, aprovador e publicador» como implementação. Não foi encontrada no acervo consultado uma decisão expressa da Tempo ou um requisito aplicável que imponha essa separação ao cadastro fiscal. Classificação: **escolha de implementação**, não regra organizacional aprovada. A autoria do commit não equivale à aprovação da condição. A Tempo agora instruiu expressamente a sua remoção neste fluxo e indicou `nala@tempo.co.mz` como responsável. Não se criam quatro designações nem uma excepção artificial limitada à primeira regra; a administração autorizada pode manter os parâmetros fiscais, sem extensão à independência acordada noutros processos.
**O que ficou corrigido em QA:**
- A administração existente, incluindo o perfil administrativo já autorizado comMFA recente, pode cadastrar/manter parâmetros sem preencher quatro listas específicas. As listas existentes continuam disponíveis para atribuições específicas. Utilizadores comuns e perfis apenas de auditoria não adquirem manutenção fiscal; autenticação, verificação de email eMFA permanecem. O investidor continua a utilizar o simulador pelas permissões próprias de execução/consulta; a manutenção não é requisito de utilização.
- O mesmo responsável pode percorrer `draft` → `submitted` → `reviewed` → `approved` → `published`, conservando osID reais em cada etapa. Retiraram-se as comparações de identidade no serviço fiscal e nos respectivosCHECKs; mantiveram-se evidência, datas, estado, versão, coerência da fonte, unicidade/sobreposição, bloqueios, RLS e eventos imutáveis. Não há alegação de revisão independente quando o actor é o mesmo.
- Como o destino não tem a fonte deste lote cadastrada, o percurso fiscal permite registá-la em `POST /api/v2/investment/admin/tax-rule-sources`: metadados existentes, origem oficial identificada, citação eSHA256. Regista a aceitação oficial já decidida pela Tempo em08/10/2026, responsável e momento, com `legal_opinion:false` e `independent_review:false`. O estado geral da fonte permanece `pending`; o fundamento `fiscal_acceptance` permite a sua utilização apenas na governação/resolução fiscal. Assim, não fica automaticamente validada para zonas ou outros processos, nem certifica juridicamente uma operação ou atribui parecer à Tempo. A regra deve referenciar o mesmo hash e versão. Cadastro de fonte não publica regra nem activa consumidor.
- A mesma pessoa pode concluir essa curadoria fiscal sem inventar outro actor para «validar» a fonte oficial aceite. A condição normal da fonte fora deste percurso, as relações/validações de zonas, oportunidades e perfis mantêm-se. O novo fundamento explícito `fiscal_acceptance` fica na fonte e na auditoria; o endpoint normal de zonas não permite marcar esse fundamento. Não se alteraram os fluxos com independência já acordada.
**Esquema:** `2026_10_21_130000_correct_fiscal_maintenance_actor_constraints.php` foi ensaiada apenas na base QA isolada. O delta é explícito: retirar desigualdades de identidade dos trêsCHECKs fiscais; acrescentar o fundamento de aceitação fiscal à fonte; registar a responsabilidade única na aceitação fiscal da fonte; ajustar apenas a projecção pública/resolução fiscal para reconhecer esse fundamento com hash e versão correspondentes. A condição normal de validação independente da fonte não foi removida e o seu estado continua pending. A guarda recusa execução da migration foraQA. Nenhuma constraint de oportunidades, zonas, perfis ou outra tabela foi modificada. A reversão só é permitida em QA sem registos dependentes; quando existirem cadastros legítimos, recusa apagá-los ou voltar a impor uma condição que os invalide. Desligar novas execuções preserva os históricos.
**Verificação estritamente do controlo alterado:**6testes/86asserções passaram em `simulator_fiscal_checks_20261021_test`, em transacções revertidas. Cobrem administração existente por lista e por perfil/MFA recente, sem quatro designações; curadoria da fonte, cadastro e quatro etapas pelo mesmo actor; responsabilidade/hashes dos eventos; consulta e suspensão; consumidor ainda desligado; recusa de investidor, auditor, ausência/expiração deMFA, falta de evidência, hash/versão de fonte incoerente e versão optimista errada; imutabilidade; compatibilidade das constraints não fiscais; reversão vazia e recusa de reversão com dados dependentes; guarda contra alvo nãoQA. Não se executou uma simulação: a contagem de execuções foi preservada. Não houve testes novos de cálculo, decimal, exportadores, PDF, TypeScript ou build, pois esse código não mudou. Pint foi conferido nos ficheirosPHP do delta. Prova privada: `outputs/simulator-continuation-20261020/fiscal-maintenance-actor-tests.txt`. Os testes antigos de governação foram ajustados para retirar a expectativa de uma proibição agora expressamente revogada, preservando os casos de permissões, versões e histórico.
O esquema foi depois aplicado, apenas pelo caminho desta migration, ao clone persistente `simulator_fiscal_delta_20261021_test`, mantendo a guarda de isolamento. Prova por leitura antes/depois:2versões de regra,7eventos de governação,11resultados,11relatórios e8fontes com as mesmas contagens e hashes de conteúdo. Na comparação da fonte exclui-se apenas a coluna nova, que ficaNULL nos registos anteriores; nenhum conteúdo ou snapshot histórico foi reescrito. Prova privada `fiscal-maintenance-schema-proof.json` na mesma pasta. O clone QA está, assim, com o controlo aplicado para revisão; a base activa e os seus actores não foram alterados.
**Estado real do destino:** a dificuldade de introduzir o comandoSSH foi resolvida abrindo directamente a consola habitual; a autenticação foi feita pelo utilizador e a sessão ficou pronta. A leitura do vhost identificou o directório servido `/var/www/html/teste-investidor.tempo.co.mz/current`, que aponta para a release226fd308; a cópia anterior conservada na raiz não é o consumidor servido. Foi confirmado por leitura que `nala@tempo.co.mz` existe (ID15), email verificado, MFA configurado e perfil de superutilizador existente; não foi criada conta ou alterado perfil. Não se confunde matrículaMFA com sessão autenticada recente: as acções de cadastro continuam a exigir a autenticação normal do responsável.
O backup/restauro anterior à escrita ficou comprovado em21/10/2026 às10:18:00UTC: cópia consistente por snapshot de `teste_investidor`, arquivo privado `/var/backups/investmoz/iva-rollout-20261021_101750/database.dump`, SHA256 `4e972c028fbe70c347c518ba86fe7e293093b7c81278ac4d64a077bf3e5325b7`,4.584.693bytes; restauro isolado em `iva_rollout_20261021_101750_test`, acesso público à base revogado. Contagens e hashes canónicos das171tabelas coincidiram. A configuração anterior ficou protegida na mesma pasta. Isto comprova backup/restauro, sem ensaiar ou escrever os parâmetros no activo. A primeira validação local do cabeçalho do arquivo usava texto inadequado e foi corrigida para o formatoPGDMP; a segunda operação completou o restauro e a comparação. Não é falha do simulador.
#### Procedimento actualizado das três operações, sem quatro responsáveis obrigatórios
1. **Código e controlo de esquema:** a revisão226fd308 já está integrada e servida. A presente correcção de governação fica na branch QA para revisão; não foi enviada amain. O delta inclui o esquema acima, cuja aplicação no destino deve acompanhar a referência exacta do código, por operação técnica autenticada e combackup/restauro actualizado conforme o estado nesse momento. A migration protegida é o ensaioQA, não um comando a executar contra a base activa. Conservar os históricos e o consumidor desligado durante a aplicação do controlo; não usar o deploy para cadastrar/publicar dados. Não voltar a integrar226fd308 nem repetir as provas de cálculo.
2. **Fonte e versão fundamentada:** o responsável real indicado pela Tempo autentica-se normalmente, comMFA. Reutiliza a fonte aceite se já existir; caso contrário, cadastra-a pelo endpoint fiscal acima, com exemplar/hash/citação reais. Cadastra `IVA-GERAL-BASE-LIQUIDA`, `rule_version:1`, conforme fichav1.2 e âmbito/exclusões2026. A mesma conta autorizada conclui `submit` → `review` → `approve` → `publish`, sempre com `version_no` corrente e evidência fiel: manutenção técnica autorizada, não parecer jurídico nem revisão independente. Registar oID real; não usar a fixtureQA deversão2 nem contas sintéticas. Não é preciso criar quatro designações.
3. **Activação do consumidor:** permanece separada da publicação. Aplicar a ligação aoID e `rule_version` reais autorizados e o interruptor de activação pelo procedimento já definido, conservando cálculo sobre base declarada como estimativa, sem apuramento final do imposto a pagar. Conferir somente a revisão/regra efectivamente servidas e o delta no destino. Desactivar o consumidor ou suspender a regra recusa novas admissões, preservando as já admitidas, históricos e relatórios.
**Saldo actual:** controlo corrigido e verificado emQA, fontes e funcionamento do cálculo não reabertos; sem nova publicação ou activação na homologação. A referência servida continua226fd308 e o consumidor desligado. A correcção não foi aplicada ao esquema activo. A indicação do responsável e a autorização operacional não são tratadas como conferência de ficheiros locais ou validação jurídica da Tempo. Esta actualização substitui a indicação anterior de que a simples falta de quatro listas configuradas bloqueava obrigatoriamente a disponibilização.
### Execução autorizada da correcção fiscal — 21/10/2026
A Tempo autorizou incorporar4a4d1212 e concluir apenas as operações pendentes. A correcção foi integrada emmain; a equipa integrou entretanto d3fe8a02, que inclui4a4d1212 e conserva o mesmo código fiscal. A revisão efectivamente servida, conferida em `/build/revision.json`, é `d3fe8a02ec7f5dcd01984c4f4a1eba18b5a0cf19`. Não se reinstalou226fd308.
Backup/restauro actualizado: `/var/backups/investmoz/iva-rollout-20261021_104253`, dumpSHA256 `b37bf4aad0abd839bc81d70adf802535ad7738fabe75fda61d74f39dfc11e74b`; restauro isolado `iva_rollout_20261021_104253_test`,171tabelas com contagens e hashes coincidentes. A operação técnica autenticada aplicou exclusivamente oDDL revisto, com registos/históricos e constraints alheias preservados. A migration protegida não foi executada na base activa nem teve a guarda enfraquecida. O contrato do executor foi actualizado apenas para essa migration, após registo da compatibilidade; a primeira disponibilização conservou a release anterior ao encontrar a alteração de esquema. A retoma usou o mesmo executor e não passou a executar migrations no destino.
Publicação e activação concluídas conforme recibo. O consumidor está activo. Não foram repetidas provas aceites do cálculo, precisão, exportadores ouPDF, nem importada a fixtureQA. A autenticação da infraestrutura não é identidade administrativa do portal. A conta Christiana/example.test reportada pelo utilizador pertence ao ensaio local anterior e não é usada no destino.
Mantém-se o procedimento único: **1)** código/controlo do esquema, agora integrados; **2)** cadastro das fontes reais com hash/citação e da versão1 `IVA-GERAL-BASE-LIQUIDA`, seguido das etapas autenticadas pelo mesmo responsável autorizado; **3)** activação separada, ligando o consumidor aoID e versão publicados. Cobertura2026, operação interna geral sobre base líquida declarada emMZN; excluídos preços comIVA incluído, taxas reduzidas, isenções, regimes especiais, importações, digitais não residentes, deduções/créditos/reembolsos, incentivos, pagamento/declaração e outros impostos. O resultado é estimativa deIVA sobre a base declarada, não apuramento final a pagar.
Para suspender, desligar `INVESTMOZ_IVA_CONSUMER_ENABLED` e reiniciar graciosamente os workers pelo mecanismo existente, ou suspender a versão publicada pela governação autenticada, com `version_no` corrente e evidência. Novas admissões são recusadas; as já admitidas concluem com snapshot congelado, mantendo históricos e relatórios. Não apagar fontes, versões ou execuções para reverter a exposição. A autorização e o cadastro não constituem conferência local dos ficheiros pelo utilizador nem validação jurídica atribuída à Tempo. O recibo JSON existente regista a revisão, a regra e o estado efectivamente observados.
**Recibo final das três operações:** código4a4d1212 integrado e servido dentro ded3fe8a02; três fontes oficiais reais cadastradas comversão1, hashes e citações; regra `IVA-GERAL-BASE-LIQUIDA`, **rule_version1**, ID `01a15e1a-5338-700f-8a45-f87707d696a7`, estado `published`, controlo optimista `version_no5`; autor/revisor/aprovador/publicador reais: conta indicada `nala@tempo.co.mz`, ID15. A sessão normal comMFA foi efectuada pelo utilizador numa página temporária de apoio ligada apenas a127.0.0.1; não se guardaram palavra-passe, códigoMFA ou cookies. Não há alegação de revisão independente ou parecer jurídico. A aceitação fiscal das fontes tem o fundamentoTempo08/10/2026; o estado geral permanecepending, sem validação automática noutros processos.
Activação separada aplicada em21/10/2026 às10:55:58UTC, alterando apenas as três chaves do consumidor. Ligação exacta aoID acima, rule_version1 eenabled=true; restante configuração preservada. `/api/v1/fiscal-consumer` confirmouenabled=true porHTTP200; a leitura do resolvedor, sem executar cálculo, confirmouIVA_GENERAL_RATE=16percent, mesmoID/versão/fonte no contextoMZN/internal_general_net de2026. Cinco eventos de governação; contagens de resultados/relatórios preservadas, sem simulações sintéticas no destino. O restauro de segurança e a configuração anterior estão conservados no arquivo privado identificado no recibo.
Suspensão concreta: configurar `INVESTMOZ_IVA_CONSUMER_ENABLED=false` e executar o reinício gracioso existente `php8.4 artisan queue:restart` na releaseactual como operador autorizado; ou `POST /api/v2/investment/admin/tax-rules/01a15e1a-5338-700f-8a45-f87707d696a7/suspend`, com `version_no:5` enquanto corrente e evidência verdadeira de pelo menos20caracteres, pela administração autenticada/MFA. Não repetir a suspensão se o estado/versão já tiver mudado. Conservar a ligação e todos os registos; futuras retomas respeitam o estado da regra e o procedimento de versão aplicável, sem escolher automaticamente outra regra.
### Diagnóstico da consulta IVA — correcção em QA,21/10/2026
O caso reportado identifica «Componentes fiscais e contributivas», IVA_GENERAL_RATE, período01/01/2026–31/12/2026 e «As condições publicadas não correspondem às respostas indicadas». É consulta de parâmetros, não execução do consumidor IVA. O utilizador não forneceu a moeda ou a operação originais; não se afirma que tenha seleccionadoUSD, omitido um campo ou indicado uma operação excluída.
**Causa confirmada no contrato:** o formulário envia `fiscal_operation`, sem `operation` ou `currency`; a versão publicada exige `operation=internal_general_net` e `currency=MZN`. A validação da consulta nem aceitava esses nomes canónicos. O resolvedor reduzia qualquer condição falhada a `indeterminate_context`; a interface mostrava apenas a frase genérica e o código interno. Esta restrição de ligação é escolha de implementação, não condição organizacional ou jurídica adicional da Tempo. O mesmo contrato reproduziu o estado no período anual reportado, independentemente de se presumir uma moeda no caso original.
**Correcção delimitada:** cada condição não satisfeita tem categoria, campo, valor recebido e critério esperado, ligados à versão e fonte. Distinguem-se campo em falta, formato inválido, operação/moeda fora do âmbito, versão que não cobre o período e configuração do sistema. Quando não existe versão elegível visível e a evidência não permite separar ausência de cobertura de indisponibilidade de fonte/configuração, a interface mantém essa incerteza; não inventa uma causa nem atribui culpa às respostas.
A consulta IVA passa a ter moeda explícita (`fiscal_currency`), guardada no rascunho, sem presumirMZN ou copiar moeda do orçamento ou do cálculo IVA. A API recebe esse valor em `context.currency`. A operação genérica não se converte automaticamente no âmbito «operação interna à taxa geral sobre base líquida»: a limitação de integração é explicada com a resposta recebida e o critério esperado, e existe ligação para o formulário IVA com declaração explícita do âmbito. Uma importação/exportação/não residente/reembolso expressamente seleccionado é identificado como exclusão deste lote; não se altera a resposta para a fazer passar.
As notas aparecem junto dos campos e num resumo com ligações que focam o campo correspondente, mantendo o rascunho. O nome apresentado é «IVA — taxa geral»; «Ver fundamento» contém versão, vigência e fonte, sem expressãoDSL. As falhas de consulta/configuração são identificadas como sistema. O percurso de cálculo e as suas regras de admissão não foram ampliados. A consulta diagnóstica pode observar versões publicadas fora do período seleccionado, sob a mesma política de visibilidade, apenas para explicar a falta de cobertura; não as torna elegíveis.
**Prova dirigida:**5testes unitários novos/24asserções;1testeAPI/16asserções no clone isolado simulator_fiscal_checks_20261021_test;7testes existentes do resolvedor/25asserções; TypeScript, build(exit0), Pint e diff-check. A primeira execuçãoAPI detectou que esse clone ainda não tinha a coluna fiscal_acceptance; aplicou-se nele a correcção de esquema já aceite, com a guardaQA intacta, e repetiu-se apenas o testeAPI afectado. Não é diagnóstico da base de homologação. A comparação por índice do ensaio foi ajustada à ordem das chavesJSONB; não se alterou o critério fiscal.
No Edge, simulador normal QA em127.0.0.1:18580: consulta anual2026, operação genérica explicitamente escolhida para o ensaio; moeda inicialmente vazia, depoisUSD escolhido expressamente como caso dirigido. Confirmaram-se notas, resumo, foco em «Rever moeda»/«Rever operação», fundamento da versão sintética3, eUSD/ano/periodicidade/operação conservados após gravação e recuperação do mesmo rascunho. A captura de prova é um ensaioUSD, não evidência deUSD no caso original. Não se executou cálculo nem se exportou relatório. A fixture publicada apenas emQA para observar o diagnóstico foi depois suspensa; o consumidorQA permaneceu desligado. Nenhuma fixture foi importada, regra alterada ou consumidor activado na homologação nesta correcção.
Provas privadas na pasta `outputs/simulator-continuation-20261020`: iva-diagnostic-api-test.txt, iva-diagnostic-summary.txt, iva-diagnostic-summary.png e iva-diagnostic-build-final.txt. O reciboJSON existente tem o estado detalhado. A candidata permanece na branch `codex/iva-diagnostics-QA`, separada de main; não é uma nova publicação fiscal nem altera a entrega autorizada anteriormente.
### IVA mensal ligado às vendas — candidata QA, 21/10/2026
Pedido da Tempo: o formulário recolhe pressupostos do negócio e calcula o IVA correspondente, sem repetir vendas nem tomar o imposto manual como entrada fiscal. A falta de ligação entre a projecção empresarial e o consumidor publicado era uma escolha de implementação; não uma nova obrigação do utilizador. Alteração na branch `codex/iva-monthly-projection-QA`, separada de main. Não altera a instalação, regra ou activação da homologação.
**Ligação:** o simulador normal reutiliza quantidades e preços por produto/mês do plano empresarial, com a mesma representação monetária das vendas já usada nesse plano. Se não existir plano, admite a receita mensal constante já guardada na projecção personalizada, identificando essa origem e exigindo declaração do enquadramento. Não usa os valores demonstrativos de um widget ainda não guardado. A indicação sem IVA/com IVA incluído fica explícita, sem a presumir. Neste lote só a base líquida declarada permite calcular: preços com IVA incluído, base desconhecida e meses sem vendas informadas permanecem indeterminados. Zero explícito calcula IVA zero; não se preenche um mês vazio com zero.
O cliente envia meses, vendas, moeda e declaração; não envia taxa, versão ou imposto manual. O servidor selecciona a versão publicada ligada ao consumidor e reutiliza o resolvedor temporal e avaliador decimal existentes em cada mês, dentro da mesma admissão transaccional. Nenhuma taxa foi fixada no formulário, nem introduzido arredondamento fiscal. A versão é congelada antes da fila; mantém-se a autorização/idempotência/histórico/relatório existentes. Sem migration ou reconstrução dos mecanismos. Cobertura permanece 2026, no máximo 12 meses consecutivos; um horizonte que atravessa o limite é explicado, sem truncar a projecção.
**Ecrã e dependências:** cada mês apresenta vendas declaradas, vendas tributáveis sem IVA, taxa efectivamente aplicada, IVA calculado e total com IVA. Após a primeira execução, a actualização automática fica ligada e pode ser pausada. Alterar vendas, moeda, período ou tratamento do IVA assinala imediatamente o resultado anterior como histórico desactualizado. A nova execução guarda outra versão sem modificar a anterior. Repetir sem alterações recupera a execução concluída; uma execução pendente é consultada pelo mesmo ID. Falhas conservam os pressupostos e permitem tentar novamente. O rascunho conserva configurações e resultado; o relatório distingue resultado actual e histórico desactualizado e não recalcula o IVA na exportação. Se algum mês estiver indeterminado, o total fiscal fica indeterminado, conservando os meses conhecidos.
**Tesouraria adoptada:** o IVA é um memorando fiscal separado; não se soma novamente aos recebimentos introduzidos, não se deduz como custo e não se apresenta como imposto final a pagar. Conserva-se o calendário de recebimentos/pagamentos declarado, sem inferir cobrança integral no mesmo mês nem liquidação fiscal. O imposto manual permanece exclusivamente no painel recolhido «Cenário personalizado de tesouraria — imposto manual, separado do IVA calculado», com rótulos de cenário personalizado. Não alimenta a base nem o resultado do consumidor IVA. Não foram acrescentados deduções, créditos, reembolsos, importações, isenções, taxas reduzidas ou regimes especiais ao cálculo fiscal; as hipóteses antigas de tesouraria continuam personalizadas e separadas.
**Provas apenas do delta:** 3 testes API novos/46 asserções, transacções no clone `simulator_fiscal_checks_20261021_test` com guarda QA; 4 testes frontend novos para reutilização das vendas, zero/informação em falta, invalidação e recuperação/relatório. Incluem taxa proibida no cliente, âmbito não declarado, preços brutos, período excluído, repetição sem duplicações, versão congelada antes da suspensão e conclusão depois dela. TypeScript/build/Pint/diff-check verificados. Reutilizadas as provas aceites do avaliador decimal, exportadores e PDF; não se voltou a testar paginação nem a ampliar cobertura legal.
**Percurso Edge no simulador normal:** dossier sintético privado «QA IVA mensal - vendas projectadas», ID `e0201d19-286e-4d5c-9585-2c1e3bd1ad1e`, QA127.0.0.1:18580. Vendas Janeiro1000.03/ Fevereiro2000.06/ Março0/ Abril vazio produziram IVA160.0048/320.0096/0/indeterminado. Alterar apenas a quantidade de Janeiro de1 para3 actualizou vendas3000.09, IVA480.0144 e total3480.1044. Assinalou-se resultado desactualizado durante a mudança. Seleccionar preços com IVA incluído conservou vendas e tornou base/IVA/total indeterminados; voltar manualmente à base líquida gerou nova execução. Após recarregar e recuperar o mesmo dossier, os valores permaneceram iguais e o fundamento mostrou a versão congelada. Quatro execuções mensais intencionais (inicial, venda alterada, base incluída, regresso à líquida), quatro relatórios com snapshots iguais às respectivas execuções; recuperação não criou a quinta. Não se atribui aplicabilidade fiscal universal a estes exemplos aritméticos.
A fixture sintética **v4**, ID `01a15e50-7972-72e0-8991-82ca28df9b49`, está publicada e o consumidor ligado **apenas no clone QA** `simulator_fiscal_delta_20261021_test`, deixados disponíveis para esta revisão. Isto substitui o estado local suspenso/off da prova diagnóstica anterior; não é indisponibilidade na homologação. Não importar esta fixture, os seus actores ou os scripts privados. A regra real de homologação v1 mantém-se fora desta execução.
**Revisão e disponibilização:** rever esta candidata QA e integrar somente o código autorizado, mantendo a ligação ao ID/versão reais do destino. Este delta não precisa de publicar outra regra nem de mudar a taxa/âmbito ou de reactivar um consumidor já activo. Conferir apenas revisão servida e ligação mensal no destino após autorização própria; conservar o procedimento separado de publicação e activação se alguma dessas operações vier a ser expressamente pedida. Suspender novas admissões pelo interruptor existente ou estado da regra preserva as já admitidas e os históricos. Não apagar snapshots para suspender ou reverter código. Sem integração em main, escrita activa, validação jurídica atribuída à Tempo ou nova ronda de preparação identificada nesta entrega QA.
Provas privadas em `outputs/simulator-continuation-20261020`: monthly-iva-api-test.txt, monthly-iva-build-final.txt, monthly-iva-initial.txt, monthly-iva-updated.txt, monthly-iva-stale.txt, monthly-iva-included.txt, monthly-iva-recovered.txt, monthly-iva-live-history.json e monthly-iva-proof.png. O recibo existente conserva o saldo e os IDs; AUD-SIM-INT02 da matriz existente recebe esta referência, sem outra matriz/backlog.
### Reconciliação Modelo A — primeiro incremento QA,21/10/2026
Por instrução expressa da Tempo, o **Novo Modelo A.docx** é a referência estrutural, incluindo o quadro incorporado como imagem; não foi substituído por um modelo genérico. A [reconciliação campo a campo](../simulations/modelo-a-reconciliacao-20261021.md) contém todos os quadros, campos01–27, finalidade, origem, cálculo, fundamento e estado; inventário/versões/exemplares e divergências preservados. A componente16% existente representa apenas01/02. O apuramento14–18 não se presume igual ao IVA das vendas; não é saldo final a pagar.
Original532008bytes/SHA25662c198e90d8c6792f7d5ccefc6386df2e4b6089573067d3a6e1eae7fc2b9572b e variantev1 de532100bytes/63d45fa80db17a60b43619d55a25813229d670d14c758273f86316285db8e02b preservados na biblioteca **privada QA**, recursos33/34, documentos e revisões/auditoria existentes. Importador existente, pré-visualização, conflitos/duplicados, scans reais limpos com mesmohash e nova tentativa sem criar documentos ou scans. Actor166 é a fixture administrativa do ensaio isolado, sem papel real ou alteração persistente de permissões atribuída à Tempo. Nenhuma publicação ou importação na homologação. Aceitação das fontes oficiais da Tempo08/10 mantida; edição vigente do formulário não afirmada pela data OOXML nem pela leitura do anexo.
No simulador normal, resumo de simulação parcial e correspondência consultável ligados ao resultado mensal recuperado. Compras/encomendas projectadas reutilizadas conforme agenda activa por produto, calendário ou valor constante; não são custo das vendas ou pagamentos de fornecedor. Zero difere de falta. A indicação semIVA/IVA/comIVA/dedutível fica separada e por determinar quando faltam classificação e documentos; não se considera toda compra dedutível. Relatório acrescenta correspondência/limites e referências dos pressupostos actuais de compras, sem os apresentar como resultado fiscal histórico nem reescrever snapshots. Nenhuma nova fórmula fiscal activada ou taxa fixada no formulário; sem migration ou motor novo.
Cotejo dirigido: Lei22/2022 art.17-A/aditamento e eficácia; Lei10/2025 arts.18/19/21/32/34 nas páginas12–15 do BR248, com leitura visual13/14. Registadas remissões divergentes do exemplar (regularizações11/12 versus caixas12/13; crédito18/19 versus caixas19/20; reembolsoart20 versus matéria noart21; quadro8 ambíguo). Não corrigir original nem aplicar instrução ambígua como regra. AT directa expirou; resultado indexado com numeração distinta é limitação de consulta, não substituto nem prova de edição vigente. Lei3/2025/dispositivos/regulamentos pertinentes por consolidar na extensão concreta, sem reabrir genericamente a base líquida16% aceite.
Provas novas apenas do incremento:3testes frontend (agendas, zero/falta, sem IVA/dedução fabricados e relatório sem mutar resultado), TypeScript/build, Edge de consulta após recuperação e biblioteca idempotente/scan. Avaliador, selecção publicada, precisão, históricos e exportadores aceites reutilizados. DossierQA e fixtureIVA v4 continuam disponíveis para revisão; este incremento não calcula novamente. Provas privadas outputs/modelo-a-20261021; recibo e inventário existentes actualizados. Branch codex/iva-modelo-a-reconciliation-QA, parent3f5430c3, separada de main; homologação/regra real/interruptor inalterados.
Saldo explícito: **reconciliação e primeiro incremento de interface concluídos, apuramento completo não implementado**. Próximo lote: classificação por operação e origem prevista/efectiva, cadastro fundamentado de âmbito/dedução e ligação a totais14–18 propagando indeterminação; 8–16h cadastro/consolidação e20–36h código/aceitação, total28–52h condicionado ao âmbito/consolidação, sem créditos anteriores/substituições/pagamento/reembolso ou simulador integral. O documento contém o delta e critérios concretos. Integração do código, publicação e activação continuam operações distintas; esta preparação não executa nenhuma no destino.
### Continuidade e adaptação contextual — primeiro incremento QA, 22/10/2026
Instrução expressa da Tempo de 22/10: o contexto do investimento é transversal ao percurso, formulários, documentos e simulações, mesmo sem IA. Base Git e revisão servida confirmadas em leitura: `57fc914821d0cefa02caa80a74b3d42e663f2742`. Trabalho isolado em `codex/project-context-continuity-QA`, sem integração ou publicação/activação fiscal no destino.
Causas identificadas no código e reprodução dirigida: `InvestorContext` só distribuía título, descrição, sector e geografia; o preenchimento comum copiava apenas campos vazios sem distinguir herança de edição deliberada; o IVA pontual mantinha moeda/data/base em estado local que desaparecia ao desmontar o ecrã. A projecção empresarial sobrepunha a moeda do projecto à moeda de origem do plano ao apresentar/calcular/exportar. No ensaio anterior, projecto guardado em MZN e selector IVA vazio. A primeira tentativa de abertura encontrou uma sessão expirada, confirmada pela leitura sem autenticação e resolvida pelo início de sessão normal; não foi classificada como perda do dossier.
Correcção comum: consolidar exclusivamente o rascunho explicitamente ligado ao mesmo dossier e proprietário; conservar origem, versão e hash. Não procurar o último projecto da conta. Herança persistida com assinaturas de alteração por campo e âmbito dossier/consumidor; não são tokens de autorização nem prova jurídica. Actualizar valores ainda herdados, preservar alterações deliberadas e campos apagados; manter a hierarquia geográfica e os restantes dados das parcelas. O armazenamento do navegador passa a incluir conta e projecto. Consumidores do padrão: simulador, apoio à terra, Balcão e manifestação de investimento; esta última recebe também a moeda compatível. Campos sem correspondência semântica não são copiados. Nacionalidade, residência e país de constituição não são equiparados.
O IVA pontual guarda as entradas com o rascunho. A exploração fiscal, oportunidade e componente pontual reutilizam a moeda compatível; a projecção mensal reutiliza o plano e o início previsto. Não se infere exigibilidade fiscal a partir da data prevista de início, nem residência jurídica a partir do país. USD é conservado e explicado como não suportado pela versão MZN. Uma divergência entre moeda guardada no plano e moeda do projecto é apresentada, sem conversão ou reclassificação. O plano conserva a moeda de origem também na apresentação e nos exportadores existentes. Vendas, zero/falta e snapshots de IVA continuam a usar o motor publicado e os históricos aceites; nenhum imposto mensal fixo é introduzido.
Adaptação deste incremento: o editor existente passa a permitir moeda, início previsto, origem do investidor, tipo/situação da entidade e residência fiscal declarada, sempre pela prévia/confirmar existente. A prévia continua a identificar dados/documentos conservados, resultados e etapas afectados; alterações apenas ao título/descrição não invalidam a aplicabilidade. O selector territorial reutiliza o catálogo público `InvestmentZones` para parques, ZEE e ZFI, preservando versão/hash, fontes e condições públicas já divulgadas no complemento existente. A mesma síntese aparece no contexto e simulador e segue para o relatório do dossier. Os ramos de terra/financiamento reutilizam a definição publicada, factos, estados e razões de `InvestorPath`; não foi criado outro avaliador nem activado benefício.
Escolher uma zona abre a análise específica: referência consultada, condições documentadas ou em falta, período/fonte cadastrados quando disponíveis e reconhecimento por determinar. O benefício e o cenário condicionado permanecem indeterminados quando não existe ligação executável às condições/reconhecimento. O IVA disponível é identificado como cenário geral sem benefício; não representa automaticamente o regime escolhido nem apuramento final. Reutilizam-se `InvestorContext`, `InvestmentZones`, `InvestmentZoneOperationalDetails`, `InvestorPath`/definição publicada, resolvedor/avaliador fiscal e snapshots existentes. A escolha técnica de exigir uma nova confirmação genérica dos mesmos dados em cada formulário foi retirada por instrução da Tempo; permissões, declarações, submissões formais, revisões independentes aprovadas e campos autorizados de pedidos devolvidos mantêm os controlos existentes.
Provas novas: 4 testes HTTP/74 asserções, em transacções revertidas na base isolada `simulator_fiscal_delta_20261021_test`/loopback55441; 8 testes frontend de herança, edição/apagamento, isolamento, parcelas, moeda divergente e contexto no relatório; TypeScript/build. Percurso Edge: A nacional/MZN e residência fiscal ZA, vendas guardadas de quatro meses → cálculo mensal → gravação → edição deliberada do título → recarregamento → terminar sessão/iniciar sessão → recuperação e relatório pelos dados efectivamente guardados. Resultado `ae401465-834f-4761-9406-5af060c499fb`, uma única execução nova no dia; Janeiro1000.03/IVA160.0048, Fevereiro2000.06/320.0096, Marçozero e Abrilsem informação. O modelo de relatório recuperado conserva os valores exactos e não altera o snapshot. B estrangeiro/USD, primeiro divergência explícita planoMZN/projectoUSD, depois declaração manual USD no plano, IVA recusado sem conversão; selecção de parque pela prévia e análise específica. Os testes de ZEE/ZFI usam apenas metadados sintéticos revertidos: não demonstram elegibilidade real. Provas privadas em `outputs/context-continuity-20261022`; não entregar fixtures ou credenciais.
Limites concretos: este é o primeiro incremento utilizável de continuidade e explicação contextual, não a conclusão da adaptação de todos os sectores/regimes. O catálogo QA consultado contém parques sintéticos; a ligação jurídica completa de requisitos/documentos/licenças e benefícios às condições de cada actividade/regime não está demonstrada. Condições sem estrutura/relação executável continuam explicitamente em falta; nenhuma alegação nova de direito ou benefício é feita. Não houve nova validação jurídica nem revalidação genérica de credibilidade das fontes oficiais aceites em08/10. Não repetir cálculo decimal/exportadores/paginação já aceites. A demonstração visual foi feita no percurso e simulador; os outros três consumidores receberam o reconciliador comum, sem se afirmar um ensaio visual integral de cada processo.
Revisão: abrir o simulador QA com o dossier A `92d0836c-0ffb-4e07-a7f5-0e8ca3f594b9` ou B `1872412f-251a-4f6d-bf18-70388e71a16a`; usar a conta sintética QA existente, nunca actores sintéticos na homologação. O router privado de QA usa a instalação já existente do percurso, activada apenas neste ensaio isolado; não há migration ou nova exigência de funções. Entregar a candidata para revisão pelo processo Git existente, preservando alterações concorrentes. Uma integração futura exige rever o delta relativamente a main e o estado de instalação/configuração do percurso no destino; não publicar regras ou alterar interruptores fiscais como consequência desta correcção. Preservar todos os históricos na suspensão ou reversão de código.
Fecho da observação deste incremento: o regresso A ao dossier gravou a evidência financeira `43317c22-50c8-40cf-98e4-e5d4d108c372`, rascunho `cf4dec46-c96c-4aa6-aa9c-1a8d6ecb1bea` v8, contexto v1, com hash preservado. A ligação de inspecção do Edge interrompeu-se depois da navegação; o inventário mostrou o endereço do mesmo dossier e a leitura autenticada do serviço confirmou a gravação. A parte visual final do regresso não foi observada e fica como limitação da ferramenta, não falha do simulador. B recuperou contexto v2 e gravou rascunho v6, mantendo USD e sem execução fiscal. O relatório gerado destes dados recuperados conserva a versão do parque e benefício indeterminado. O enriquecimento automático do rascunho privado é exclusivo do proprietário autenticado; o acesso de gestão ao dossier não concede por esta alteração acesso ao rascunho privado da conta.
### Revisão de a76369eb e primeiro percurso adaptativo fundamentado — v1, 22/10/2026
**Revisão encerrada no âmbito de a76369eb, sem integração.** O último regresso tem prova equivalente: destino do mesmo dossier observado pelo inventário do navegador e evidência `43317c22-50c8-40cf-98e4-e5d4d108c372` confirmada no serviço, ligada ao rascunho v8/contexto v1 e ao hash `a21fa5779bf8b8df366f16683c8a9bb9817a6d2921c7384f85df02135307fa13`. A observação visual final continuou limitada pela ferramenta. Não se repetiram A/B, cálculos, exportações ou nova sessão; reutilizam-se as provas aceites. Leitura dirigida do delta: isolamento por proprietário/dossier, edições/apagamentos, origem monetária, controlo de escrita e ausência de publicação fiscal. Não foi identificada nova preparação técnica que condicione a decisão de integração deste âmbito. A adaptação jurídica seguinte é independente.
**Percurso delimitado preparado:** preparação de um pedido de investimento privado para empreendimento económico em Moçambique, com actividade concreta declarada, instalação identificada e operações mensais enquadráveis no consumidor IVA geral existente. Trata a preparação do pedido de investimento; não se apresenta como licenciamento sectorial completo, certificado emitido, direito a incentivo ou submissão à Administração. Investidor nacional/estrangeiro, actividade, modalidade e localização são condições distintas. A saída é formulário orientado, etapas/documentos aplicáveis e simulação identificada pelo seu âmbito. Não se escolhe “Turismo” ou uma ZEE para presumir requisitos ou benefícios.
**Fonte primária aplicada:** compilação oficial APIEX “Legislação de Investimentos em Moçambique”, exemplar obtido de https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf — 6467655 bytes, SHA256 `ba1e9eaabfec5d0c4cb2d9f2444a6860f7a90f222034764197829be402d408ed`. Contém Lei8/2023 (pp.8–32), Decreto8/2024/Regulamento (pp.36–59), CBF (pp.64–92). Aceitação oficial de08/10 mantida; não há exigência de segunda autenticação ou aprovação jurídica da Tempo. Inventários existentes de604 registos/exemplares/anexos e9901 caminhos do acervo reutilizados antes da selecção; cópia privada, texto extraído e citações em `outputs/adaptive-path-20261022`. Não importar/publicar automaticamente. As pendências históricas genéricas de nova certificação nos levantamentos de28/09 e03/10 não são gates deste lote.
**Período:** Decreto8/2024, artigo5 do diploma preambular, indica aplicação desde a publicação de7/03/2024; Lei8/2023, artigo32, indica90dias após publicação. O lote proposto cobre datas de projecto em2026 e preserva estes inícios, exemplar e versão. Não foi encontrada alteração nas consultas oficiais dirigidas desta execução; isso não certifica inexistência de actos posteriores. A compilação de2025 não constitui consolidação universal de2026. Na ligação executável, uma fonte/versão sem cobertura temporal não autoriza fallback para a versão mais recente. Os artigos citados são do **Regulamento anexo** quando identificado “R”; “L” designa a Lei8/2023. Rastrear esta distinção evita confundir os dois artigos9 ou o artigo5 preambular com o do Regulamento.
| Condição | Fonte e período | Efeito no formulário | Efeito no percurso | Efeito no cálculo | Informação em falta / estado da ligação |
| --- | --- | --- | --- | --- | --- |
| Projecto económico em Moçambique que procura garantias/incentivos; actividade efectiva conhecida | L2(1)–(3), pp.8–9; período2026 deste lote | Reutilizar localização e actividade; perguntar a finalidade jurídica apenas se ausente | S02 identifica cobertura do procedimento; actividades excluídas recebem encaminhamento próprio | Não inferir benefício ou exclusão de IVA a partir deste âmbito | Falta ligação executável da cobertura ao perfil; legislação sectorial especial fica fora deste lote |
| Capital/recursos do exterior ou nacionais; pessoa, residência e constituição separadas | L3/anexo, pp.28–29; R4–6, pp.38–40; período2026 | Reutilizar os dados declarados; pedir origem/forma do aporte quando faltar, sem transformar nacionalidade em classificação do capital | Acrescentar preparação da prova/registo do aporte externo quando aplicável | Nenhuma mudança automática de taxa IVA ou conversão; transferências cambiais não calculadas | Falta dado estruturado de aporte e ligação à documentação; requisitos cambiais concretos não abrangidos |
| Implementadora em constituição ou já registada | R9(1)(b), p.41; R14(1)(b), p.45; desde7/03/2024, recorte2026 | Mostrar a alternativa documental correspondente, sem exigir novo preenchimento da entidade | S05/S06: reserva de denominação ou certidão comercial, com estado declarado/associado por conferir | Não altera IVA | Falta associar categorias documentais existentes à alternativa e ao estado do cadastro |
| Implantação em instalações arrendadas ou com DUAT | R9(1)(c)–(d), pp.41–42; R14(1)(c)–(d), p.46; recorte2026 | Reutilizar local; perguntar apenas a modalidade/evidência ainda ausente | S06 identifica esboço/planta e alternativa contrato/DUAT; sem converter ausência de título em ausência de direito | Não atribui taxa, custo fundiário ou benefício | Falta modalidade estruturada e associação ao documento; sem decidir titularidade/disponibilidade |
| Pedido por representação comercial estrangeira, distinto de qualquer investidor estrangeiro | R9(2)–(3), p.42; recorte2026 | Pergunta de representação apenas quando a implantação declarada o permita; depois referência da licença e documento | S06 acrescenta licença de representação e identifica formalidades documentais pertinentes | Sem alteração da taxa geral | Falta modalidade e origem documental estruturadas; não exigir licença a todo investidor estrangeiro |
| Nenhuma condição de autorização; ou dimensão/actividade/terra que a exija | L22(1)(b)–(2), pp.20–21; R13(2), R14 e R21, pp.45–50; recorte2026 | Reutilizar montante/moeda/actividade/área; perguntar apenas PPP/concessão, implicações ou dados ainda desconhecidos | Seleccionar preparação de mero registo ou autorização; na segunda, S02/S06 acrescentam estudo de viabilidade e capacidade financeira | Comparação do montante exige MZN ou equivalência documentada; não converter nem inventar taxa; IVA permanece componente separada | Falta ligar os predicados e evidências ao perfil. “Não sei” não equivale a “não”. Limite de32mil milhões MZN só resolve a condição de montante, não todas as restantes |
| Localização geral versus ZEE/ZFI com referência e âmbito documentados | R13(1)(a)–(b), p.45; R10(4), p.43; recorte2026 | Reutilizar zona/versionamento; pedir reconhecimento/condições pertinentes em falta | Diferenciar entidade de encaminhamento e articulação aplicável; S08/S17 conservam evidências e pendências | Sem regra executável do benefício, mostrar resultado especial indeterminado e cenário geral separado | a76369eb já apresenta referência/limite; faltam ligações jurídicas de encaminhamento e condições. Parque industrial não equivale automaticamente a ZEE/ZFI |
| Operação mensal interna geral, base líquida, MZN e período coberto pela versão publicada | Regra IVA já fundamentada/autorizada; Lei22/2022 art.17(1), fontes/limites no recibo existente; recorte fiscal2026 | Reutilizar vendas/mês/moeda; perguntar apenas pressupostos fiscais ainda ausentes | Ligar cálculo e relatório à mesma versão de contexto/regra | Consumidor existente calcula por versão servida; zero difere de falta; preço com IVA incluído não vira base líquida | Ligação mensal já demonstrada. Dedução, reembolso, saldo final e benefício especial continuam fora |
| Mudança de título/contacto ou outra condição irrelevante para a regra aplicada | Decisão Tempo22/10 e contratos existentes; escolha técnica de dependências identificada | Preservar a edição e preencher o que continuar compatível | Não reiniciar revisões/etapas sem dependência relevante | Não criar outra execução nem mudar taxa/base | Prova de título e testes de dependências já aceites; estender por dependência, não por “qualquer alteração” |
**Diferença útil que a implementação deste lote deve demonstrar, sem repetir A/B:** um caso C exclusivamente sintético, empresa em constituição, actividade económica não extractiva declarada, instalações arrendadas, capital emMZN, período2026. Primeiro completar as condições de enquadramento; depois a preparação de mero registo mostra a alternativa reserva de denominação/arrendamento. Alterar a condição de autorização para verdadeira troca o resultado procedimental, os campos necessários e os documentos para estudo/capacidade financeira. Com condição desconhecida, a selecção fica indeterminada e pede apenas essa informação. Alterar para representação comercial estrangeira acrescenta a licença pertinente; mudar apenas nacionalidade não produz esse efeito. Alterar de localização geral para ZEE/ZFI documentada altera o encaminhamento e as verificações, sem conceder benefício. O IVA geral continua matematicamente igual quando a mudança não afecta a sua regra; mudar vendas/base/moeda/período altera ou impede o cálculo conforme a cobertura existente. Não forçar um resultado numérico diferente quando a lei não o determina.
**Cadastro e código separados:** o conteúdo jurídico acima está preparado, com artigos, exemplar e início temporal. Falta cadastrar as correspondências versionadas campo/condição/etapa/documento e ligar a sua execução ao perfil existente. Usar o mesmo dossier, prévia/confirmar, definições/questionários versionados e suporte condicional `when` já utilizado nos formulários; reutilizar resolvedor temporal, avaliação e aritmética exacta nas interfaces em que sejam compatíveis, com adaptador explícito para os predicados procedimentais, sem criar outro motor nem armazenar procedimentos como imposto. O contexto já tem sector/actividade/localização/moeda/período/entidade; as respostas de modalidade e evidências ainda não ligadas devem ser acrescentadas ao mesmo contexto cifrado, com origem e alterações deliberadas. Reutilizar vínculos documentais/históricos e permissões existentes. Não alterar a definição publicada dos19passos nem activar outra regra fiscal para executar este cadastro.
**Estado desta entrega:** revisão de a76369eb concluída no âmbito aceite; **percurso seguinte fundamentado e delimitado, ainda não implementado/demonstrado de ponta a ponta**. A tabela não é prova de funcionalidade nem declara licenciamento sectorial completo. Não integrar este documento como se instalasse o percurso. Próxima execução técnica concreta: ligar as condições acima, persistir decisões/documentos e verificar apenas as transições de C, recuperação e relatório; conservar as provas aceites de contexto e IVA. Código, publicação de parâmetros/regras e activação continuam operações separadas. Não há nova autorização organizacional ou validação jurídica da Tempo presumida.
### Percurso de investimento privado executável em QA — 22/10/2026
Implementação do delta de `52fbb146`, na branch `codex/private-investment-path-executable-QA`. A integração independente de `a76369eb` não foi repetida nem condicionada por este lote. Não houve escrita na homologação, integração em main, publicação ou activação de regras fiscais.
**Orientação permanente aplicada:** decisão expressa da Tempo de 22/10, registada também em AGENTS.md: legislação, modelos e procedimentos oficiais disponibilizados são a base aceite para implementar; decisões da Tempo orientam a implementação. Não existe nova aprovação jurídica genérica, aprovador adicional ou validação de credibilidade como condição para executar este percurso. Dados concretamente em falta são identificados por campo e não anulam os restantes requisitos. Associação documental não equivale a reconhecimento administrativo; esta distinção é informativa, sem bloquear a gravação ou a preparação.
**Ligação executável:** `PrivateInvestmentPreparation` é o adaptador delimitado das condições procedimentais já documentadas para o contexto cifrado do dossier. Correspondência `private-investment-preparation-2026-v1`, exemplar APIEX/hash anteriormente registados, recorte de início previsto em 2026. Reutiliza campos conhecidos do mesmo projecto/rascunho, permissões, vínculos documentais, prévia assinada, controlo de concorrência, chave idempotente, eventos antes/depois e os 19 passos existentes. Não é outro motor fiscal, catálogo paralelo de benefícios ou novo processo de aprovação. O resolvedor fiscal publicado e a aritmética decimal existentes continuam responsáveis pelos seus cálculos; não se armazenaram predicados procedimentais como impostos.
No dossier normal, «Preparar o pedido de investimento privado» abre o editor de contexto. Valores já declarados ficam recuperados e editáveis, sem nova confirmação por ecrã. Dados condicionais de área só aparecem para extensão de terra/concessão florestal. A prévia mostra ramo antes/depois, documentos novos/inactivos, passos afectados e resultados que precisam de revisão. Apenas a confirmação grava uma revisão; cancelamento conserva o estado anterior. Os requisitos correspondentes aparecem nas etapas S02/S05/S06 e o próximo passo acompanha mero registo/autorização. As dependências formais da definição publicada não foram substituídas por novos bloqueios.
**Fundamento executado:** Lei 8/2023 artigos 2 e 22; Regulamento aprovado pelo Decreto 8/2024 artigos 5, 9, 13, 14 e 21, conforme a tabela de 52fbb146 acima. Na interface, R designa este Regulamento. As exigências têm razão, artigo, período e ligação ao exemplar oficial. As alternativas certidão/reserva e DUAT/arrendamento não são cumulativas. Representação estrangeira determina a licença correspondente; nacionalidade ou residência, isoladamente, não a determinam. Aporte externo tem origem declarada separadamente da nacionalidade. Montante >32 mil milhões MZN, terra >=10 mil ha e concessão florestal >100 mil ha usam comparação decimal exacta, sem conversão cambial presumida. R21 distingue Finanças/Conselho de Ministros conforme as condições efectivamente conhecidas; uma condição positiva suficiente não espera pelas restantes para identificar o pedido de autorização.
| Comparação nova, exclusivamente QA | Campos e documentos | Próximo passo / efeito no cálculo | Prova |
| --- | --- | --- | --- |
| C: empresa em constituição, actividade geral, 1.000.000 MZN, 01/01/2026, modalidade doméstica, capital nacional, instalações arrendadas; condições de autorização negativas | Reserva de denominação, identificação, planta e arrendamento; não exige certidão, DUAT, licença de representação ou estudo/capacidade de autorização | Mero registo; encaminhamento Governador. Nenhuma taxa IVA alterada | UI → prévia → confirmar; contexto v1 |
| C: associar suporte sintético já existente ao arrendamento | Mesmo documento associado por ID; não cria cópia nem o aprova | Conserva o pedido; contexto v2 | UI; um vínculo em desk_documents |
| C: PPP/concessão empresarial passa a sim, modalidade passa a representação estrangeira e instalações a DUAT | Entram estudo/capacidade, licença de representação e DUAT; arrendamento deixa de ser exigido, conservando o documento anterior | Autorização, Conselho de Ministros; S02/S05/S06/S07 para revisão. Esta mudança meramente procedimental não invalida os inputs do IVA | Prévia visual antes de confirmar; contexto v3 recuperado; PNG C-authorization-preview |
| C: recuperação e simulador | MZN, data, título, contexto v3 e preparação por autorização recuperados sem reintrodução; arrendamento inactivo visível no histórico | Mesmo âmbito fiscal geral; nenhuma execução fiscal nova pedida neste ensaio | Dossier normal, simulador e rascunho ligado; leitura persistida e relatório Word |
| Regresso autorização → mero registo; entidade em constituição → existente | Estudo/capacidade/licença deixam de ser exigidos; arrendamento anterior volta associado; certidão substitui reserva, sem apagar histórico | Ramo e requisitos recalculados | Controlo HTTP transaccional dirigido, não repetição visual A/B |
| ZEE/ZFI versus parque industrial | Encaminhamento APIEX condicionado ao enquadramento reconhecido; parque não é convertido em ZEE/ZFI | Nenhum benefício ou taxa especial concedidos | Predicados com referências sintéticas de QA; não prova de elegibilidade de uma zona real |
**Provas novas:** três testes feature, 96 asserções, exclusivamente na base `simulator_fiscal_delta_20261021_test`, loopback 55441, com transacções revertidas e guarda do alvo. Cobrem limiares exactos, desconhecidos, exclusões/período, não inferência por nacionalidade, alternativas e preservação de vínculo, recuperação, idempotência, prévia sem escrita, adulteração, documento de outro dossier recusado e consumidor desligado, incluindo edição independente do título sem perder snapshot ou vínculos. Um teste frontend confere somente o novo conteúdo congelado do relatório. TypeScript e build passaram. Não foram repetidos cálculos decimais, exportadores completos, paginação PDF, CSV ou ensaios A/B aceites.
**Recibo C da fase por autorização (histórico v3):** dossier `4133311b-5426-4cec-900d-1f1484484083`; contexto v3; rascunho `beba1dec-fc05-4820-b099-24952c5ab36c`, v4, SHA256 `4136e82a3b9420ab8f980ec0d7bda6dc2a67b568c18535b5ba0ecc80efd37e37`. Um documento sintético `e5ce142a-1178-46a2-b039-f187139c62cd`, sem pretensão de autenticidade oficial ou reconhecimento; mantém quarentena/scan pendente, sem contornar o carregamento/antimalware. A fixture apenas disponibiliza metadados privados para observar associação, não ensaia novamente uploads e não faz parte de dados/migrações de entrega. Três eventos de mudança do contexto; uma associação documental. C não contém projecção de vendas nem novo resultado fiscal; as provas anteriores dessa componente são reutilizadas.
**Relatório efectivamente transferido:** `relatorio-investmoz-sintetico-qa-c-pedido-de-investimento-privado.docx`, 16.674 bytes, SHA256 `28cfe4b75116c892d3e29fde389de7d067b456761756bab732af625178ee69ce`. Conferido o novo conteúdo: pedido de autorização, versão, fundamentos R14 e exigências não aplicáveis com histórico preservado. Não se repetiu a verificação de paginação/exportadores. Os anexos do dossier não são inseridos silenciosamente no pacote; mantém-se a selecção explícita existente. O relatório identifica a preparação/simulação e não alega submissão ou aceitação administrativa.
**Limitação da ferramenta:** a observação do evento de download excedeu o tempo e a extensão perdeu a ligação. O ficheiro terminou e foi encontrado em Transferências antes de qualquer repetição; conteúdo/hash conferidos localmente. Não se repetiu a exportação nem se classificou o incidente como falha do simulador. A prévia de impacto e recuperação já tinham prova visual; a recepção do ficheiro é prova distinta.
**Limites concretos:** este lote prepara o pedido de garantias/incentivos de investimento privado, não executa submissão administrativa, licenciamento completo por sector ou decisão de elegibilidade. Prospecção/pesquisa/produção extractiva, investimento público/exclusivamente social sem fins lucrativos e períodos fora de 2026 não são cobertos por este adaptador; processamento industrial tem o ramo de autorização documentado. A equivalência de montante noutra moeda não é determinada sem dados/fundamento cambial aplicáveis; condições positivas independentes continuam executáveis. O catálogo visual deste QA contém apenas parques sintéticos, não uma ZEE/ZFI real elegível: encaminhamento especial conferido no predicado, sem declarar prova visual de reconhecimento real. Benefícios territoriais, deduções, reembolsos e apuramento final do IVA continuam indisponíveis; o cálculo geral já autorizado conserva o seu âmbito.
**Utilização e integração:** abrir `http://127.0.0.1:18580/investimentos/4133311b-5426-4cec-900d-1f1484484083` na conta QA habitual. Usar «Comparar e alterar contexto», editar apenas a condição relevante, comparar e confirmar; para documentos, usar o Balcão do mesmo dossier e associar os existentes. QA habilita `investor-path.private_preparation=true` no router privado. A configuração de entrega `PRIVATE_INVESTMENT_PREPARATION_ENABLED` fica desligada por defeito; a homologação não foi alterada. Integração eventual pelo processo Git existente: rever branch/código e limites, incorporar sobre main actualizado preservando alterações concorrentes, build e conferir apenas o delta no destino; habilitação deste percurso é uma configuração separada, não publicação/activação fiscal. Suspender a configuração oculta o novo consumidor e recusa novas escritas específicas, sem eliminar contexto, documentos, eventos, rascunhos ou relatórios congelados. Sem migração ou seed de fixtures no destino. Nenhuma nova ronda de preparação técnica foi identificada para este recorte; revisão/decisão de integração continuam separadas.
**Distinção final conferida, sem nova validação genérica:** Lei 8/2023 artigo 14(b) distingue investimento directo/indirecto; R13 refere expressamente investimento directo nos limiares monetários. Acrescentado o dado «Tipo deste investimento», separado da origem/nacionalidade. Sem esse dado, a via dependente da dimensão permanece indeterminada; para forma indirecta este lote não determina dimensão/encaminhamento monetário, mas continua a avaliar as condições independentes documentadas. PPP/concessão positiva continua a determinar autorização e Conselho de Ministros, sem esperar por esse campo irrelevante à decisão já suficiente. A interface foi conferida: retirar a condição PPP pede exactamente o tipo em falta; declarar directo resolve mero registo e retira estudo/capacidade financeira. Comparação antes de confirmar em `C-final-directness-preview.png` e percurso recuperado em `C-final-path.png`.
Estado final C: dossier v8, contexto **v4**, investimento directo declarado, PPP não, pedido de **mero registo**; quatro eventos de contexto e um documento/vínculo conservados. A modalidade estrangeira/DUAT conservou a edição deliberada. O rascunho v4 e o Word já transferido mantêm o contexto **v3 por autorização**, com hash acima: são prova histórica congelada e não o relatório da revisão v4. Não houve reescrita silenciosa nem reexportação. Ao abrir o simulador, o mecanismo comum de continuidade disponibiliza a revisão actual para o próximo rascunho/relatório; as provas anteriores deste mecanismo continuam reutilizadas.
### Correspondência do novo relatório com o contexto v4 e dependências Git — 22/10/2026
Verificação dirigida solicitada pela Tempo após a avaliação do resumo de `a1fbda06`; nenhuma alteração funcional necessária. A prova anterior só demonstrava o Word congelado v3. Nesta continuação abriu-se o simulador pelo link normal do mesmo dossier C (incluindo o rascunho ligado), recuperou-se automaticamente o contexto v4 e gerou-se **uma** saída Word nova. Não se repetiram cálculos, CSV, paginação, anexos ou ensaios A/B aceites; não houve nova validação jurídica, integração ou alteração fiscal.
| Elemento | Word histórico v3 | Novo Word do contexto v4 |
| --- | --- | --- |
| Identificação dentro do documento | «Revisão 3» e «Contexto congelado v3» | «Revisão 4» e «Contexto congelado v4» |
| Pedido | Autorização | Mero registo |
| Estudo de viabilidade | Documento por associar | Não aplicável; histórico preservado |
| Capacidade financeira | Documento por associar | Não aplicável; histórico preservado |
| Bytes / SHA256 | 16.674 / `28cfe4b75116c892d3e29fde389de7d067b456761756bab732af625178ee69ce` | 16.678 / `a2849427f4a91fa6ab78e433be8b919baca80a0f4f452a4546a5b169b921d753` |
| Cópia identificada para revisão | `outputs/private-path-executable-20261022/C-report-HISTORICO-contexto-v3.docx` | `outputs/private-path-executable-20261022/C-report-contexto-v4.docx` |
O original v3 conserva exactamente os bytes/hash anteriores; a cópia está explicitamente designada **HISTÓRICO**. A revisão congelada no próprio Word identifica o contexto a que pertence, sem o transformar no estado actual. O novo ficheiro foi encontrado em Transferências após o clique de exportação e conferido por leitura das células efectivamente geradas; recibo em `C-report-v3-v4-comparison.json`. O rascunho passou a v5, SHA256 `ba824fe0362ca87b301da55298122796f44fdfb11507830b3fe8f54cf88d132d`, com contexto v4 e preparação por mero registo; dossier v8/contexto v4 e um documento mantidos. Prova visual de recuperação em `C-new-report-context-v4.png`. Não se altera a validade histórica da prova v3.
**Dependências para integração após a avaliação da Tempo:** ancestralidade confirmada `57fc9148 → a76369eb → 52fbb146 → a1fbda06`. `a76369eb` fornece a continuidade comum do contexto/edições e é dependência funcional de `a1fbda06`; pode ser integrado independentemente. `52fbb146` é a preparação documental intermédia, sem código executável. A branch `codex/private-investment-path-executable-QA` inclui essa ancestralidade e a presente prova documental; não aplicar apenas o diff de `a1fbda06` num destino sem a continuidade ou alteração equivalente. Se a76369eb já tiver sido integrado, o merge normal reconhece a ancestralidade e não o reaplica como um segundo desenvolvimento.
Procedimento: após a decisão, recuperar o main actual, rever a comparação e fazer integração Git que preserve as alterações concorrentes; resolver apenas conflitos efectivos, sem reset/force-push de main. A leitura desta continuação encontrou main em `57fc914821d0cefa02caa80a74b3d42e663f2742`; é fotografia do estado, não autorização para ignorar commits posteriores. Conferir a revisão servida e só o delta no destino. A habilitação `PRIVATE_INVESTMENT_PREPARATION_ENABLED` é configuração distinta; não publica nem activa regras fiscais. Nesta execução a branch permanece separada, sem integração/habilitação na homologação. Limitações sectoriais/fiscais explícitas mantêm âmbito próprio e não bloqueiam este recorte concluído.
### Integração autorizada do percurso adaptativo — 22/10/2026
A Tempo autorizou a implementação após o fecho do recorte. Main remoto foi actualizado por avanço directo de `57fc9148` para `064296e1`, preservando a sequência `a76369eb → 52fbb146 → a1fbda06 → 064296e1`; não havia alteração concorrente ou conflito no momento do envio. A cadeia automática recusou a primeira tentativa no teste frontend «server draft snapshots split private identity from economic values»: 188/189 passaram. A versão servida anterior `57fc9148` foi conservada, conforme o mecanismo de recuperação; não se contornou a verificação.
Causa concreta: o adaptador de continuidade acrescentava `compatible_fields_snapshot` vazio na recuperação de um rascunho legado sem nenhuma origem compatível. Correcção delimitada na branch `codex/adaptive-delivery-compatibility-QA`: devolver o rascunho original quando não há origem compatível nem metadados de herança anteriores. A herança com origem/metadados continua executada, preservando edições e limites por dossier. Nove controlos dirigidos da herança e o teste exacto da partição/recuperação passaram; não se reabriram os cálculos, relatórios ou cenários aceites. A correcção deve seguir o mesmo processo automático; não altera regras fiscais, esquema ou dados activos. Recibos da tentativa e da disponibilização final em `outputs/investmoz-adaptive-path-delivery-20261022`.
### Percurso adaptativo disponibilizado na homologação — 22/10/2026, 10:17 Maputo
Autorização expressa da Tempo executada. Main passou de `57fc9148` por avanço directo para `064296e1`, conservando a ancestralidade `a76369eb → 52fbb146 → a1fbda06 → 064296e1`. O remoto não tinha alterações concorrentes adicionais no momento da integração; não houve conflito, reset ou envio forçado. A primeira verificação automática recusou o novo metadado vazio na recuperação legada e conservou a release anterior; a causa e a correcção dirigida constam da secção anterior. A correcção `042ffbde44cac27704d6f8b7ec25b525a42e3575` foi ensaiada em branch e integrada por avanço directo, sem suprimir o teste que detectou a incompatibilidade.
**Revisão efectivamente servida:** `042ffbde44cac27704d6f8b7ec25b525a42e3575`, branch main, build de 22/10/2026 às 10:16:14 Maputo; instalação automática concluída às 10:16:16. Manifesto SHA256 `655eef6e6a5e71c5138c68465a5b2ffc36d2cadeff96e6b35682dbfd19d42e79`. Conferida em `https://teste-investidor.tempo.co.mz/build/revision.json` e no recibo protegido da cadeia. A cadeia correu as verificações próprias; manualmente só foram repetidos os dez controlos afectados pela incompatibilidade. Zero migrations e zero importações no destino, sem copiar fixtures QA.
**Funcionalidade utilizável:** `https://teste-investidor.tempo.co.mz/investimentos` (login normal); abrir ou criar o dossier legítimo do investimento e usar «Preparar o pedido de investimento privado» / «Comparar e alterar contexto». Estão disponíveis as perguntas condicionais, fundamentos, comparação antes de confirmação, requisitos/documentos e próximos passos do lote `private-investment-preparation-2026-v1`, com os limites de 2026 já documentados. A conferência do destino limitou-se à revisão, configuração efectiva e entradas: investimentos redirecciona ao login (302), simulador responde 200. Não se criaram dossiers/contas reais ou sintéticas no destino nem se repetiram os cenários funcionais QA aceites.
**Habilitação concreta, distinta de fiscalidade:** a configuração do percurso `PRIVATE_INVESTMENT_PREPARATION_ENABLED` estava ausente/false; passou a true mediante a sessão SSH habitual autenticada pelo utilizador. Antes da escrita foram conferidos revisão servida e hash da configuração; cópia protegida dos bytes anteriores verificada, alteração limitada a essa chave, proprietário e modo0640 preservados, cache de configuração reconstruída e workers notificados de reinício gracioso. Hash .env anterior `7cdaba591bb5787485a349c24db2fd7c8279436da0eb31502e44db2fffa21d03`; posterior `a6b4e9064cf6dbb2d945b53418a6c9252f9c736ff3fde4016b95b74885a9ee0f`. A leitura posterior confirmou `investor-path.enabled=true` e `private_preparation=true`. A cópia de recuperação permanece privada em `/var/lib/investmoz-deploy/backups/private-preparation-20261022/`; não contém uma publicação de parâmetros e não segue para Git.
**Estado fiscal preservado:** nenhuma publicação/activação de nova regra e nenhuma operação na base activa. O consumidor IVA geral previamente autorizado manteve a mesma disponibilidade, cobertura2026 e mensagem antes/depois. As exclusões sectoriais, forma indirecta/dimensão monetária não determinada, benefícios territoriais e apuramento fiscal completo continuam explícitos e não bloqueiam o lote procedimental concluído.
**Suspensão/recuperação:** desligar somente a chave do novo percurso e reconstruir cache/reiniciar graciosamente os workers mantém dossiers, documentos, respostas, versões, relatórios e históricos. Para reverter configuração, usar o valor anterior apenas depois de conferir o hash actual e preservar alterações posteriores; não substituir todo o .env sem conferir concorrência. A reversão de código/build segue o procedimento de releases já existente, sem restaurar dados antigos. A release anterior foi efectivamente conservada na primeira tentativa recusada. Recibo completo local em `outputs/investmoz-adaptive-path-delivery-20261022/delivery-receipt.json`; a presente actualização documental fica na branch de registo, sem novo push main ou nova disponibilização desnecessária.
## Revis?o das 30 fichas de Inhambane e cat?logo vazio ? 22/10/2026
Leitura remota autenticada, transac??o READ ONLY, base teste_investidor: 30 oportunidades em draft; zero publicadas em toda a base. As consultas independentes de Cabo Delgado e Inhambane e a consulta exacta Inhambane/Vilankulo devolveram zero sem falha de consulta. A alega??o de oportunidades publicadas de Cabo Delgado n?o se confirma neste destino; n?o inventar associa??es para preencher a lista.
As 30 localiza??es permanecem nos recursos privados opportunity_location_drafts, referidos no cadastro can?nico; os campos e v?nculos p?blicos ainda n?o s?o a projec??o dessas fichas. A inexist?ncia desses v?nculos p?blicos em rascunhos n?o prova perda da localiza??o. Na publica??o, projectar somente a abrang?ncia documentada e conferir a pesquisa territorial.
Decis?o Tempo nesta conversa: incompletude e autoriza??es necess?rias ? execu??o n?o s?o impedimentos autom?ticos ? divulga??o como oportunidade em prepara??o. Proposta: divulgar resumos das 30 fichas em prepara??o com fonte, edi??o, data-base e lacunas vis?veis; n?o declarar financiamento, terreno, licen?as, promotor autorizado ou execu??o confirmados. Valores hist?ricos, agregados e divergentes devem ser identificados sem somas indevidas. Nenhum impedimento concreto de divulga??o do resumo foi demonstrado nesta leitura. Isto n?o declara o conte?do privado integral public?vel: originais, contactos e evid?ncias continuam separados. N?o houve publica??o, altera??o de pap?is ou escrita remota.
Prepara??o por ficha: outputs/investmoz-territory-recovery-20261022/publication-review.json (30 identidades reais, localiza??o privada, lacunas e valores). Evid?ncia: read-catalogue-remote.py e remote-read.log no mesmo direct?rio. A tabela hist?rica de lacunas de prepara??o ? reutilizada, sem a converter em bloqueios legais. A prepara??o t?cnica do fluxo de publica??o ainda deve reconciliar essas condi??es com o contrato existente; n?o marcar publica??o como pronta ou executada.
O URL recebido ? do cat?logo/iniciar por territ?rio, n?o do dossier afectado. N?o permite ler o contexto inicial, a altera??o e a ?ltima ac??o desse processo. A falha da pr?via territorial e do t?rmino sem resposta continua aberta e ainda n?o foi reproduzida no dossier real; n?o encerrar pela disponibiliza??o anterior ou por esta leitura de cat?logo.
<!-- search-territory-recovery-20261022 -->
## Continuidade territorial da pesquisa — correcção em QA, 22/10/2026
Esclarecimento da Tempo: o percurso reportado foi navegação pública entre carteira e catálogo, sem clicar em iniciar/continuar ideia. Não existe evidência de alteração de um dossier pessoal neste caso. Retira-se a exigência de URL do dossier e de confirmação de impacto; a alteração é de filtros de pesquisa.
Causa verificada: ligações da carteira para oportunidades e zonas e ligações dos menus públicos usavam destinos sem província/distrito. O contexto transportado para detalhes por `catalogue_query` também não acompanhava as ligações cruzadas. No ecrã de zonas, os parâmetros de exportação/detalhe omitiam a hierarquia territorial.
Correcção: `searchTerritoryHref` transporta exclusivamente os quatro níveis territoriais na URL entre carteira, oportunidades, zonas e educação, incluindo os respectivos detalhes. Reutiliza o `catalogue_query` existente e preserva os filtros próprios do destino. Uma selecção explícita de outro ascendente não herda descendentes incompatíveis. Sem armazenamento global, mistura de contas ou alteração de dossiers. Menus, navegação relacionada, breadcrumbs e ligações cruzadas usam a mesma função. O selector partilhado mantém a limpeza dos descendentes; não há confirmação adicional para pesquisar.
Verificação dirigida: seis testes Node (incluindo o selector partilhado), TypeScript e build isolado. No navegador QA: carteira Cabo Delgado → selecção Inhambane/Vilankulo → pesquisar → catálogo com ambos os campos preenchidos; recarregamento e regresso preservam a selecção; zonas herdam os dois níveis; mudar para Cabo Delgado limpa Vilankulo. Contagem zero, filtros activos e mensagem de ausência coerentes na base QA consultada. Não foram repetidos cálculos ou exportadores fiscais nem publicados registos para produzir resultados artificiais.
Provas: `outputs/investmoz-territory-recovery-20261022/before-carteira.png` e `after-catalogue.png` no workspace de entrega; testes em `tests/frontend/search-territory.test.mjs` e `territory-path.test.mjs`; logs QA `outputs/territory-types.log` e `territory-build.log`. O router privado QA selecciona o build isolado; não integra a candidata.
Entrega limitada a branch/QA. A homologação não foi alterada por esta correcção. Publicação das 30 fichas de Inhambane permanece separada: revisão por ficha preparada, nenhuma publicação executada; ainda por reconciliar a preparação técnica da divulgação em preparação com o fluxo existente. A correcção da pesquisa não depende dessa publicação.
## Inhambane — divulgação dos 30 resumos executada, 22/10/2026
Decisão expressa da Tempo na conversa: «publicar» os 30 resumos como oportunidades em preparação; responsável confirmado `nala@tempo.co.mz`. A conta concluiu autenticação normal com MFA. A primeira ligação abriu indevidamente `/management/opportunities`, que exige administração geral; `/minhas-oportunidades` estava acessível e foi confirmado no navegador. A ligação SSH estava pronta; não foi necessário pedir outra autenticação.
Executada uma operação privada de dados, limitada às 30 identidades e aos hashes das versões autorizadas, sob a decisão de divulgação da Tempo e a sessão real de Nala. Não se atribuiu papel geral de Publicador, administração, revisões técnicas, aprovação jurídica ou autorização de execução. O fluxo genérico de aprovação de projectos completos permaneceu intacto. A publicação promocional tem finalidade própria registada em `workflow_context.publication.purpose=promotional_preparation`; não equivale à conclusão desse fluxo. Cada operação registou responsável, conteúdo anterior, hash do lote e finalidade no histórico restrito.
O executor preserva o cadastro canónico privado, proprietário e organização, originais e contactos. A projecção pública contém título, resumo, limitações, fonte/edição e indicação «Em preparação». Montantes não confirmados não são apresentados como investimento disponível; os dados técnicos/financeiros históricos são assinalados como tal. A conferência editorial retirou instruções internas dos resumos, usando as limitações da tabela aprovada na conversa.
Detectado e resolvido o vínculo territorial em falta: as localizações privadas mencionavam Vilankulos, mas só tinham província estruturada. O cadastro público confirma MUNICÍPIO DE VILANKULO → posto VILANKULO → distrito VILANKULO → Inhambane. Foram associados ao distrito apenas 15 registos cuja localização documental menciona expressamente Vilankulos, incluindo programas de abrangência múltipla. Não se inferiu implantação, coordenadas ou relação por proximidade. As restantes fichas conservam a abrangência documentada; não foram associadas indiscriminadamente ao distrito.
Verificação pública sem sessão da aplicação: **30 resultados em Inhambane, 15 em Vilankulo**, todos com indicação «Em preparação», limitações e sem `management_draft`/`workflow_context` na resposta pública. No navegador foram conferidos catálogo, contagem e a ficha do Hotel Boutique no Terreno INATUR, incluindo a ausência de confirmação da disponibilidade do terreno/concessão. Foram reutilizadas as provas anteriores; o ensaio novo em base QA isolada conferiu publicação, alteração editorial, repetição sem duplicados, preservação do cadastro privado, recusa de alteração concorrente e associação exacta das 15 referências. Todos os dados sintéticos foram revertidos por rollback em QA; nenhum actor QA foi transportado.
Backup fresco em formato PostgreSQL custom antes de cada revisão de dados, protegido fora da raiz pública; catálogo de restauro verificado e provas anteriores de restauro reutilizadas. A leitura final confirmou as mesmas 30 identidades, cadastro privado/proprietários preservados, zero novos papéis Publicador e administração geral de Nala mantida desligada. A reversão deve partir do backup e do `before_data` restrito, comparar a versão actual e restaurar apenas as projecções/vínculos afectados, preservando edições posteriores; não restaurar toda a base sobre alterações concorrentes. Uma suspensão deve retirar os resumos do público, mantendo cadastro, relações e histórico, como operação expressamente registada; não apagar as fichas.
Recibos no workspace de entrega: `outputs/investmoz-territory-recovery-20261022/public-verification.json`, `integrity-receipt.log`, `publication-final-result.log`, `published-vilankulo-results.png`; executores privados e manifestos no mesmo directório, arquivados fora da raiz pública no destino. Hash final do lote: `d0a1146a7113aabafc3c2d08c0bdc17d12fc8a51606ba9ff7e79b154556d6e08`.
Revisão efectivamente servida: `042ffbde44cac27704d6f8b7ec25b525a42e3575`. Nenhuma instalação Git, migration ou activação fiscal foi executada nesta publicação. A candidata `9ac623a4` de continuidade dos filtros permanece separada de `main` e pronta para revisão de integração. A divulgação das 30 fichas deixa de estar pendente; os estudos, financiamento e autorizações de execução continuam como limitações explícitas das oportunidades.
## Integração territorial e contacto comercial — 22/10/2026
Tempo autorizou integrar `9ac623a4` e pediu conferir a revisão servida. `57fc9148` é antepassado de `042ffbde`, e não uma revisão posterior: os hashes não têm ordem cronológica. A conferência remota comparou 685 ficheiros de aplicação/configuração com `042ffbde`, sem diferenças; o manifesto de build também coincide com o identificador publicado. O histórico de instalação confirma a passagem de `57fc9148` para `042ffbde`. Não se identificou reposição de código/build ou desaparecimento da entrega do simulador.
Integração Git por avanço directo até `1ec31856`, incluindo `9ac623a4` e recibos documentais. A primeira tentativa automática preservou a revisão anterior por falha Node: importação sem extensão em `catalogue-context.ts`. Corrigida a referência explícita `.ts`; passaram os dez testes dirigidos do módulo afectado e da continuidade territorial. Não se repetiram os percursos aceites nem a publicação de oportunidades.
Decisão expressa da Tempo: o rodapé deve apresentar **comercial@tempo.co.mz**, porque o contacto comercial com a instituição é feito por esse endereço. Actualizado o valor por omissão de `portal.institutional_contact_email`. Na homologação existia uma sobreposição explícita `PORTAL_INSTITUTIONAL_CONTACT_EMAIL=info@tempo.co.mz`: alterada exclusivamente essa chave para `comercial@tempo.co.mz`, com backup privado íntegro, propriedade/permissões preservadas e reconstrução da cache de configuração. A apresentação e o `mailto:` utilizam a mesma configuração existente; não houve envio de email.
Recibos de destino e integração: `outputs/investmoz-territory-recovery-20261022/revision-before-integration.log`, `footer-email-receipt.log` e recibo final de instalação no mesmo directório do workspace de entrega. A confirmação final deve identificar a revisão efectivamente instalada pelo processo automático, mantendo os 30 resumos e as regras fiscais existentes.
## Selectores especializados do catálogo — QA, 22/10/2026
Pedido da Tempo: explorar estruturas públicas independentemente da existência de oportunidades, sem inventar associações nem divulgar registos privados. A homologação em `7b1c3579` já devolve e apresenta **Parque Industrial de Balama** e **Parque Industrial de Montepuez** no selector de Cabo Delgado; “Todos” é o valor inicial, não uma prova de ausência das opções. A API devolve duas zonas nessa província e oito no âmbito nacional. A base QA local tem outro conjunto de dados: não continha parques públicos ligados a essa província. Esta diferença de dados não é uma regressão demonstrada do selector da homologação.
Falhas verificadas no código: actividades e cadeias eram reduzidas às oportunidades publicadas encontradas no território/sector; estruturas públicas podiam depender da existência adicional de uma ficha publicada da carteira; os selectores vazios só apresentavam “Todos”; a consulta da ficha de um parque sem oportunidades não era apresentada no resultado vazio da grelha.
Correcção na branch `codex/catalogue-specialised-selectors-QA`: opções provenientes dos cadastros públicos existentes, preservando os critérios de consulta e os vínculos territoriais. Estruturas com território principal documentado não dependem de uma oportunidade nem de uma ficha adicional da carteira. Mantêm-se os vínculos territoriais já resolvidos pelas fichas publicadas. Cadeias acompanham os clusters públicos do âmbito seleccionado. Actividades são o catálogo público de classificação, explicitamente separado da disponibilidade local de investimentos; não se inventa uma associação territorial ou sectorial. Os oito selectores apresentam contagem ou explicação de ausência. Uma selecção indisponível permanece identificada, com possibilidade de remoção, em vez de parecer “Todos”.
Quando a estrutura seleccionada não tem correspondências, o resultado informa “Sem oportunidades publicadas associadas com estes filtros” e mantém ligação à ficha pública existente, em qualquer vista. Um registo mestre sem ficha pública detalhada é identificado como tal; não se cria uma ficha ou publicação automaticamente. A pesquisa continua a combinar todos os filtros, sem alargamento automático ou atribuição de benefícios. A alteração territorial mantém a limpeza dos níveis e selecções incompatíveis já implementada.
Provas novas, sem repetir os percursos anteriormente aceites: Cabo Delgado com dois parques sintéticos identificados; selecção de Balama, zero oportunidades, abertura da ficha, regresso e recarregamento com selecção conservada; limpeza; Nampula com parque sintético Topuito; ausência de restrição territorial com ambos os conjuntos disponíveis e selecção anterior limpa. Fixtures limitadas à base `simulator_fiscal_delta_20261021_test`, porta 55441, excluídas do Git e da disponibilização. Testes `CatalogueSelectorReferencesTest`: três testes, 27 verificações, transacções revertidas, incluindo exclusão de privados/outro território, catálogo de actividades independente dos resultados, estruturas/cadeia sem oportunidades ou fichas adicionais. `npm run types:check` e build QA passaram. Provas locais: `outputs/catalogue-selectors-20261022/audit.json`, `qa-balama.png`, `qa-nampula.png`, `qa-national.png` e logs de build/tipos no mesmo directório.
Diagnóstico separado do mapa: API pública de Cabo Delgado devolve território cadastrado, zero limites e nenhuma geometria acessível. Leitura SSH em transacção **READ ONLY** confirmou zero linhas em `geo_geometries` para essa província; neste caso não é uma camada existente ocultada pelo perfil. Não foram criadas coordenadas, geometrias, vínculos ou permissões. Este diagnóstico não equivale à implementação dos limites administrativos.
Integração pendente da revisão prevista: comparar com `origin/main`, preservar alterações concorrentes e integrar pelo processo Git automático existente. Não há migrations, publicação de oportunidades, activação fiscal ou transporte das fixtures. Conferir apenas o delta e a revisão servida depois de autorizada a integração; não repetir as provas já aceites.
### Revisão dirigida da interacção do selector — 22/10/2026
Na candidata `1d921c9d`, ensaio pelo navegador Edge em QA, sem usar `selectOption` como prova desta interacção: clique no campo/seta deu foco; teclado `Alt+Down`, seta para cima e Enter mudou a selecção de Montepuez para Balama, actualizando o filtro e a ligação à ficha. Prova local `outputs/catalogue-selectors-20261022/qa-keyboard-balama.png`. Dados sintéticos explicitamente identificados como QA.
A ferramenta não observou o menu nativo aberto pelo rato; a tentativa de clicar na opção nativa devolveu opção não visível. Assim, a escolha completa **apenas pelo rato permanece não demonstrada** nesta revisão. Trata-se de limitação de observação/interacção da ferramenta, sem evidência suficiente para atribuir falha ao portal. A presença das opções na API/DOM e o uso anterior de `selectOption` não encerram esse ponto. Sem alteração funcional, repetição dos restantes testes ou integração em `main`; geometrias continuam como pendência distinta.
### Decisão de integração — 22/10/2026
A Tempo encerrou a revisão de `1d921c9d` com as provas existentes e autorizou integrar a candidata e o registo documental `916819eb`. A interacção exclusivamente pelo rato permanece não observada pela ferramenta, **sem carácter bloqueante**; não constitui falha comprovada do portal, não exige substituir o selector nem repetir ensaios. A ausência de geometrias administrativas de Cabo Delgado permanece independente. Executar o processo Git automático existente, preservando alterações concorrentes; não transportar fixtures QA, publicar dados ou activar regras fiscais.
## Entrada única de gestão — preparação em QA, 22/10/2026
Origem: prioridade expressa da Tempo para a equipa cadastrar oportunidades e carregar, editar e publicar documentos e legislação pela interface, sem depender do Codex. A decisão de 22/10 sobre fontes e execução mantém-se. Geometrias administrativas ficam no saldo separado.
Implementada `/gestao-conteudos`, acessível pelo painel, menu lateral, Balcão e editores. Reutiliza os cadastros, biblioteca, armazenamento, perfis e histórico existentes. Nova oportunidade abre o editor operacional para quem tem contexto de criação; acesso legado é identificado como tal, sem o apresentar como publicação operacional completa. A entrada não atribui papéis. Escolhas técnicas deste delta: navegação agregada, contagens no âmbito do utilizador, pesquisa documental e pré-selecção do ficheiro acabado de carregar. Não são novas regras organizacionais.
Percurso diário: **Gestão de conteúdos e oportunidades → Nova oportunidade**, escolher promotor/modalidade, preencher e guardar; recuperar em **Gerir oportunidades e histórico**. Para documentos: **Novo documento** ou **Nova legislação → Receber documento em privado → Guardar em quarentena → Registar documento na biblioteca**. O original recém-carregado fica seleccionado; título e emitente conhecidos são recuperados. Completar os metadados, guardar, pesquisar o registo e usar **Editar metadados**, **Publicar** ou **Retirar da publicação**. Associações apresentam nomes dos registos existentes. Campos ainda não conhecidos não são inventados. Publicação para consulta não activa cálculos.
Verificação nova: quatro testes, 99 asserções, com transacções revertidas exclusivamente em `simulator_fiscal_delta_20261021_test`, loopback 55441, guarda explícita. Demonstrados acesso de equipa sem administração geral, recusa a utilizador sem autorização e ciclo documental completo (upload, ClamAV real, recuperação por pesquisa, edição, publicação/download, retirada, preservação do hash e quatro revisões). Exemplo documental exclusivamente sintético, sem origem oficial ou valor jurídico. Corrigida a classificação de retirados: o histórico existente usa `unpublished`; o filtro e o rótulo reconhecem também `withdrawn` histórico. TypeScript e build QA passaram.
No navegador, login e MFA normais da conta sintética de equipa; entrada → legislação; entrada → nova oportunidade → guardar preparação incompleta → recarregar, com título recuperado e ficha QA 241. Não foi repetido o workflow integral de revisão/publicação de oportunidades já existente; a prova nova cobre a entrada e a gravação/recuperação. O ensaio documental completo foi HTTP transaccional, não uma prova visual de cada clique. A conta QA tem autorizações explicitamente sintéticas; não comprova as designações das contas reais. Não transportar fixtures, configurações QA ou concessões de acesso.
Provas locais: `outputs/content-management-20261022/entrada-final.png`, `build-final.log`, e `tests/Feature/ContentManagementEntryTest.php`. Estado: candidata em branch `codex/content-management-entry-QA`, ainda sem integração ou instalação na homologação. Não declarar encerrada a autonomia operacional de todas as contas/categorias apenas por esta entrada. Não houve migration, publicação remota, alteração de papéis activos ou activação fiscal.
## Entrada única — integração e conferência real, 22/10/2026
A pedido expresso da Tempo, revista e integrada por avanço directo a candidata 97d51db6, seguida de aa2b3d05 (ligação da antiga página de acesso para a entrada autorizada) e 8286d750 (restituição do acesso editorial nominal). Push em main e instalação pelo processo Git automático existente; revisão efectivamente servida 8286d7504d9d368de81b4e8d095398764278bb95, build 22/10/2026 19:04:38 UTC. Nenhuma migration, publicação de conteúdo ou alteração de atribuições activas nesta entrega.
A leitura da sessão real de Osmane Nala após login/MFA confirmou a dupla 15/16, Promotor e Validador activos da conta 15, uma atribuição anterior revogada preservada e nenhuma atribuição Publicador na lista actual. Criação abre as seis categorias atribuídas e os promotores reais; a lista recupera os 30 resumos já publicados e a fila Validação e pareceres abre sem casos. Isto não afirma revisão de um caso inexistente nem publica para ensaio. Atribuições futuras de Aurora/Palmira/Mario conservam a decisão da Tempo e não são pré-condição dos acessos de Nala/onala. Não foi repetido login da outra titular; reutiliza-se a prova nominal anterior e a mesma regra editorial delimitada.
Falha concreta encontrada no destino: `portal.homologation_only=false` fazia `DocumentManagementAccess::editorial` recusar a designação já aprovada. A reserva do host era escolha técnica, não condição organizacional da edição. 8286d750 remove apenas essa dependência; preserva lista nominal Nala/onala, dupla existente, MFA recente, exclusão de Auditor e âmbito documental. Um teste novo, 24 asserções, exclusivamente em QA isolada com rollback, demonstrou independência da reserva e recusa sem designação/MFA. Não foram repetidos os quatro testes/99 asserções anteriores. TypeScript foi conferido para o delta da antiga página de acesso; build/testes obrigatórios pertencem ao processo automático.
Na mesma sessão real, após instalação, a entrada passou a mostrar Novo documento e Nova legislação e 29 publicados/2 não publicados. A biblioteca abriu com Receber documento em privado, Guardar em quarentena, Registar documento na biblioteca, pesquisa, Editar metadados, Publicar/Retirar e históricos. ClamAV real 1.4.3 disponível, 32 ficheiros limpos no âmbito. Nenhum conteúdo foi alterado ou publicado para repetir prova já aceite em QA.
**Dependências que permanecem explícitas:** (1) o fluxo normal de oportunidades tem interface, mas a lista actual não tem Publicador activo; falta a disponibilidade da conta/função já designada, não um comando do Codex. Nala não recebe outro papel por inferência. (2) A divulgação promocional de resumos em preparação, usada no lote Inhambane, foi operação por executor privado; ainda não existe acção genérica equivalente na interface. A candidata não encerra essa autonomia específica, nem as variantes de submissão que o editor já identifica como não implementadas. (3) manutenção de indisponibilidade persistente do scanner/worker continua técnica; as operações documentais normais não exigem comandos. Geometrias administrativas permanecem em saldo separado.
Link único e guia curto encontram-se no início deste manual: https://teste-investidor.tempo.co.mz/gestao-conteudos. Recibo e imagem da conferência: outputs/content-management-delivery-20261022/. Registo documental mantido na branch QA, sem novo push main/desencadeamento de instalação apenas para o recibo.
## Divulgação promocional pela equipa — implementação QA, 22/10/2026
Origem: instrução expressa da Tempo nesta conversa para eliminar a dependência do executor privado na divulgação corrente de resumos em preparação. A autorização promocional é distinta de aprovação interna/publicação de uma oportunidade completa e de autorização externa de execução. Não se criaram papéis nem aprovadores. Reutiliza-se o Promotor **interno**, no âmbito operacional e cadastro que lhe foram atribuídos, e o Publicador no seu âmbito existente. O Promotor externo não ganha publicação por esta alteração. Auditor, âmbito alheio, MFA ausente/expirado e publicação normal já disponibilizada continuam a ser recusados pelas regras existentes. Esta passagem limita-se a QA; nenhuma atribuição ou conteúdo activo foi alterado.
A lista e a ficha da oportunidade oferecem **Divulgação promocional**. O editor recupera o último resumo deliberadamente divulgado; não copia automaticamente texto, contactos ou documentos privados. A prévia apresenta título, resumo, descrição com aviso, fonte/edição/data-base e abrangência escolhida. Ausências são explicitadas, não preenchidas por presunção; a incompletude do projecto não exige a submissão completa para divulgar o resumo. Confirmar vincula o utilizador, operação, conteúdo exacto e versão da ficha à prévia cifrada, válida 15 minutos. Prazo, chave idempotente, serialização e comparação de versão são escolhas técnicas de integridade, não novas aprovações organizacionais. Cancelar não escreve. Alteração concorrente ou referência territorial modificada exige outra prévia, sem substituir o cadastro.
Os mesmos registos de oportunidade, contexto privado e auditoria são reutilizados. O rascunho original mantém os bytes; as colunas privadas anteriores à projecção pública ficam preservadas no contexto oculto e no histórico restrito. A projecção não transporta anexos, contactos, montantes privados, validação documental ou perfis privados. A consulta pública promocional recupera apenas a abrangência explicitamente divulgada; não publica automaticamente evidências ou relações do dossier. Retirar deixa o resumo indisponível em público, preservando a ficha, documentos, relações privadas e histórico. Não equivale a arquivar ou rejeitar o investimento.
Ensaios novos em base isolada `simulator_fiscal_delta_20261021_test`, loopback 55441, com guarda e transacções revertidas: ciclo de prévia/divulgação/actualização/retirada/nova divulgação, repetição sem duplicar auditoria, preservação do original privado e metadados/vínculo de documento em quarentena, projecção pública sem conteúdo privado, ausência de concessão de papéis/aprovação, recusa de sessão/âmbito não autorizados, prévia adulterada/expirada/desactualizada e recuperação. A integração com o middleware foi ajustada para delegar a autorização destas duas operações ao serviço promocional; um Publicador não recebe por isso edição do rascunho.
No navegador, conta sintética de Promotor interno autenticada normalmente com MFA; ficha sintética 255, `SINTETICO-QA-PROMO-20261022`: prévia revista → confirmação → ficha pública com Em preparação, fonte, lacunas e abrangência Cabo Delgado → recarregar e recuperar → actualizar o resumo → rever e confirmar → retirar com prévia → consulta pública 404 e histórico com três operações. Não foram usados comandos para executar essas operações. A fixture preparou apenas o cadastro/identidade de teste e ficou retirada do público QA; não é dado de entrega. A primeira tentativa MFA da fixture tinha formato de cifra incompatível com Fortify e foi corrigida apenas na fixture; não se alterou a autenticação nem se utilizou essa tentativa como prova. Provas em `outputs/promotional-interface-20261022/01-previa.png`, `02-historico-retirada.png`, `tests-final.log`, `types.log` e `build-final.log`.
Para Mario, mantém-se a designação de Publicador já aprovada, sem pedir outra decisão da Tempo. A gestão de perfis passou a permitir seleccionar a conta existente por nome/email e mostrar a verificação de email/MFA, em vez de exigir descoberta de IDs por comandos. Quando a conta estiver disponível e cumprir os requisitos existentes, a dupla autorizada aplica a atribuição interna no mesmo ecrã de perfis e no âmbito aprovado, com auditoria. Não foram criadas contas nem aplicados papéis no activo. A indisponibilidade de outras contas não bloqueia as operações já autorizadas.
Fecho deste recorte: as operações correntes incluídas têm percurso de interface em QA; a divulgação promocional já não exige o executor privado. Provas documentais, criação/gravação normal de oportunidades e revisão/publicação normal já aceites foram reutilizadas. Não se declara nova demonstração integral desses fluxos nem instalação no activo. Permanecem separados: disponibilização da candidata pelo processo Git após revisão, atribuições reais quando as contas estiverem disponíveis, variantes de oportunidades ainda não implementadas e geometrias administrativas. Publicação documental continua a disponibilizar consulta, sem activar cálculos fiscais.
Validação final deste delta QA: cinco testes/81 asserções passaram em 116,299 s, com transacções revertidas; TypeScript e build QA passaram. As provas visuais cobrem as três operações promocionais novas; as provas documentais e do workflow normal continuam reutilizadas, sem repetição. Nenhuma fixture, segredo, build QA ou atribuição sintética integra os ficheiros versionados.
@@ -7382,3 +7382,11 @@ P1,AUD-SEARCH-T09,QA — revisão Tempo pendente,Gestão de procura/capacidade p
P1,AUD-SEARCH-T10,QA — revisão Tempo pendente,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,"Resumo único nomeado Documentos, chips removíveis, controlos vazios ocultos sem apagar valores seleccionados, rodapé comum.",,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portal/services; portal/document,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
P1,AUD-SEARCH-T11,QA — revisão Tempo pendente,"Situação jurídica, consulta e cálculo continuam independentes.",Reutiliza a correcção da biblioteca e preserva filtros jurídicos/documentais pertinentes.,,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portal/services; portal/document,"Situação jurídica, consulta e cálculo continuam independentes.",,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
P1,AUD-GUIDE-CARD-20261020,QA review pending,New delta; not approved for main,Common educational card and existing source/context adapter,,Desktop/mobile/keyboard; public-state and source checks,,,,,,educational presentation,Tempo 20/10/2026; same manual,,,Tempo visual review; library integration verified on existing QA; previous dossier proof retained,,Integration approval for new delta,,,,1 test / 8 assertions; TypeScript/build; real public-source visual preview,,,,No import; no remote changes; no claim of new dossier proof,
P1,AUD-SIM-INT01,Previous inline-date proof retained; unchanged delta not repeated,Invalid one-off fiscal date can throw instead of returning validation state,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Previous inline-date proof retained; unchanged delta not repeated,,,,No claim of full review completion,
P1,AUD-SIM-INT02,QA zero rules verified with full visibility; prepared archive and analyses reused; current homologation read awaits SSH authentication,Review actual fiscal parameters sources periods and eligibility,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,QA zero rules verified with full visibility; prepared archive and analyses reused; current homologation read awaits SSH authentication,,,,No claim of full review completion,
P1,AUD-SIM-INT03,Two dossiers and new login recovered in browser; cashflow persistence corrected and HTTP-tested; new visual cashflow delta pending,Review isolation recovery across projects and sessions,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Two dossiers and new login recovered in browser; cashflow persistence corrected and HTTP-tested; new visual cashflow delta pending,,,,No claim of full review completion,
P1,AUD-SIM-INT04,Historical results visible after fix; 12 and 24 recovered; hashes unchanged; synthetic temporal resolver versions verified in memory only,Review stale and historical results and correct dossier,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Historical results visible after fix; 12 and 24 recovered; hashes unchanged; synthetic temporal resolver versions verified in memory only,,,,No claim of full review completion,
P1,AUD-SIM-INT05,Independent business-plan and cashflow ledgers passed; complete credit recipe included in screen and exports,Independent financial controls unknown zero units and currencies,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Independent business-plan and cashflow ledgers passed; complete credit recipe included in screen and exports,,,,No claim of full review completion,
P1,AUD-SIM-INT06,Normal UI CSV and DOCX match shared values; all six DOCX pages checked; native Edge PDF awaits user save; new cashflow visual output pending,Generated reports screen download and pagination,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Normal UI CSV and DOCX match shared values; all six DOCX pages checked; native Edge PDF awaits user save; new cashflow visual output pending,,,,No claim of full review completion,
P1,AUD-SIM-INT07,Calculation navigation and failed-save retry fixed and browser-verified; normal downloads verified; interactive attachment blocked by extension permission,Explicit validate calculate save export errors and retry,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Calculation navigation and failed-save retry fixed and browser-verified; normal downloads verified; interactive attachment blocked by extension permission,,,,No claim of full review completion,
P1,AUD-SIM-INT08,Generic HTTP and fiscal-expression validation messages translated; status/correlation verified; selectable translations and source titles preserved,Portuguese across portal and simulator outputs,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Generic HTTP and fiscal-expression validation messages translated; status/correlation verified; selectable translations and source titles preserved,,,,No claim of full review completion,
Can't render this file because it is too large.
@@ -2997,3 +2997,12 @@ Inventário finito AUD-SEARCH-T01–T11 na mesma matriz/CSV e comparação no ma
Verificação complementar concluída: código funcional `3258a25f7659` ligado ao acervo QA existente `legacy_trio_20261015_test`, sem importação. Página normal `/sectores/turismo` apresenta imagem local `/documentos/25/view` (HTTP 200; 900 px naturais) e resumo; «Ler o guia» abre `/turismo`, ligado ao exemplar real `/documentos/10`. A limitação de integração com biblioteca indicada na primeira prévia fica superada por esta prova. Mantém-se a prova anterior do dossier, pois código/contexto não mudaram. Falta apenas revisão visual da Tempo antes de integrar. IA fora do lote.
AUD-GUIDE-CARD-20261020: componente comum com imagem autorizada, título, resumo do artigo publicado, autoria e «Ler o guia». Reutilizado em sectores, exploração editorial e leituras relacionadas; recomendado no catálogo por sector explícito e nas fichas de oportunidades. Sem associação territorial inferida. Botão secundário apenas com oportunidades públicas. QA: teste isolado 1/8, TypeScript/build e exemplos computador/móvel. Falta decisão sobre integração deste novo delta; a prévia visual de Turismo usa uma cópia por leitura do conteúdo público, sem importação. Recuperação documental num dossier com este acervo não demonstrada, pois o artigo não existe na base QA actual. Percurso e regras anteriores preservados; IA fora do lote.
## Integrated simulator review - QA 20/10/2026
AUD-SIM-INT01 through AUD-SIM-INT08 are recorded in the same matrix and manual. Initial diagnosis and targeted date/Portuguese corrections only; full review remains open. Initial effort 18-30 h, subject to discounting accepted evidence and completed work. No main integration, real-data modification or tax parameter activation.
Actualização QA de 20/10/2026: os mesmos oito itens foram reconciliados no manual e nas duas tabelas existentes. Data com mensagem junto ao campo; inventário dos parâmetros efectivos (zero regras fiscais QA); controlos matemáticos independentes; aviso de recálculo; quatro relatórios reais do exportador com 6/6/6/5 páginas; consulta fiscal com erro/nova tentativa e traduções comuns verificados. O formulário personalizado já não exige contexto fiscal que não envia ao cálculo. Restam continuidade autenticada entre dois dossiers/nova sessão e histórico, mudança de parâmetros, controlos delimitados de cash-flow/plano, impressão nativa Edge/comparação pela interface e mensagens dinâmicas/falhas dirigidas de gravação/exportação. Estimativa restante **6–10 h**, distinta das 18–30 h iniciais. Sem main, servidor remoto, importação ou activação fiscal. Exemplos PDF são renderizações do DOCX; não certificam impressão do Edge. Ver o saldo detalhado AUD-SIM-INT01–08 no mesmo manual.
Continuação delimitada: dois dossiers e nova entrada conferidos no navegador; histórico agora visível no percurso de pequeno negócio, hashes preservados; Abrir cálculo encaminha para a etapa financeira; repetição de gravação após 503 corrigida e conferida; controlos independentes de tesouraria/plano passaram. CSV e Word descarregados pela interface coincidem nos valores comuns e apresentam a receita completa do crédito. Seis páginas do DOCX conferidas; PDF nativo ainda aguarda gravação pelo utilizador. QA tem zero regras com visibilidade completa, mas existem três originais fiscais versionados e análises/referências reutilizáveis; não se concluiu ausência na homologação sem autenticação SSH. Versões sintéticas testadas apenas em memória, sem cadastro/activação. Encontrada e corrigida perda dos pressupostos da tesouraria: snapshot no JSON existente, recuperação por dossier e relatório, sem migration; testes HTTP/frontend passaram, falta conferência visual deste novo delta. Saldo **2–4 h** após acessos/ficheiro, detalhado no mesmo manual; integração do motor fiscal permanece separada e não foi implementada. Sem main ou intervenção remota.
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
{
"The provided two factor authentication code was invalid.": "O código de autenticação está incorrecto ou expirou. Introduza um novo código da sua aplicação autenticadora.",
"The provided two factor recovery code was invalid.": "O código de recuperação está incorrecto ou já foi utilizado.",
"The provided password is incorrect.": "A palavra-passe está incorrecta.",
"The provided password does not match your current password.": "A palavra-passe indicada não corresponde à palavra-passe actual."
}
+6
View File
@@ -0,0 +1,6 @@
<?php
return [
'failed' => 'O email ou a palavra-passe não correspondem aos nossos registos.',
'password' => 'A palavra-passe está incorrecta.',
'throttle' => 'Foram feitas demasiadas tentativas. Tente novamente dentro de :seconds segundos.',
];
+29
View File
@@ -0,0 +1,29 @@
<?php
return [
'required' => 'O campo :attribute é obrigatório.',
'required_if' => 'O campo :attribute é obrigatório quando :other tem o valor :value.',
'present' => 'O campo :attribute deve estar presente.',
'filled' => 'Preencha o campo :attribute.',
'numeric' => 'O campo :attribute deve conter um número.',
'integer' => 'O campo :attribute deve conter um número inteiro.',
'decimal' => 'O campo :attribute deve ter :decimal casas decimais.',
'string' => 'O campo :attribute deve conter texto.',
'array' => 'O campo :attribute deve conter uma lista.',
'boolean' => 'O campo :attribute deve indicar sim ou não.',
'email' => 'Introduza um endereço de correio electrónico válido em :attribute.',
'uuid' => 'A referência indicada em :attribute não é válida.',
'in' => 'Seleccione uma opção válida em :attribute.',
'exists' => 'A referência seleccionada em :attribute não foi encontrada.',
'unique' => 'O valor indicado em :attribute já está registado.',
'confirmed' => 'A confirmação de :attribute não corresponde ao valor introduzido.',
'current_password' => 'A palavra-passe actual está incorrecta.',
'date' => 'Introduza uma data válida em :attribute.',
'date_format' => 'Introduza uma data válida para :attribute.',
'before_or_equal' => 'A data em :attribute deve ser anterior ou igual a :date.',
'after_or_equal' => 'A data em :attribute deve ser posterior ou igual a :date.',
'accepted' => 'Confirme o campo :attribute para continuar.',
'between' => ['numeric' => 'O valor de :attribute deve estar entre :min e :max.', 'string' => 'O campo :attribute deve ter entre :min e :max caracteres.', 'array' => 'O campo :attribute deve ter entre :min e :max elementos.', 'file' => 'O ficheiro :attribute deve ter entre :min e :max KB.'],
'min' => ['numeric' => 'O valor de :attribute deve ser igual ou superior a :min.', 'string' => 'O campo :attribute deve ter pelo menos :min caracteres.', 'array' => 'O campo :attribute deve ter pelo menos :min elementos.', 'file' => 'O ficheiro :attribute deve ter pelo menos :min KB.'],
'max' => ['numeric' => 'O valor de :attribute não pode exceder :max.', 'string' => 'O campo :attribute não pode exceder :max caracteres.', 'array' => 'O campo :attribute não pode exceder :max elementos.', 'file' => 'O ficheiro :attribute não pode exceder :max KB.'],
'attributes' => ['base' => 'base de cálculo', 'rate' => 'taxa', 'reduction' => 'redução', 'email' => 'email', 'password' => 'palavra-passe', 'current_password' => 'palavra-passe actual', 'economic_snapshot.fiscal_reference_date' => 'data da operação fiscal'],
];
+82 -272
View File
@@ -7,66 +7,34 @@
@custom-variant dark (&:is(.dark *));
@import './tempo-header.css';
/* Institutional shell adapted from Tempo; page content keeps its own theme. */
.tempo-header {
position: relative;
position: sticky;
top: 0;
z-index: 40;
background: #090909;
color: #fff;
}
.tempo-header-inner {
position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
grid-template-rows: 72px;
column-gap: 0.5rem;
align-items: center;
max-width: 1440px;
margin-inline: auto;
padding-inline: 0.75rem;
grid-template-rows: 84px;
}
.tempo-logo-link {
grid-area: 1 / 1;
display: inline-flex;
width: fit-content;
align-items: center;
flex-shrink: 0;
width: 140px;
border-radius: 0.25rem;
}
.tempo-header-logo {
width: auto;
height: 36px;
object-fit: contain;
}
.tempo-primary-navigation {
display: none;
grid-area: 2 / 1 / auto / -1;
min-width: 0;
border-top: 1px solid #ffffff1a;
}
.tempo-primary-navigation > ul {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.25rem 0.5rem;
padding-block: 0.75rem;
}
.tempo-primary-navigation .tempo-nav-trigger {
font-size: 0.9375rem;
padding-inline: 0.5rem;
}
.tempo-nav-trigger {
display: flex;
width: 100%;
min-height: 44px;
min-height: 48px;
align-items: center;
justify-content: space-between;
gap: 0.4rem;
padding: 0.5rem 0.75rem;
border-radius: 0.4rem;
font-size: 1rem;
line-height: 1.5;
gap: 0.5rem;
padding: 0.6rem 0.75rem;
border-radius: 0.5rem;
font-size: 0.9375rem;
font-weight: 600;
white-space: nowrap;
color: #eee;
transition:
color 0.2s,
@@ -84,85 +52,16 @@
.tempo-nav-trigger[data-state='open'] > svg {
transform: rotate(180deg);
}
.tempo-primary-navigation .tempo-nav-trigger > svg {
transform: none;
flex-shrink: 0;
}
.tempo-section-bar {
border-top: 1px solid #ffffff14;
background: inherit;
}
.tempo-section-inner {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 0.25rem 0.75rem;
max-width: 1440px;
margin-inline: auto;
padding: 0 0.75rem 0.5rem;
}
.tempo-section-navigation {
grid-area: 1 / 1 / auto / -1;
min-width: 0;
}
.tempo-section-navigation > ul {
display: flex;
justify-content: center;
gap: 0.25rem;
}
.tempo-section-navigation .tempo-nav-trigger {
justify-content: center;
min-height: 44px;
gap: 0.25rem;
padding: 0.5rem 0.375rem;
border-radius: 0;
font-size: 0.8125rem;
letter-spacing: normal;
}
@media (min-width: 640px) {
.tempo-section-navigation .tempo-nav-trigger {
min-height: 48px;
gap: 0.5rem;
padding-inline: 0.75rem;
font-size: 1rem;
@media (max-width: 640px) {
.tempo-nav-trigger {
gap: 0.25rem;
padding-inline: 0.25rem;
font-size: 0.8125rem;
}
}
@media (min-width: 768px) {
.tempo-section-inner {
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
padding: 0.25rem 1.25rem;
.tempo-nav-trigger > svg {
width: 12px;
height: 12px;
}
.tempo-section-navigation {
grid-area: 1 / 2;
}
.tempo-section-inner .tempo-panel-summary {
grid-area: 1 / 1;
}
.tempo-section-inner .tempo-interest-button {
grid-area: 1 / 3;
}
}
.tempo-header-extras {
display: none;
grid-area: 3 / 1 / auto / -1;
flex-direction: column;
gap: 0.75rem;
min-width: 0;
padding-block: 0.75rem;
border-top: 1px solid #ffffff1a;
}
.tempo-header[data-expanded] .tempo-primary-navigation {
display: block;
}
.tempo-header[data-expanded] .tempo-header-extras {
display: flex;
}
.tempo-header-utilities {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
}
.tempo-utility-button {
display: flex;
@@ -171,7 +70,7 @@
gap: 0.4rem;
padding: 0.4rem;
border-radius: 0.5rem;
font-size: 0.8125rem;
font-size: 0.75rem;
color: #e2e8f0;
}
.tempo-utility-button:hover {
@@ -193,6 +92,11 @@
transform: translateY(-2px);
opacity: 0.8;
}
@media (max-width: 360px) {
.tempo-header-social {
width: 26px;
}
}
.tempo-login-button {
display: inline-flex;
min-height: 40px;
@@ -200,10 +104,9 @@
gap: 0.5rem;
border: 1px solid #ffffff50;
border-radius: 0.4rem;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
padding: 0.5rem 0.85rem;
font-size: 0.8rem;
font-weight: 600;
white-space: nowrap;
color: #fff;
transition:
background 0.2s,
@@ -213,23 +116,19 @@
background: #ed1c24;
border-color: #ed1c24;
}
.tempo-login-button > span {
max-width: clamp(2rem, calc(100vw - 18rem), 6rem);
}
.tempo-header-actions {
grid-area: 1 / 2;
order: 2;
display: flex;
min-width: 0;
align-items: center;
gap: 0.5rem;
margin-left: auto;
}
.tempo-interest-button {
display: inline-flex;
grid-area: 2 / 2;
justify-self: end;
align-items: center;
justify-content: center;
min-height: 40px;
padding: 0.5rem 0.65rem;
padding: 0.5rem 0.75rem;
border: 1px solid #ed1c24;
border-radius: 0.4rem;
background: #ed1c24;
@@ -243,171 +142,81 @@
background: #c9151c;
}
.tempo-panel-summary {
grid-area: 2 / 1;
order: 4;
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: center;
gap: 0.1rem;
align-items: center;
justify-content: space-between;
gap: 0.5rem;
width: 100%;
min-height: 44px;
font-size: 0.8125rem;
line-height: 1.4;
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:hover {
.tempo-panel-summary a:hover {
text-decoration: underline;
}
.tempo-panel-count {
.tempo-panel-summary span {
color: #cbd5e1;
font-size: 0.6875rem;
white-space: nowrap;
}
.tempo-header-toggle {
grid-area: 1 / 3;
display: inline-flex;
width: 44px;
height: 44px;
flex-shrink: 0;
align-items: center;
justify-content: center;
border: 1px solid #ffffff33;
border-radius: 0.4rem;
color: #fff;
cursor: pointer;
}
.tempo-header-toggle:hover,
.tempo-header-toggle[aria-expanded='true'] {
background: #ffffff10;
@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 {
padding-inline: 1.25rem;
grid-template-rows: 90px;
}
.tempo-header-logo {
height: 44px;
}
.tempo-primary-navigation > ul {
display: flex;
justify-content: space-between;
}
}
@media (min-width: 1024px) {
.tempo-header-inner {
grid-template-rows: 88px;
padding-inline: 1.5rem;
}
.tempo-header-logo {
height: 56px;
.tempo-logo-link {
width: 160px;
}
}
@media (min-width: 1280px) {
/* Shared columns keep each portal area below its institutional link. */
.tempo-header-layout {
display: grid;
grid-template-columns: auto repeat(4, max-content) minmax(0, 1fr) auto;
column-gap: 0.25rem;
max-width: 1440px;
margin-inline: auto;
padding-inline: 2rem;
}
.tempo-header-inner {
grid-column: 1 / -1;
grid-template-columns: subgrid;
min-height: 88px;
column-gap: normal;
max-width: none;
margin: 0;
padding: 0;
grid-template-rows: 96px;
}
.tempo-logo-link {
margin-right: 0.75rem;
}
.tempo-header-logo {
height: 64px;
}
.tempo-header[data-expanded] .tempo-primary-navigation,
.tempo-primary-navigation {
display: grid;
grid-area: 1 / 2 / 2 / 6;
grid-template-columns: subgrid;
border: 0;
}
.tempo-primary-navigation > ul {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
gap: normal;
padding: 0;
}
.tempo-nav-trigger {
justify-content: center;
padding-inline: 0.875rem;
font-size: 1.125rem;
letter-spacing: 0.025em;
}
.tempo-primary-navigation .tempo-nav-trigger {
padding-inline: 0.625rem;
font-size: 1.125rem;
}
.tempo-header-extras {
display: flex;
grid-area: 1 / 6;
justify-self: end;
flex-shrink: 0;
padding: 0 0.75rem;
border-top: 0;
border-inline: 1px solid #ffffff1a;
}
.tempo-header-utilities {
flex-wrap: nowrap;
gap: 0.5rem;
}
.tempo-header-social {
width: 26px;
}
.tempo-header-toggle {
display: none;
width: 152px;
}
.tempo-header-actions {
grid-area: 1 / 7;
order: 6;
margin-left: 0;
}
.tempo-section-bar {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
.tempo-header-inner .tempo-nav-trigger {
padding-inline: 0.5rem;
font-size: 0.9375rem;
}
.tempo-section-inner {
grid-column: 1 / -1;
grid-template-columns: subgrid;
gap: normal;
max-width: none;
margin: 0;
padding-inline: 0;
.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;
}
.tempo-section-navigation {
display: grid;
grid-area: 1 / 2 / 2 / 5;
grid-template-columns: subgrid;
}
@media (min-width: 1440px) {
.tempo-header-inner {
grid-template-rows: 102px;
}
.tempo-section-navigation > ul {
display: grid;
grid-column: 1 / -1;
grid-template-columns: subgrid;
gap: normal;
.tempo-logo-link {
width: 168px;
}
.tempo-section-navigation .tempo-nav-trigger {
padding-inline: 0.625rem;
}
.tempo-primary-navigation .tempo-nav-trigger,
.tempo-section-navigation .tempo-nav-trigger {
justify-content: flex-start;
}
.tempo-interest-button {
padding-inline: 0.85rem;
font-size: 0.8125rem;
}
.tempo-section-inner .tempo-interest-button {
grid-area: 1 / 6 / 2 / -1;
.tempo-header-inner .tempo-nav-trigger {
font-size: 1rem;
}
}
.tempo-header :is(a, button):focus-visible {
@@ -441,7 +250,7 @@
outline: none;
}
.tempo-footer {
margin-top: 2rem;
margin-top: 1.5rem;
background: #050505;
color: #fff;
}
@@ -475,7 +284,8 @@
.tempo-contact-link {
display: flex;
align-items: center;
gap: 1rem;
gap: 0.75rem;
min-height: 40px;
transition: color 0.2s;
}
.tempo-contact-link:hover {
+321
View File
@@ -0,0 +1,321 @@
/* Presentation choice for Tempo's 21/10/2026 request: one shared navigation. */
.tempo-header {
position: relative;
z-index: 40;
background: #090909;
color: #fff;
border-bottom: 1px solid #ffffff14;
}
.tempo-header-inner {
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
grid-template-rows: 71px;
align-items: center;
column-gap: 0.5rem;
max-width: 1600px;
margin-inline: auto;
padding-inline: 0.75rem;
}
.tempo-logo-link {
grid-area: 1 / 1;
display: inline-flex;
align-items: center;
width: 112px;
max-width: 100%;
border-radius: 0.25rem;
}
.tempo-header-logo {
width: 100%;
height: auto;
object-fit: contain;
}
.tempo-primary-navigation {
display: none;
grid-area: 2 / 1 / auto / -1;
min-width: 0;
border-top: 1px solid #ffffff1a;
padding-block: 0.75rem;
}
.tempo-primary-navigation > ul {
display: grid;
gap: 0.25rem;
}
.tempo-nav-trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.4rem;
width: 100%;
min-height: 44px;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
color: #e2e8f0;
font-size: 1rem;
line-height: 1.5;
font-weight: 600;
white-space: nowrap;
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;
background: #ffffff08;
box-shadow: inset 0 -2px #ed1c24;
}
.tempo-nav-trigger > svg {
flex-shrink: 0;
transition: transform 0.2s;
}
.tempo-nav-trigger[data-state='open'] > svg {
transform: rotate(180deg);
}
.tempo-header-extras {
display: none;
grid-area: 3 / 1 / auto / -1;
min-width: 0;
padding-block: 0.75rem;
border-top: 1px solid #ffffff1a;
}
.tempo-header-utilities {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
}
.tempo-utility-button {
display: flex;
align-items: center;
gap: 0.4rem;
min-height: 44px;
padding: 0.4rem;
border-radius: 0.5rem;
color: #e2e8f0;
font-size: 0.8125rem;
}
.tempo-utility-button:hover {
background: #ffffff10;
}
.tempo-header-actions {
grid-area: 1 / 2;
display: flex;
align-items: center;
gap: 0.5rem;
min-width: 0;
}
.tempo-login-button {
display: inline-flex;
align-items: center;
gap: 0.5rem;
min-height: 44px;
padding: 0.5rem 0.625rem;
border: 1px solid #ffffff40;
border-radius: 0.5rem;
color: #fff;
font-size: 0.875rem;
font-weight: 600;
white-space: nowrap;
transition:
background 0.2s,
border-color 0.2s;
}
.tempo-login-button:hover {
background: #ffffff10;
border-color: #ffffff80;
}
.tempo-login-button > span {
max-width: clamp(2rem, calc(100vw - 18rem), 6rem);
}
.tempo-panel-summary {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
flex-shrink: 0;
border-radius: 0.5rem;
color: #e2e8f0;
}
.tempo-panel-summary:hover {
background: #ffffff10;
color: #fff;
}
.tempo-panel-count {
position: absolute;
top: 0;
right: -0.125rem;
min-width: 17px;
padding-inline: 0.2rem;
border: 2px solid #090909;
border-radius: 999px;
background: #ed1c24;
color: #fff;
font-size: 0.5625rem;
line-height: 1.4;
text-align: center;
}
.tempo-interest-button {
display: none;
grid-area: 4 / 1 / auto / -1;
align-items: center;
justify-content: center;
gap: 0.5rem;
min-height: 44px;
margin-bottom: 1rem;
padding: 0.5rem 0.875rem;
border: 1px solid #ed1c24;
border-radius: 0.5rem;
background: #ed1c24;
color: #fff;
font-size: 0.8125rem;
font-weight: 600;
white-space: nowrap;
transition: background 0.2s;
}
.tempo-interest-button:hover {
background: #c9151c;
}
.tempo-header-toggle {
grid-area: 1 / 3;
display: inline-flex;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
border: 1px solid #ffffff33;
border-radius: 0.5rem;
color: #fff;
cursor: pointer;
}
.tempo-header-toggle:hover,
.tempo-header-toggle[aria-expanded='true'] {
background: #ffffff10;
}
.tempo-header[data-expanded] .tempo-primary-navigation {
display: block;
}
.tempo-header[data-expanded] .tempo-header-extras {
display: flex;
}
.tempo-header[data-expanded] .tempo-interest-button {
display: inline-flex;
}
.tempo-header :is(a, button):focus-visible {
outline: 2px solid #fff;
outline-offset: 3px;
}
.tempo-submenu {
max-width: min(
calc(100vw - 1.5rem),
var(--radix-dropdown-menu-content-available-width)
);
}
@media (min-width: 640px) {
.tempo-header-inner {
grid-template-columns: minmax(0, 1fr) auto auto auto;
column-gap: 0.75rem;
padding-inline: 1.5rem;
}
.tempo-logo-link {
width: 132px;
}
.tempo-interest-button {
display: inline-flex;
grid-area: 1 / 3;
margin-bottom: 0;
}
.tempo-header-toggle {
grid-area: 1 / 4;
}
}
@media (min-width: 1280px) {
.tempo-header-inner {
grid-template-columns: auto minmax(0, 1fr) auto auto auto;
column-gap: 0.25rem;
padding-inline: 1.25rem;
}
.tempo-logo-link {
width: 100px;
}
.tempo-login-button > span {
max-width: 3rem;
}
.tempo-primary-navigation,
.tempo-header[data-expanded] .tempo-primary-navigation {
display: block;
grid-area: 1 / 2;
border: 0;
padding: 0;
justify-self: center;
}
.tempo-primary-navigation > ul {
display: flex;
align-items: center;
gap: 0;
}
.tempo-nav-trigger {
justify-content: center;
padding-inline: 0.25rem;
font-size: 1.125rem;
letter-spacing: 0.025em;
}
.tempo-header-extras,
.tempo-header[data-expanded] .tempo-header-extras {
display: flex;
grid-area: 1 / 3;
padding: 0 0.5rem 0 0;
border: 0;
border-right: 1px solid #ffffff1a;
}
.tempo-header-utilities {
flex-wrap: nowrap;
}
.tempo-header-actions {
grid-area: 1 / 4;
}
.tempo-interest-button {
grid-area: 1 / 5;
}
.tempo-header-toggle {
display: none;
}
}
@media (min-width: 1440px) {
.tempo-header-inner {
column-gap: 0.75rem;
padding-inline: 2rem;
}
.tempo-nav-trigger {
padding-inline: 0.375rem;
font-size: 1.125rem;
}
.tempo-logo-link {
width: 132px;
}
.tempo-login-button > span {
max-width: 5rem;
}
.tempo-header-extras {
padding-right: 0.75rem;
}
.tempo-primary-navigation > ul {
gap: 0.125rem;
}
}
@media (min-width: 1536px) {
.tempo-header-inner {
column-gap: 1rem;
}
.tempo-nav-trigger {
padding-inline: 0.75rem;
}
.tempo-login-button > span {
max-width: 6rem;
}
}
+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,
+3 -3
View File
@@ -12,9 +12,9 @@ export default function AppearanceToggleTab({
const { appearance, updateAppearance } = useAppearance();
const tabs: { value: Appearance; icon: LucideIcon; label: string }[] = [
{ value: 'light', icon: Sun, label: 'Light' },
{ value: 'dark', icon: Moon, label: 'Dark' },
{ value: 'system', icon: Monitor, label: 'System' },
{ value: 'light', icon: Sun, label: 'Claro' },
{ value: 'dark', icon: Moon, label: 'Escuro' },
{ value: 'system', icon: Monitor, label: 'Sistema' },
];
return (
@@ -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;
}
+20 -13
View File
@@ -23,14 +23,16 @@ export default function DeleteUser() {
<div className="space-y-6">
<Heading
variant="small"
title="Delete account"
description="Delete your account and all of its resources"
title="Eliminar conta"
description="Eliminar a sua conta e os recursos associados"
/>
<div className="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10">
<div className="relative space-y-0.5 text-red-600 dark:text-red-100">
<p className="font-medium">Warning</p>
<p className="text-sm">
Please proceed with caution, this cannot be undone.
{' '}
Esta operação é irreversível. Confirme os dados antes de
continuar.{' '}
</p>
</div>
@@ -40,18 +42,20 @@ export default function DeleteUser() {
variant="destructive"
data-test="delete-user-button"
>
Delete account
{' '}
Eliminar conta{' '}
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>
Are you sure you want to delete your account?
{' '}
Pretende eliminar a sua conta?{' '}
</DialogTitle>
<DialogDescription>
Once your account is deleted, all of its resources
and data will also be permanently deleted. Please
enter your password to confirm you would like to
permanently delete your account.
{' '}
Ao eliminar a conta, os recursos e dados associados
também serão eliminados permanentemente. Introduza a
sua palavra-passe para confirmar.{' '}
</DialogDescription>
<Form
@@ -70,14 +74,15 @@ export default function DeleteUser() {
htmlFor="password"
className="sr-only"
>
Password
{' '}
Palavra-passe{' '}
</Label>
<PasswordInput
id="password"
name="password"
ref={passwordInput}
placeholder="Password"
placeholder="Palavra-passe"
autoComplete="current-password"
/>
@@ -92,7 +97,8 @@ export default function DeleteUser() {
resetAndClearErrors()
}
>
Cancel
{' '}
Cancelar{' '}
</Button>
</DialogClose>
@@ -105,7 +111,8 @@ export default function DeleteUser() {
type="submit"
data-test="confirm-delete-user-button"
>
Delete account
{' '}
Eliminar conta{' '}
</button>
</Button>
</DialogFooter>
+27 -58
View File
@@ -1,10 +1,12 @@
import { useEffect, useRef, useState } from 'react';
import { InvestmentContextAssessment } from './investment-context-assessment';
import { Link } from '@inertiajs/react';
import {
contextApi,
contextFieldNames,
type ContextView,
} from '@/lib/investor-context';
import { inheritCompatibleFields } from '@/lib/context-inheritance';
type Props = {
contextId?: string | null;
consumer: 'simulator' | 'land' | 'desk' | 'submission';
@@ -12,6 +14,7 @@ type Props = {
enabled?: boolean;
readOnly?: boolean;
allowedFields?: string[];
persistInheritance?: boolean;
boundRevision?: number;
onApply: (patch: Record<string, unknown>, replace: boolean) => void;
onReview?: (revision: number, reviewed: boolean) => void;
@@ -40,17 +43,16 @@ export function DossierPrefill(props: Props) {
const [selected, setSelected] = useState<string[]>([]);
const [reviewed, setReviewed] = useState(false);
const [message, setMessage] = useState('');
const filled = useRef('');
useEffect(() => {
if (!contextId) return;
let stopped = false;
setView(null);
setError('');
void contextApi<ContextView>(`/investimentos/${contextId}/contexto`)
.then((v) => {
if (!stopped) {
setView(v);
setReviewed(false);
latest.current.onReview?.(v.context.revision, false);
}
})
.catch((e) => {
@@ -61,44 +63,20 @@ export function DossierPrefill(props: Props) {
};
}, [contextId, retry]);
useEffect(() => {
if (!view || !enabled || readOnly || !view.can_write) return;
const generation = `${contextId}:${view.context.revision}:${consumer}`;
if (filled.current === generation) return;
if (!view || view.case.id !== contextId || !enabled || readOnly || !view.can_write) return;
const source = view.prefill[consumer] ?? {};
const now = latest.current;
const patch: Record<string, unknown> = {};
const conflict = geo.some(
(k) =>
!empty(now.values[k]) &&
String(now.values[k]) !== text(source[k]),
);
for (const [k, v] of Object.entries(source)) {
if (
(now.allowedFields && !now.allowedFields.includes(k)) ||
(geo.includes(k) && conflict)
)
continue;
if (k === 'parcels') {
const old = now.values.parcels as
| Record<string, unknown>[]
| undefined;
if (
(!old?.length || geo.every((g) => empty(old[0]?.[g]))) &&
Array.isArray(v) &&
v[0] &&
Object.keys(v[0]).length
)
patch[k] = v;
} else if (empty(now.values[k]) && !empty(v)) patch[k] = v;
}
filled.current = generation;
if (Object.keys(patch).length) {
now.onApply(patch, false);
const inherited = inheritCompatibleFields(`dossier:${contextId}:${consumer}`, now.values, source, now.values.context_reuse, now.allowedFields);
if (Object.keys(inherited.patch).length || (now.persistInheritance !== false && now.values.context_reuse !== inherited.metadata)) {
// The shared reconciliation has already selected safe fields; metadata travels with the same saved form.
now.onApply({...inherited.patch, ...(now.persistInheritance === false ? {} : {context_reuse:inherited.metadata})}, true);
setMessage(
'Campos vazios preenchidos a partir do dossier. Reveja-os; os campos já preenchidos foram preservados.',
'Dados compatíveis reutilizados do mesmo dossier. Os valores herdados acompanham o contexto; as suas alterações locais foram preservadas.',
);
}
}, [view, enabled, readOnly, contextId, consumer]);
setReviewed(true);
now.onReview?.(view.context.revision, true);
}, [view, enabled, readOnly, contextId, consumer, props.values]);
if (!contextId) return null;
if (error)
return (
@@ -126,7 +104,7 @@ export function DossierPrefill(props: Props) {
),
);
const differences = Object.keys(source).filter(
(k) => text(props.values[k]) !== text(source[k]),
(k) => k !== 'context_applicability_snapshot' && text(props.values[k]) !== text(source[k]),
);
const linkedRevision =
consumer === 'simulator'
@@ -152,6 +130,7 @@ export function DossierPrefill(props: Props) {
. Mestres, declarações deste formulário e conteúdo já submetido
mantêm-se separados.
</p>
<InvestmentContextAssessment contextId={contextId} raw={view.prefill.simulator?.context_applicability_snapshot} />
{stale && (
<p className="font-semibold" role="status">
{linkedRevision > 0
@@ -254,7 +233,17 @@ export function DossierPrefill(props: Props) {
const patch: Record<string, unknown> = {};
for (const k of selected)
patch[k] = source[k] ?? '';
props.onApply(patch, true);
const scope = `dossier:${contextId}:${consumer}`;
let raw = String(props.values.context_reuse ?? '');
try {
const metadata = JSON.parse(raw);
if(metadata.scope === scope) {
for(const key of selected) delete metadata.inherited?.[key];
raw=JSON.stringify(metadata);
}
} catch { /* Legacy metadata does not grant ownership of other fields. */ }
const adopted=inheritCompatibleFields(scope,{...props.values,...Object.fromEntries(selected.map(key=>[key,undefined]))},patch,raw,props.allowedFields);
props.onApply({...patch,...(props.persistInheritance === false ? {} : {context_reuse:adopted.metadata})}, true);
setSelected([]);
setReviewed(false);
props.onReview?.(view.context.revision, false);
@@ -267,27 +256,7 @@ export function DossierPrefill(props: Props) {
</button>
</details>
)}
{!readOnly &&
view.can_write &&
props.onReview &&
view.context.revision > 0 && (
<label className="block">
<input
type="checkbox"
checked={reviewed}
onChange={(e) => {
setReviewed(e.target.checked);
props.onReview?.(
view.context.revision,
e.target.checked,
);
}}
/>{' '}
Conferi os dados e pressupostos deste formulário para o
contexto v{view.context.revision}; os valores diferentes
que conservei são declarações deste processo.
</label>
)}
{!readOnly && reviewed && <p>Contexto v{view.context.revision} recuperado neste formulário, sem nova confirmação dos dados compatíveis. As edições locais conservam-se; as declarações e submissões formais mantêm os seus controlos próprios.</p>}
<p className="text-xs">
Associar uma referência não confirma incentivos, licenças, terra
disponível, reserva ou autorização.
@@ -0,0 +1,24 @@
import { fiscalQaRows } from "@/lib/fiscal-qa-preview";
export default function FiscalQaPreview({ snapshot }: { snapshot: unknown }) {
const rows = fiscalQaRows(snapshot);
if (rows.length === 0) return null;
return (
<section
aria-label="Resultado fiscal preservado"
className="my-3 rounded border border-amber-700 bg-amber-50 p-3"
>
<h5 className="font-semibold">
{rows[0][1]} — cálculo exacto sem arredondamento
</h5>
<dl className="mt-2 space-y-2 text-sm">
{rows.map(([label, value]) => (
<div key={label}>
<dt className="font-medium">{label}</dt>
<dd className="break-all">{value}</dd>
</div>
))}
</dl>
</section>
);
}
@@ -0,0 +1,232 @@
import {
PreparationSummary,
type Preparation,
} from '@/components/private-investment-preparation';
import { Link } from '@inertiajs/react';
type Snapshot = {
preparation?: Preparation | null;
context_revision: number;
path?: {
definition_version: number;
steps: Array<{
code: string;
title: string;
applicability: string;
reason: string;
state: string;
context_review_required?: boolean;
}>;
};
zone?: {
title: string;
href: string;
version: number | string;
fields?: Record<string, string>;
analysis?: {
conditions?: Array<{
id: string;
label: string;
knowledge: string;
source_reference?: string;
valid_from?: string;
valid_until?: string;
}>;
source?: {
title: string;
url: string;
diploma?: string;
article?: string;
valid_from?: string;
valid_until?: string;
} | null;
};
} | null;
fields?: Record<string, string>;
};
export function InvestmentContextAssessment({
raw,
contextId,
}: {
raw: unknown;
contextId?: string | null;
}) {
let snapshot: Snapshot | null = null;
try {
const value = JSON.parse(String(raw ?? ''));
if (value.version === 1) snapshot = value;
} catch {
/* Missing context remains explicit. */
}
if (!snapshot) return null;
const fields = snapshot.fields ?? {};
const zone = snapshot.zone;
const regime = zone?.fields?.investment_regime ?? fields.investment_regime;
const names: Record<string, string> = {
zee: 'ZEE',
zfi: 'ZFI',
industrial_park: 'Parque industrial',
};
return (
<section
className="space-y-2 rounded border border-amber-400 p-3"
aria-label="Aplicabilidade ao investimento"
>
<h3 className="font-semibold">
Aplicabilidade ao investimento · contexto v
{snapshot.context_revision}
</h3>
<p>
Origem do investidor:{' '}
{{
national: 'nacional',
foreign: 'estrangeiro',
mixed: 'misto',
}[fields.investor_origin] ?? 'informação em falta'}
. Residência fiscal declarada:{' '}
{fields.tax_residency_country || 'informação em falta'}. Estas
condições são distintas; uma não determina a outra.
</p>
{zone ? (
<>
<p>
<strong>
Potencialmente aplicável — análise específica de{' '}
{names[regime ?? ''] ?? 'zona seleccionada'}.
</strong>{' '}
<Link className="underline" href={zone.href}>
{zone.title} · versão {zone.version} · consultar
ficha e fundamentos
</Link>
.
</p>
<p>
A selecção identifica a referência consultada; não
confirma o reconhecimento da entidade, actividade
autorizada, licença ou acesso a benefícios. Consulte as
condições documentadas na ficha e reúna a autorização ou
evidência correspondente no dossier.
</p>
{zone.analysis?.source && (
<p>
Fundamento cadastrado:{' '}
<a
className="underline"
href={zone.analysis.source.url}
>
{zone.analysis.source.diploma ||
zone.analysis.source.title}{' '}
{zone.analysis.source.article}
</a>
. Período cadastrado:{' '}
{zone.analysis.source.valid_from ||
'início por indicar'}{' '}
a{' '}
{zone.analysis.source.valid_until ||
'sem termo indicado'}
. A cobertura do investimento exige correspondência
com estas condições.
</p>
)}
{zone.analysis?.conditions?.length ? (
<ul className="list-disc pl-5">
{zone.analysis.conditions.map((condition) => (
<li key={condition.id}>
<strong>{condition.label}</strong>:{' '}
{condition.knowledge === 'documented'
? 'condição documentada; cumprimento pelo investimento por confirmar'
: condition.knowledge === 'disputed'
? 'condição com informação divergente'
: 'informação em falta'}
.{' '}
{condition.source_reference && (
<>
Fundamento:{' '}
{condition.source_reference}.
</>
)}{' '}
{condition.valid_from && (
<>Desde {condition.valid_from}.</>
)}{' '}
{condition.valid_until && (
<>Até {condition.valid_until}.</>
)}
</li>
))}
</ul>
) : (
<p>
Condições específicas: informação estruturada em
falta nesta referência. A ficha e os documentos
continuam consultáveis; não se presume cumprimento.
</p>
)}
<p role="status">
Benefício fiscal deste enquadramento: indeterminado. Não
existe neste consumidor uma regra executável ligada ao
reconhecimento e condições da zona. No simulador, o IVA
disponível corresponde apenas ao cenário geral sem
benefício, sobre vendas declaradas compatíveis; não
simula o regime especial. O cenário com benefício e o
apuramento final estão indisponíveis.
</p>
</>
) : (
<p>
Enquadramento geral de preparação: sem ZEE, ZFI ou parque
seleccionado. Isso não confirma a elegibilidade para uma
operação fiscal. O cálculo mantém o âmbito e as exclusões da
versão publicada.
</p>
)}
<p>
Confirmado neste quadro significa apenas dado recuperado do
dossier; declarações e referências não são decisões jurídicas.
Requisitos específicos não documentados permanecem
indeterminados.
</p>
{snapshot.path && (
<details>
<summary>
Passos adaptados pelo percurso existente · versão{' '}
{snapshot.path.definition_version}
</summary>
<ul className="list-disc pl-5">
{snapshot.path.steps.map((step) => (
<li key={step.code}>
{step.code} — {step.title}:{' '}
{step.context_review_required
? 'informação alterada; rever fundamento'
: step.applicability ===
'not_needed_for_preparation'
? 'não aplicável à preparação declarada'
: step.applicability === 'undetermined'
? 'informação em falta'
: ['completed', 'approved'].includes(
step.state,
)
? 'preparação confirmada no percurso; não é decisão fiscal'
: 'potencialmente aplicável'}
. {step.reason}
</li>
))}
</ul>
</details>
)}
{snapshot.preparation?.enabled && (
<PreparationSummary
data={snapshot.preparation}
caseId={contextId ?? undefined}
/>
)}
{contextId && (
<Link
className="underline"
href={`/investimentos/${contextId}/contexto/editar`}
>
Alterar contexto e comparar impacto antes de guardar
</Link>
)}
</section>
);
}
+5 -4
View File
@@ -17,9 +17,10 @@ const EmptyState = () => {
<div className="bg-muted mx-auto mb-4 flex h-14 w-14 items-center justify-center rounded-2xl">
<KeyRound className="text-muted-foreground h-7 w-7" />
</div>
<p className="font-medium">No passkeys yet</p>
<p className="font-medium"> Ainda não existem chaves de acesso </p>
<p className="text-muted-foreground mt-1 text-sm">
Add a passkey to sign in without a password
{' '}
Adicione uma chave de acesso para entrar sem palavra-passe{' '}
</p>
</div>
);
@@ -47,8 +48,8 @@ export default function ManagePasskeys(props: Props) {
<div className="space-y-6">
<Heading
variant="small"
title="Passkeys"
description="Manage your passkeys for passwordless sign-in"
title="Chaves de acesso"
description="Gerir as chaves de acesso para entrar sem palavra-passe"
/>
<div className="border-border overflow-hidden rounded-lg border">
+16 -13
View File
@@ -48,15 +48,15 @@ export default function ManageTwoFactor(props: Props) {
<div className="space-y-6">
<Heading
variant="small"
title="Two-factor authentication"
description="Manage your two-factor authentication settings"
title="Autenticação de dois factores"
description="Gerir a autenticação de dois factores"
/>
{twoFactorEnabled ? (
<div className="flex flex-col items-start justify-start space-y-4">
<p className="text-muted-foreground text-sm">
You will be prompted for a secure, random pin during
login, which you can retrieve from the TOTP-supported
application on your phone.
{' '}
Ao entrar, será solicitado o código temporário da
aplicação autenticadora do seu telefone.{' '}
</p>
<div className="relative inline">
@@ -67,7 +67,9 @@ export default function ManageTwoFactor(props: Props) {
type="submit"
disabled={processing}
>
Disable 2FA
{' '}
Desactivar autenticação de dois
factores{' '}
</Button>
)}
</Form>
@@ -82,17 +84,16 @@ export default function ManageTwoFactor(props: Props) {
) : (
<div className="flex flex-col items-start justify-start space-y-4">
<p className="text-muted-foreground text-sm">
When you enable two-factor authentication, you will be
prompted for a secure pin during login. This pin can be
retrieved from a TOTP-supported application on your
phone.
{' '}
Depois de activar a autenticação de dois factores, use o
código temporário da aplicação autenticadora para
entrar.{' '}
</p>
<div>
{hasSetupData ? (
<Button onClick={() => setShowSetupModal(true)}>
<ShieldCheck />
Continue setup
<ShieldCheck /> Continuar configuração{' '}
</Button>
) : (
<Form
@@ -101,7 +102,9 @@ export default function ManageTwoFactor(props: Props) {
>
{({ processing }) => (
<Button type="submit" disabled={processing}>
Enable 2FA
{' '}
Activar autenticação de dois
factores{' '}
</Button>
)}
</Form>
@@ -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>
);
+9 -9
View File
@@ -43,13 +43,14 @@ export default function PasskeyItem({ passkey, onDelete }: Props) {
)}
</div>
<p className="text-muted-foreground text-sm">
Added {passkey.created_at_diff}
{' '}
Adicionada em {passkey.created_at_diff}
{passkey.last_used_at_diff && (
<>
<span className="text-muted-foreground/50 mx-1">
/
</span>
Last used {passkey.last_used_at_diff}
</span>{' '}
Última utilização {passkey.last_used_at_diff}
</>
)}
</p>
@@ -64,19 +65,18 @@ export default function PasskeyItem({ passkey, onDelete }: Props) {
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
<span className="sr-only">Remove</span>
<span className="sr-only"> Remover </span>
</Button>
</DialogTrigger>
<DialogContent>
<DialogTitle>Remove passkey</DialogTitle>
<DialogTitle>Remover chave de acesso</DialogTitle>
<DialogDescription>
Are you sure you want to remove the "{passkey.name}"
passkey? You will no longer be able to use it to sign
in.
Pretende remover a chave de acesso «{passkey.name}»?
Deixará de poder usá-la para entrar.
</DialogDescription>
<DialogFooter className="gap-2">
<DialogClose asChild>
<Button variant="secondary">Cancel</Button>
<Button variant="secondary"> Cancelar </Button>
</DialogClose>
<Button
variant="destructive"
+11 -7
View File
@@ -59,7 +59,8 @@ export default function PasskeyRegistration({ onSuccess }: Props) {
if (!isSupported) {
return (
<div className="text-muted-foreground text-sm">
Passkeys are not supported in this browser.
{' '}
Este navegador não suporta chaves de acesso.{' '}
</div>
);
}
@@ -67,7 +68,8 @@ export default function PasskeyRegistration({ onSuccess }: Props) {
if (!showForm) {
return (
<Button variant="outline" onClick={() => setShowForm(true)}>
Add passkey
{' '}
Adicionar chave de acesso{' '}
</Button>
);
}
@@ -78,18 +80,19 @@ export default function PasskeyRegistration({ onSuccess }: Props) {
className="border-border bg-muted/50 space-y-4 rounded-lg border p-4"
>
<div className="grid gap-2">
<Label htmlFor="passkey-name">Passkey name</Label>
<Label htmlFor="passkey-name"> Nome da chave de acesso </Label>
<Input
id="passkey-name"
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., MacBook Pro, iPhone"
placeholder="Por exemplo, MacBook Pro ou iPhone"
className="border-foreground/20 mt-1 block w-full"
autoFocus
/>
<p className="text-muted-foreground text-xs">
A name helps you identify this passkey later.
{' '}
O nome permite identificar esta chave mais tarde.{' '}
</p>
</div>
@@ -97,10 +100,11 @@ export default function PasskeyRegistration({ onSuccess }: Props) {
<div className="flex gap-2">
<Button type="submit" disabled={isLoading || !name.trim()}>
{isLoading ? 'Registering...' : 'Register passkey'}
{isLoading ? 'Registering...' : 'Registar chave de acesso'}
</Button>
<Button type="button" variant="ghost" onClick={handleCancel}>
Cancel
{' '}
Cancelar{' '}
</Button>
</div>
</form>
+3 -3
View File
@@ -51,8 +51,8 @@ export default function PasskeyVerify({
>
{isLoading ? <Spinner /> : <KeyRound className="h-4 w-4" />}
{isLoading
? (loadingLabel ?? 'Authenticating...')
: (label ?? 'Sign in with a passkey')}
? (loadingLabel ?? 'A autenticar…')
: (label ?? 'Entrar com uma chave de acesso')}
</Button>
{error && (
<InputError message={error} className="text-center" />
@@ -65,7 +65,7 @@ export default function PasskeyVerify({
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background text-muted-foreground px-2">
{separator ?? 'Or continue with email'}
{separator ?? 'Ou continuar com email'}
</span>
</div>
</div>
+310 -236
View File
@@ -1,16 +1,19 @@
import PortalProductIdentity from '@/components/portal-product-identity';
import { searchTerritoryHref } from '@/lib/search-territory';
import { Link, router, usePage } from '@inertiajs/react';
import {
ChevronDown,
ArrowUpRight,
LayoutDashboard,
LogOut,
Menu,
UserRound,
X,
} from 'lucide-react';
import { useEffect, useId, useRef, useState } from 'react';
import { Fragment, useEffect, useId, useRef, useState } from 'react';
import { useIsMobile } from '@/hooks/use-mobile';
import { useNavigationContext } from '@/components/navigation-context';
import { TempoSocialLinks, useTempoUi } from '@/components/tempo-brand';
import { tempoSocialLinks, useTempoUi } from '@/components/tempo-brand';
import {
TempoLanguageSelector,
TempoWeather,
@@ -52,6 +55,7 @@ export function PortalGroupedMenu() {
scopedAdministration?: {label:string;href:string}[];
}>();
const { t, language, requestCount } = useTempoUi();
const mobile = useIsMobile();
const navigation = useNavigationContext();
const groups = portalNavigation({
context: navigation.view
@@ -72,24 +76,34 @@ export function PortalGroupedMenu() {
const currentItem = currentNavigationItem(groups, url);
const [loggingOut, setLoggingOut] = useState(false);
const [expanded, setExpanded] = useState(false);
const institutionalId = useId();
const navigationId = useId();
const toolsId = useId();
const interestId = useId();
const toggle = useRef<HTMLButtonElement>(null);
useEffect(() => router.on('navigate', () => setExpanded(false)), []);
const items = (children: PortalNavItem[]) =>
children.map((item) =>
item.children ? (
<DropdownMenuSub key={item.id}>
<DropdownMenuSubTrigger className="tempo-menu-item font-semibold">
{t(item.label)}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
collisionPadding={12}
className="tempo-dropdown max-h-[min(70vh,34rem)] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
>
mobile ? (
<Fragment key={item.id}>
<DropdownMenuLabel className="mt-2 border-t px-3 pt-3 text-xs text-slate-500">
{t(item.label)}
</DropdownMenuLabel>
{items(item.children)}
</DropdownMenuSubContent>
</DropdownMenuSub>
</Fragment>
) : (
<DropdownMenuSub key={item.id}>
<DropdownMenuSubTrigger className="tempo-menu-item font-semibold">
{t(item.label)}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
collisionPadding={12}
className="tempo-dropdown tempo-submenu max-h-[min(70vh,34rem)] w-72 overflow-y-auto p-2"
>
{items(item.children)}
</DropdownMenuSubContent>
</DropdownMenuSub>
)
) : (
<DropdownMenuItem
key={item.id}
@@ -97,7 +111,7 @@ export function PortalGroupedMenu() {
className="tempo-menu-item"
>
<Link
href={item.href!}
href={searchTerritoryHref(item.href!, url)}
aria-current={
currentItem === item.id ? 'page' : undefined
}
@@ -111,6 +125,8 @@ export function PortalGroupedMenu() {
(group) =>
!['home', 'discovery', 'realization', 'support'].includes(group.id),
);
const panelLabel = `${t('O meu painel')} — ${requestCount} ${t(requestCount === 1 ? 'pedido recebido' : 'pedidos recebidos')}`;
const institutionalCurrent = url.split('?')[0] === '/contacto';
return (
<header
className="tempo-header"
@@ -120,245 +136,303 @@ export function PortalGroupedMenu() {
if (
event.key === 'Escape' &&
expanded &&
!event.defaultPrevented
!event.defaultPrevented &&
!(
event.target instanceof Element &&
event.target.closest('[role="menu"]')
)
) {
setExpanded(false);
toggle.current?.focus();
}
}}
>
<div className="tempo-header-layout">
<div className="tempo-header-inner">
<Link
href="/"
aria-label="InvesteMoz — início"
className="tempo-logo-link"
>
<PortalProductIdentity />
</Link>
<nav
id={institutionalId}
aria-label="Navegação institucional"
className="tempo-primary-navigation"
>
<ul>
<li>
<Link
href="/"
className="tempo-nav-trigger"
aria-current={
url.split('?')[0] === '/'
? 'page'
: undefined
}
data-current-group={
url.split('?')[0] === '/' || undefined
}
>
{t('Início')}
</Link>
</li>
<li>
<a
href="https://revista.tempo.co.mz/"
target="_blank"
rel="noopener noreferrer"
className="tempo-nav-trigger"
aria-label={`${t('Revista Tempo')} — ${t('Abre num novo separador')}`}
>
{t('Revista Tempo')}
<ArrowUpRight
size={14}
aria-hidden="true"
/>
</a>
</li>
{[
['/sobre', 'Sobre nós'],
['/contacto', 'Contacto'],
].map(([href, label]) => (
<li key={href}>
<Link
href={href}
className="tempo-nav-trigger"
aria-current={
url.split('?')[0] === href
? 'page'
: undefined
}
data-current-group={
url.split('?')[0] === href ||
undefined
}
>
{t(label)}
</Link>
</li>
))}
</ul>
</nav>
<div id={toolsId} className="tempo-header-extras">
<div className="tempo-header-utilities">
<div className="flex items-center gap-1">
<TempoWeather />
<TempoLanguageSelector />
</div>
<TempoSocialLinks />
</div>
</div>
<div className="tempo-header-actions">
{props.auth?.user ? (
<DropdownMenu modal={false}>
<DropdownMenuTrigger className="tempo-login-button">
<UserRound size={16} aria-hidden="true" />
<span className="max-w-24 truncate">
{props.auth.user.name ||
t('A minha área')}
</span>
<ChevronDown size={12} aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
sideOffset={12}
collisionPadding={12}
className="tempo-dropdown max-h-[75vh] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
>
<DropdownMenuLabel className="truncate text-xs text-slate-500">
{props.auth.user.email}
</DropdownMenuLabel>
{items(accountGroups)}
<DropdownMenuSeparator />
<DropdownMenuItem
asChild
className="tempo-menu-item text-red-700"
>
<Link
href={logout()}
as="button"
type="button"
disabled={loggingOut}
data-test="portal-logout-button"
onClick={() => router.flushAll()}
onStart={() => setLoggingOut(true)}
onFinish={() =>
setLoggingOut(false)
<div className="tempo-header-inner">
<Link
href="/"
aria-label="Tempo — início"
className="tempo-logo-link"
>
<img
src="/images/logo/logo-vermelho.png"
alt="Tempo"
width="1787"
height="564"
className="tempo-header-logo"
/>
</Link>
<nav
id={navigationId}
aria-label="Áreas do portal"
className="tempo-primary-navigation"
>
<ul>
<li>
<Link
href="/"
className="tempo-nav-trigger"
aria-current={
url.split('?')[0] === '/'
? 'page'
: undefined
}
data-current-group={
url.split('?')[0] === '/' || undefined
}
>
{t('Início')}
</Link>
</li>
{groups
.filter((group) =>
[
'discovery',
'realization',
'support',
].includes(group.id),
)
.map((group) => (
<li key={group.id}>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
className="tempo-nav-trigger"
data-current-group={
current === group.id ||
undefined
}
>
<LogOut
size={16}
{t(group.label)}
<ChevronDown
size={14}
aria-hidden="true"
/>
{loggingOut
? 'A terminar sessão…'
: t('Terminar sessão')}
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Link href="/login" className="tempo-login-button">
<UserRound size={15} aria-hidden="true" />
{t('Entrar')}
</Link>
)}
</div>
<button
ref={toggle}
type="button"
className="tempo-header-toggle"
aria-label={
expanded
? 'Fechar menu e ferramentas'
: 'Abrir menu e ferramentas'
}
aria-expanded={expanded}
aria-controls={`${institutionalId} ${toolsId}`}
onClick={() => setExpanded(!expanded)}
>
{expanded ? (
<X size={22} aria-hidden="true" />
) : (
<Menu size={22} aria-hidden="true" />
)}
</button>
</div>
<div className="tempo-section-bar">
<div className="tempo-section-inner">
<Link
href="/investidor/manifestacoes"
className="tempo-panel-summary"
>
<span>{t('O meu painel')}</span>
<span
className="tempo-panel-count"
aria-live="polite"
aria-atomic="true"
{current === group.id && (
<span className="sr-only">
Grupo actual
</span>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
sideOffset={12}
collisionPadding={12}
className="tempo-dropdown max-h-[min(75vh,36rem)] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
>
{items(group.children ?? [])}
</DropdownMenuContent>
</DropdownMenu>
</li>
))}
<li>
<Link
href="/sobre"
className="tempo-nav-trigger"
aria-current={
url.split('?')[0] === '/sobre'
? 'page'
: undefined
}
data-current-group={
url.split('?')[0] === '/sobre' || undefined
}
>
{requestCount}{' '}
{t(
requestCount === 1
? 'pedido recebido'
: 'pedidos recebidos',
)}
</span>
</Link>
<nav
aria-label="Áreas do portal"
className="tempo-section-navigation"
>
<ul>
{groups
.filter((group) =>
[
'discovery',
'realization',
'support',
].includes(group.id),
)
.map((group) => (
<li key={group.id}>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
className="tempo-nav-trigger"
data-current-group={
current === group.id ||
undefined
{t('Sobre nós')}
</Link>
</li>
<li>
<DropdownMenu modal={false}>
<DropdownMenuTrigger
className="tempo-nav-trigger"
data-current-group={
institutionalCurrent || undefined
}
>
Tempo
<ChevronDown size={14} aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
sideOffset={12}
collisionPadding={12}
className="tempo-dropdown max-h-[75vh] w-64 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
>
<DropdownMenuItem
asChild
className="tempo-menu-item"
>
<a
href="https://revista.tempo.co.mz/"
target="_blank"
rel="noopener noreferrer"
aria-label={`${t('Revista Tempo')} — ${t('Abre num novo separador')}`}
>
{t('Revista Tempo')}
<ArrowUpRight
size={14}
aria-hidden="true"
/>
</a>
</DropdownMenuItem>
{[['/contacto', 'Contacto']].map(
([href, label]) => (
<DropdownMenuItem
key={href}
asChild
className="tempo-menu-item"
>
<Link
href={href}
aria-current={
url.split('?')[0] ===
href
? 'page'
: undefined
}
>
{t(group.label)}
<ChevronDown
size={14}
{t(label)}
</Link>
</DropdownMenuItem>
),
)}
<DropdownMenuSeparator />
<DropdownMenuLabel className="text-xs text-slate-500">
Revista Tempo
</DropdownMenuLabel>
{tempoSocialLinks.map(
({ name, href, icon: Icon }) => (
<DropdownMenuItem
key={name}
asChild
className="tempo-menu-item"
>
<a
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={`Revista Tempo — ${name}`}
>
<Icon
size={16}
aria-hidden="true"
/>
{current === group.id && (
<span className="sr-only">
Grupo actual
</span>
)}
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
sideOffset={8}
collisionPadding={12}
className="tempo-dropdown max-h-[min(75vh,36rem)] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
>
{items(
group.children ?? [],
)}
</DropdownMenuContent>
</DropdownMenu>
</li>
))}
</ul>
</nav>
<Link
href="/manifestar-interesse"
className="tempo-interest-button"
>
{t('Manifestar interesse')}
</Link>
{name}
<ArrowUpRight
size={12}
aria-hidden="true"
/>
</a>
</DropdownMenuItem>
),
)}
</DropdownMenuContent>
</DropdownMenu>
</li>
</ul>
</nav>
<div id={toolsId} className="tempo-header-extras">
<div className="tempo-header-utilities">
<div className="flex items-center gap-1">
<TempoWeather />
<TempoLanguageSelector />
</div>
</div>
</div>
<div className="tempo-header-actions">
<Link
href="/investidor/manifestacoes"
className="tempo-panel-summary"
aria-label={panelLabel}
title={panelLabel}
>
<LayoutDashboard size={19} aria-hidden="true" />
<span className="tempo-panel-count" aria-hidden="true">
{requestCount}
</span>
</Link>
<span
className="sr-only"
role="status"
aria-live="polite"
aria-atomic="true"
>
{panelLabel}
</span>
{props.auth?.user ? (
<DropdownMenu modal={false}>
<DropdownMenuTrigger className="tempo-login-button">
<UserRound size={16} aria-hidden="true" />
<span className="max-w-24 truncate">
{props.auth.user.name || t('A minha área')}
</span>
<ChevronDown size={12} aria-hidden="true" />
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
sideOffset={12}
collisionPadding={12}
className="tempo-dropdown max-h-[75vh] w-72 max-w-[calc(100vw-1.5rem)] overflow-y-auto p-2"
>
<DropdownMenuLabel className="truncate text-xs text-slate-500">
{props.auth.user.email}
</DropdownMenuLabel>
{items(accountGroups)}
<DropdownMenuSeparator />
<DropdownMenuItem
asChild
className="tempo-menu-item text-red-700"
>
<Link
href={logout()}
as="button"
type="button"
disabled={loggingOut}
data-test="portal-logout-button"
onClick={() => router.flushAll()}
onStart={() => setLoggingOut(true)}
onFinish={() => setLoggingOut(false)}
>
<LogOut size={16} aria-hidden="true" />
{loggingOut
? 'A terminar sessão…'
: t('Terminar sessão')}
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<Link href="/login" className="tempo-login-button">
<UserRound size={15} aria-hidden="true" />
{t('Entrar')}
</Link>
)}
</div>
<Link
id={interestId}
href="/manifestar-interesse"
className="tempo-interest-button"
>
{t('Manifestar interesse')}
<ArrowUpRight size={15} aria-hidden="true" />
</Link>
<button
ref={toggle}
type="button"
className="tempo-header-toggle"
aria-label={
expanded
? 'Fechar menu e ferramentas'
: 'Abrir menu e ferramentas'
}
aria-expanded={expanded}
aria-controls={`${navigationId} ${toolsId} ${interestId}`}
onClick={() => setExpanded(!expanded)}
>
{expanded ? (
<X size={22} aria-hidden="true" />
) : (
<Menu size={22} aria-hidden="true" />
)}
</button>
</div>
</header>
);
@@ -0,0 +1,457 @@
import { Link } from '@inertiajs/react';
import { card, input } from '@/pages/investment/shared';
export type PreparationDocument = {
document_id: string;
file_name: string;
status: string;
malware_scan_status: string;
};
export type Preparation = {
version: string;
enabled: boolean;
mode: string;
period_covered: boolean;
scope_covered: boolean;
reason: string[];
missing_fields: string[];
missing_labels?: string[];
authority: string;
next: string;
calculation_notice: string;
notice: string;
source: { url: string; sha256: string };
authority_source: { reference: string; url: string };
questions: Array<{
key: string;
label: string;
type?: string;
options?: Record<string, string>;
when?: { field: string; equals: string };
source: string;
help?: string;
}>;
requirements: Array<{
key: string;
label: string;
state: string;
required: boolean;
document_id: string | null;
document: PreparationDocument | null;
stage: string;
reason: string;
source: { reference: string; url: string; period: string };
}>;
};
const states: Record<string, string> = {
not_applicable: 'Não aplicável neste contexto',
indeterminate: 'Informação em falta para decidir',
missing: 'Documento por associar',
associated: 'Documento associado — conteúdo por conferir',
};
export const preparationMode = (mode: string) =>
({
authorization: 'Pedido de autorização',
registration: 'Pedido de mero registo',
indeterminate: 'Enquadramento por determinar',
})[mode] ?? mode;
export function PreparationSummary({
data,
caseId,
}: {
data: Preparation | null | undefined;
caseId?: string;
}) {
if (!data) return null;
return (
<section
id="private-investment-preparation"
className={card + ' space-y-3'}
aria-label="Preparação adaptada do investimento privado"
>
<h2 className="text-xl font-bold">
Preparar o pedido de investimento privado
</h2>
{!data.enabled ? (
<p>
Use os dados deste dossier para identificar o pedido e os
documentos aplicáveis. A preparação é opcional e não atribui
benefícios.
</p>
) : (
<>
<p>
<strong>{preparationMode(data.mode)}</strong> · recorte
de 2026 ·{' '}
{data.version.replace(
'private-investment-preparation-',
'',
)}
</p>
<p>{data.next}</p>
{data.missing_fields.length > 0 && (
<p>
Informação em falta:{' '}
{(
data.missing_labels ??
data.missing_fields.map(
(key) =>
data.questions?.find(
(q) => q.key === key,
)?.label ??
(
{
currency: 'Moeda do projecto',
expected_start_date:
'Data prevista de início',
entity_stage:
'Situação da entidade',
} as Record<string, string>
)[key] ??
key,
)
).join('; ')}
.
</p>
)}
{!data.period_covered && (
<p>
Este incremento cobre pedidos com início previsto em
2026. Uma data em falta ou fora desse recorte não
permite determinar o pedido por este lote; os
restantes trabalhos do dossier continuam
disponíveis.
</p>
)}
{!data.scope_covered && (
<p>
Declare a actividade efectiva. Prospecção,
pesquisa/produção extractiva e investimento público
ou exclusivamente social/sem fins lucrativos seguem
enquadramentos próprios, fora deste lote.
</p>
)}
{data.reason.map((reason) => (
<p key={reason}>{reason}</p>
))}
<p>
Encaminhamento indicativo: {data.authority}.{' '}
<a
className="underline"
href={data.authority_source.url}
>
{data.authority_source.reference}
</a>
</p>
<ul className="space-y-3">
{data.requirements
.filter((r) => r.state !== 'not_applicable')
.map((r) => (
<li key={r.key} className="rounded border p-3">
<strong>{r.label}</strong> —{' '}
{states[r.state]}.{' '}
{r.document?.file_name && (
<span>
Ficheiro: {r.document.file_name}
.{' '}
</span>
)}
<p>
{r.reason}{' '}
<a
className="underline"
href={r.source.url}
>
Ver fundamento: {r.source.reference}
</a>{' '}
· {r.source.period}.
</p>
{caseId && (
<Link
className="underline"
href={`/investimentos/${caseId}/contexto/editar#preparation-documents`}
>
Rever dados ou associar documento do
dossier
</Link>
)}
</li>
))}
</ul>
<details>
<summary>
Exigências que não se aplicam — histórico preservado
</summary>
{data.requirements
.filter((r) => r.state === 'not_applicable')
.map((r) => (
<p key={r.key}>
{r.label}: {r.reason}{' '}
{r.document_id && (
<>
Associação anterior mantida
{r.document?.file_name
? `: ${r.document.file_name}`
: ''}
.
</>
)}{' '}
<a
className="underline"
href={r.source.url}
>
{r.source.reference}
</a>
</p>
))}
</details>
<p>{data.calculation_notice}</p>
<p>{data.notice}</p>
</>
)}
<a className="underline" href={data.source.url}>
Consultar o exemplar oficial e os fundamentos
</a>
{caseId && (
<p>
<Link
className="underline"
href={`/investimentos/${caseId}/contexto/editar#preparation-questions`}
>
Completar apenas os dados em falta ou editar declarações
</Link>
</p>
)}
</section>
);
}
export function PreparationEditor({
data,
fields,
changes,
documents,
links,
onChange,
onLink,
disabled,
}: {
data: Preparation;
fields: Record<string, string>;
changes: Record<string, string>;
documents: PreparationDocument[];
links: Record<string, string | null>;
onChange: (key: string, value: string) => void;
onLink: (key: string, value: string | null) => void;
disabled: boolean;
}) {
const values = { ...fields, ...changes };
const active = values.investment_preparation === 'yes';
const excluded = ['extractive', 'public_nonprofit'].includes(
values.investment_activity_scope,
);
const applicable = (q: Preparation['questions'][number]) =>
!q.when || values[q.when.field] === q.when.equals;
// Keep a field in place while the user edits it; collapse only recovered answers.
const known = (q: Preparation['questions'][number]) =>
!!fields[q.key] && fields[q.key] !== 'unknown';
const render = (q: Preparation['questions'][number]) => (
<label className="grid gap-1" key={q.key} htmlFor={'prep-' + q.key}>
{q.label}
{q.options ? (
<select
id={'prep-' + q.key}
className={input}
disabled={disabled}
value={values[q.key] ?? ''}
onChange={(e) => onChange(q.key, e.target.value)}
>
<option value="">Por indicar</option>
{Object.entries(q.options).map(([value, text]) => (
<option key={value} value={value}>
{text}
</option>
))}
</select>
) : (
<input
id={'prep-' + q.key}
className={input}
disabled={disabled}
type="text"
inputMode="decimal"
value={values[q.key] ?? ''}
onChange={(e) => onChange(q.key, e.target.value)}
/>
)}
<span className="text-sm">
{q.help} Fundamento: {q.source}.
</span>
</label>
);
const questions = data.questions.filter(
(q) =>
q.key !== 'investment_preparation' &&
applicable(q) &&
(!excluded || q.key === 'investment_activity_scope'),
);
const needed = questions.filter(
(q) =>
!known(q) &&
(!data.enabled ||
data.mode !== 'authorization' ||
data.missing_fields.includes(q.key)),
);
return (
<section
id="preparation-questions"
className={card + ' space-y-4'}
aria-label="Dados do pedido de investimento privado"
>
<h2 className="text-xl font-bold">
Dados em falta para preparar o pedido
</h2>
<p>
Valores conhecidos são reutilizados. As alterações só são
gravadas depois da comparação de impacto. Nacionalidade e
residência não determinam a origem do capital nem a modalidade
da empresa.
</p>
{render(data.questions[0])}
{active && (
<>
{needed.map(render)}
<details>
<summary>
Dados já declarados — consultar ou alterar
</summary>
<div className="space-y-3">
{questions.filter(known).map(render)}
</div>
</details>
<p>
Data, moeda e situação da entidade usam as declarações
acima.{' '}
{data.missing_fields.filter((k) =>
[
'currency',
'expected_start_date',
'entity_stage',
].includes(k),
).length > 0 &&
'Complete esses campos em falta antes de comparar.'}
</p>
<div id="preparation-documents" className="space-y-3">
<h3 className="font-bold">
Associar documentos já existentes no dossier
</h3>
<p>
Carregue documentos pelo Balcão Virtual do mesmo
dossier. A associação não aprova o conteúdo nem
substitui a análise do pedido.
</p>
{data.requirements
.filter((r) => r.required)
.map((r) => (
<label className="grid gap-1" key={r.key}>
{r.label}
<select
className={input}
disabled={disabled}
value={
Object.hasOwn(links, r.key)
? (links[r.key] ?? '')
: (r.document_id ?? '')
}
onChange={(e) =>
onLink(
r.key,
e.target.value || null,
)
}
>
<option value="">Por associar</option>
{documents.map((doc) => (
<option
key={doc.document_id}
value={doc.document_id}
>
{doc.file_name} ·{' '}
{{
quarantined:
'Em quarentena',
active: 'Disponível',
deleted: 'Removido',
}[doc.status] ??
'Estado por conferir'}
</option>
))}
</select>
<span>
{r.reason}{' '}
<a
className="underline"
href={r.source.url}
>
{r.source.reference}
</a>
</span>
</label>
))}
</div>
</>
)}
<p>
Compare o impacto para actualizar o enquadramento, as perguntas
e as exigências. Uma exigência que deixe de ser aplicável
conserva o documento anterior no histórico.
</p>
</section>
);
}
export function PreparationImpact({
before,
after,
}: {
before: Preparation;
after: Preparation;
}) {
return (
<div
className="space-y-2 rounded border border-amber-400 p-3"
aria-label="Impacto no pedido de investimento"
>
<h3 className="font-bold">Impacto no pedido e nos documentos</h3>
<p>
{preparationMode(before.mode)} → {preparationMode(after.mode)}.
</p>
<p>Próximo passo: {after.next}.</p>
<ul className="list-disc pl-5">
{after.requirements
.filter(
(r) =>
before.requirements.find((b) => b.key === r.key)
?.state !== r.state,
)
.map((r) => (
<li key={r.key}>
{r.label}:{' '}
{
states[
before.requirements.find(
(b) => b.key === r.key,
)?.state ?? 'indeterminate'
]
}{' '}
→ {states[r.state]}.{' '}
{r.document_id && 'Vínculo documental preservado.'}
</li>
))}
</ul>
<p>{after.calculation_notice}</p>
<PreparationSummary data={after} />
</div>
);
}
+133 -100
View File
@@ -1,11 +1,15 @@
import { useMemo, useState } from 'react';
import { useMemo, useRef, useState } from 'react';
import {
DossierExportGate,
dossierExportFailure,
prepareDossierAttachments,
} from '@/lib/dossier-export-state';
import {
createProjectDossierPrintHtml,
createProjectDossierReport,
dossierReportSections,
generateProjectDossierDocx,
generateProjectDossierPackage,
validateDossierAttachments,
type DossierAttachment,
type DossierReference,
type DossierCurrentCalculation,
@@ -57,6 +61,8 @@ export function ProjectDossierExport({
const [attachments, setAttachments] = useState<DossierAttachment[]>([]);
const [error, setError] = useState('');
const [busy, setBusy] = useState('');
const [notice, setNotice] = useState('');
const exportGate = useRef(new DossierExportGate());
const selected = useMemo(
() => attachments.filter((item) => item.include),
[attachments],
@@ -66,20 +72,33 @@ export function ProjectDossierExport({
[selectedSections],
);
function addFiles(files: FileList | null): void {
async function addFiles(files: FileList | null): Promise<void> {
if (!files?.length) return;
const incoming = Array.from(files);
const combined = [...attachments.map((item) => item.file), ...incoming];
const problem = validateDossierAttachments(combined);
if (problem) {
setError(problem);
return;
}
setAttachments((current) => [
...current,
...incoming.map((file) => ({ file, include: true, caption: '' })),
]);
setError('');
await exportGate.current.run(async () => {
setBusy('attachments');
setNotice('');
try {
const result = await prepareDossierAttachments(
attachments,
incoming,
);
if (result.problem) {
setError(result.problem);
return;
}
setAttachments(result.attachments);
setError('');
if (result.duplicateCount)
setNotice(
'Os ficheiros que já estavam na lista foram mantidos uma única vez.',
);
} catch (cause) {
setError(dossierExportFailure('attachments', cause));
} finally {
setBusy('');
}
});
}
function toggleSection(id: DossierReportSectionId, checked: boolean): void {
@@ -118,99 +137,103 @@ export function ProjectDossierExport({
}
async function exportWord(): Promise<void> {
setBusy('word');
setError('');
try {
const report = model();
const blob = await generateProjectDossierDocx(
report,
selected,
selectedSections.has('annexes'),
);
download(
blob,
`relatorio-investmoz-${filenameStem(report.title)}.docx`,
);
} catch (cause) {
setError(
cause instanceof Error
? cause.message
: 'Não foi possível gerar o relatório Word.',
);
} finally {
setBusy('');
}
await exportGate.current.run(async () => {
setBusy('word');
setError('');
try {
const report = model();
const blob = await generateProjectDossierDocx(
report,
selected,
selectedSections.has('annexes'),
);
download(
blob,
`relatorio-investmoz-${filenameStem(report.title)}.docx`,
);
} catch (cause) {
setError(dossierExportFailure('word', cause));
} finally {
setBusy('');
}
});
}
async function exportPackage(): Promise<void> {
setBusy('package');
setError('');
try {
const report = model();
const docx = await generateProjectDossierDocx(
report,
selected,
selectedSections.has('annexes'),
);
const archive = await generateProjectDossierPackage(
docx,
selected,
selectedSections.has('annexes'),
);
download(
archive,
`relatorio-investmoz-${filenameStem(report.title)}-anexos.zip`,
);
} catch (cause) {
setError(
cause instanceof Error
? cause.message
: 'Não foi possível preparar o pacote de anexos.',
);
} finally {
setBusy('');
}
await exportGate.current.run(async () => {
setBusy('package');
setError('');
try {
const report = model();
const docx = await generateProjectDossierDocx(
report,
selected,
selectedSections.has('annexes'),
);
const archive = await generateProjectDossierPackage(
docx,
selected,
selectedSections.has('annexes'),
);
download(
archive,
`relatorio-investmoz-${filenameStem(report.title)}-anexos.zip`,
);
} catch (cause) {
setError(dossierExportFailure('package', cause));
} finally {
setBusy('');
}
});
}
async function exportPdf(): Promise<void> {
const printWindow = window.open('about:blank', '_blank');
if (!printWindow) {
setError(
'O browser bloqueou a janela de impressão. Permita pop-ups para abrir a versão PDF.',
);
return;
}
setBusy('pdf');
setError('');
try {
const html = await createProjectDossierPrintHtml(
model(),
selected,
selectedSections.has('annexes'),
);
printWindow.document.open();
printWindow.document.write(html);
printWindow.document.close();
} catch (cause) {
printWindow.close();
setError(
cause instanceof Error
? cause.message
: 'Não foi possível preparar a versão para PDF.',
);
} finally {
setBusy('');
}
await exportGate.current.run(async () => {
const printWindow = window.open('about:blank', '_blank');
if (!printWindow) {
setError(
'O browser bloqueou a janela de impressão. Permita pop-ups para abrir a versão PDF.',
);
return;
}
setBusy('pdf');
setError('');
try {
const html = await createProjectDossierPrintHtml(
model(),
selected,
selectedSections.has('annexes'),
);
printWindow.document.open();
printWindow.document.write(html);
printWindow.document.close();
} catch (cause) {
printWindow.close();
setError(dossierExportFailure('pdf', cause));
} finally {
setBusy('');
}
});
}
function exportCsv(): void {
const blob = new Blob([exportProjectDossierCsv(draft)], {
type: 'text/csv;charset=utf-8',
async function exportCsv(): Promise<void> {
await exportGate.current.run(async () => {
setBusy('csv');
setError('');
try {
const blob = new Blob([exportProjectDossierCsv(draft)], {
type: 'text/csv;charset=utf-8',
});
download(
blob,
`dossier-projecto-investmoz-${filenameStem(String(draft.project_title ?? ''))}.csv`,
);
} catch (cause) {
setError(dossierExportFailure('csv', cause));
} finally {
setBusy('');
}
});
download(
blob,
`dossier-projecto-investmoz-${filenameStem(String(draft.project_title ?? ''))}.csv`,
);
}
const totalBytes = attachments.reduce(
@@ -261,6 +284,7 @@ export function ProjectDossierExport({
>
<input
type="checkbox"
disabled={Boolean(busy)}
checked={selectedSections.has(section.id)}
onChange={(event) =>
toggleSection(
@@ -298,9 +322,10 @@ export function ProjectDossierExport({
id="project-report-files"
type="file"
multiple
disabled={Boolean(busy)}
accept="image/jpeg,image/png,text/csv,text/plain,.csv,.tsv,.txt,.pdf,.doc,.docx,.xlsx,.odt"
onChange={(event) => {
addFiles(event.currentTarget.files);
void addFiles(event.currentTarget.files);
event.currentTarget.value = '';
}}
/>
@@ -322,6 +347,7 @@ export function ProjectDossierExport({
<input
aria-label={`Incluir ${item.file.name}`}
type="checkbox"
disabled={Boolean(busy)}
checked={item.include}
onChange={(event) =>
updateAttachment(index, {
@@ -340,6 +366,7 @@ export function ProjectDossierExport({
<button
type="button"
className="rounded border px-2 py-1 text-sm"
disabled={Boolean(busy)}
onClick={() => removeAttachment(index)}
>
Remover
@@ -352,6 +379,7 @@ export function ProjectDossierExport({
<input
className="rounded border px-3 py-2"
maxLength={300}
disabled={Boolean(busy)}
value={item.caption}
onChange={(event) =>
updateAttachment(index, {
@@ -372,6 +400,11 @@ export function ProjectDossierExport({
em zero. O relatório identifica pressupostos, estimativas e
referências sem as apresentar como resultados confirmados.
</p>
{notice && (
<p role="status" className="text-sm">
{notice}
</p>
)}
{error && (
<p role="alert" className="text-sm text-red-800">
{error}
@@ -414,7 +447,7 @@ export function ProjectDossierExport({
type="button"
className="rounded border px-4 py-2"
disabled={Boolean(busy)}
onClick={exportCsv}
onClick={() => void exportCsv()}
>
Exportar CSV complementar
</button>
@@ -95,7 +95,7 @@ function MoneyInput({
}
export function SimulationBusinessPlan({
currency,
currency: projectCurrency,
snapshot,
onCurrencyChange,
onSnapshotChange,
@@ -253,8 +253,10 @@ export function SimulationBusinessPlan({
const plan = workingPlan ??
restoreBusinessPlanSnapshot(snapshot) ?? {
...emptyPlan,
currency,
currency: projectCurrency,
};
// A project edit is not a conversion or permission to relabel stored sales.
const currency = plan.currency || projectCurrency;
const update = (
key: keyof BusinessFinancialPlanInput,
value:
@@ -858,6 +860,7 @@ export function SimulationBusinessPlan({
<summary className="cursor-pointer text-lg font-semibold">
Plano financeiro empresarial (pressupostos editáveis)
</summary>
{plan.currency && projectCurrency && plan.currency !== projectCurrency && <p role="alert">A projecção guardada está em {plan.currency}; o projecto está em {projectCurrency}. Os valores e a moeda de origem foram preservados. Reveja o enquadramento monetário antes de os reutilizar; não foi feita conversão.</p>}
{snapshotError && (
<p
role="alert"
@@ -1,7 +1,11 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
import {
exportMonthlyCashflowCsv,
projectMonthlyCashflow,
defaultCashflowDraftInputs,
restoreCashflowDraftSnapshot,
serializeCashflowDraftSnapshot,
type CashflowDraftSnapshot,
} from '@/lib/monthly-cashflow';
import {
isSimulationCurrency,
@@ -11,28 +15,38 @@ import {
export function SimulationCashflowPlanner({
currency,
onCurrencyChange,
snapshot,
onSnapshotChange,
}: {
currency: string;
onCurrencyChange: (currency: string) => void;
snapshot?: string;
onSnapshotChange?: (snapshot: string) => void;
}) {
const [taxMode, setTaxMode] = useState<'fixed' | 'schedule'>('fixed');
const [inputs, setInputs] = useState({
openingCash: '0',
monthlyRevenue: '200000',
monthlyOperatingCosts: '150000',
monthlyDebtService: '0',
monthlyTaxAssessed: '',
monthlyTaxSchedule: Array.from({ length: 12 }, () => ''),
vatCreditEstimated: '0',
vatRefundRequested: '0',
vatRefundRecognized: '0',
vatRefundExpectedReceipt: '0',
vatRefundExpectedMonth: '',
});
const restored = restoreCashflowDraftSnapshot(snapshot);
const [taxMode, setTaxMode] = useState<'fixed' | 'schedule'>(
restored?.taxMode ?? 'fixed',
);
const [inputs, setInputs] = useState(
restored?.inputs ?? defaultCashflowDraftInputs(),
);
useEffect(() => {
const saved = restoreCashflowDraftSnapshot(snapshot);
setTaxMode(saved?.taxMode ?? 'fixed');
setInputs(saved?.inputs ?? defaultCashflowDraftInputs());
}, [snapshot]);
function update(
next: typeof inputs,
mode: CashflowDraftSnapshot['taxMode'] = taxMode,
) {
setInputs(next);
setTaxMode(mode);
onSnapshotChange?.(serializeCashflowDraftSnapshot(mode, next));
}
const set = (
key: Exclude<keyof typeof inputs, 'monthlyTaxSchedule'>,
value: string,
) => setInputs((current) => ({ ...current, [key]: value }));
) => update({ ...inputs, [key]: value });
const cashflowInput = {
...inputs,
...(taxMode === 'fixed'
@@ -45,6 +59,10 @@ export function SimulationCashflowPlanner({
let projection;
let error = '';
try {
if (snapshot && !restored)
throw new Error(
'Não foi possível recuperar os pressupostos guardados de tesouraria. Reveja os campos antes de voltar a guardar; nenhum resultado foi calculado com dados alternativos.',
);
projection = projectMonthlyCashflow({ ...cashflowInput, currency });
} catch (cause) {
error =
@@ -97,9 +115,11 @@ export function SimulationCashflowPlanner({
role="note"
className="rounded bg-amber-50 p-3 text-sm text-amber-950"
>
Os valores preenchidos ao abrir são exemplos demonstrativos.
Confirme ou altere os pressupostos antes de interpretar os
resultados ou exportar o CSV.
{restored
? 'Pressupostos recuperados do rascunho deste investimento; reveja-os antes de interpretar os resultados ou exportar.'
: snapshot
? 'O rascunho guardado não pôde ser interpretado. Os campos mostram exemplos, que não substituem os dados guardados até os editar.'
: 'Os valores preenchidos ao abrir são exemplos demonstrativos. Confirme ou altere os pressupostos antes de interpretar os resultados ou exportar o CSV.'}
</p>
{projection?.status === 'incomplete' && (
<p
@@ -121,7 +141,7 @@ export function SimulationCashflowPlanner({
type="radio"
name="cashflow-tax-mode"
checked={taxMode === 'fixed'}
onChange={() => setTaxMode('fixed')}
onChange={() => update(inputs, 'fixed')}
/>
Valor mensal fixo
</label>
@@ -130,13 +150,13 @@ export function SimulationCashflowPlanner({
type="radio"
name="cashflow-tax-mode"
checked={taxMode === 'schedule'}
onChange={() => setTaxMode('schedule')}
onChange={() => update(inputs, 'schedule')}
/>
Calendário variável por mês
</label>
{taxMode === 'fixed' ? (
<label className="block max-w-xs text-sm font-medium">
Imposto assumido por mês
Imposto manual do cenário personalizado por mês
<input
type="number"
min="0"
@@ -155,23 +175,23 @@ export function SimulationCashflowPlanner({
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
{inputs.monthlyTaxSchedule.map((value, index) => (
<label key={index} className="text-sm font-medium">
Imposto assumido — mês {index + 1}
Imposto manual do cenário personalizado — mês {index + 1}
<input
type="number"
min="0"
step="0.01"
value={value}
onChange={(event) =>
setInputs((current) => ({
...current,
update({
...inputs,
monthlyTaxSchedule:
current.monthlyTaxSchedule.map(
inputs.monthlyTaxSchedule.map(
(amount, month) =>
month === index
? event.target.value
: amount,
),
}))
})
}
className="mt-1 w-full rounded border p-2"
/>
@@ -352,6 +352,12 @@ export function SimulationFinancialTools({
/>
{schedule && isSimulationCurrency(currency) && (
<>
<p className="text-sm">
Montantes a duas casas decimais; juros e
prestação arredondados à unidade monetária
mínima, metade para cima. A última prestação
ajusta o saldo residual.
</p>
<p aria-live="polite">
Taxa por período: {schedule.periodicRatePercent}
%; desembolso líquido: {
@@ -1,5 +1,6 @@
import { useEffect, useState } from 'react';
import { fiscalComponentCodes } from '@/lib/simulation-contract';
import { ivaIssueText, type IvaDiagnostic, type IvaIssue } from '@/lib/iva-diagnostics';
type Parameter = {
parameter_code: string;
@@ -56,6 +57,8 @@ export function SimulationFiscalStatus({
periodStart,
periodEnd,
context,
onDiagnostics,
onCorrect,
}: {
salaryView?: string;
scenarioMode?: string;
@@ -63,9 +66,13 @@ export function SimulationFiscalStatus({
periodStart?: string;
periodEnd?: string;
context?: Record<string, string>;
onDiagnostics?: (issues: IvaIssue[]) => void;
onCorrect?: (field: string) => void;
}) {
const [parameters, setParameters] = useState<Parameter[]>([]);
const [diagnostics, setDiagnostics] = useState<IvaDiagnostic[]>([]);
const [requestState, setRequestState] = useState('idle');
const [retry, setRetry] = useState(0);
useEffect(() => {
const apiComponent = component
@@ -78,10 +85,14 @@ export function SimulationFiscalStatus({
!periodEnd
) {
setParameters([]);
setDiagnostics([]);
onDiagnostics?.([]);
setRequestState('idle');
return;
}
const abort = new AbortController();
setDiagnostics([]);
onDiagnostics?.([]);
setRequestState('loading');
void fetch('/api/v1/tax-parameters/resolve', {
method: 'POST',
@@ -103,13 +114,19 @@ export function SimulationFiscalStatus({
if (!response.ok) throw new Error('lookup_failed');
const payload = (await response.json()) as {
parameters?: Parameter[];
diagnostics?: IvaDiagnostic[];
};
if (abort.signal.aborted) return;
setParameters(
Array.isArray(payload.parameters) ? payload.parameters : [],
);
const details = Array.isArray(payload.diagnostics) ? payload.diagnostics : [];
setDiagnostics(details);
onDiagnostics?.(details.flatMap((detail) => detail.issues));
setRequestState('done');
})
.catch((error: unknown) => {
if (abort.signal.aborted) return;
if (
error instanceof DOMException &&
error.name === 'AbortError'
@@ -125,6 +142,7 @@ export function SimulationFiscalStatus({
periodStart,
periodEnd,
JSON.stringify(context ?? {}),
retry,
]);
const salaryViewMessages: Record<string, string> = {
@@ -154,12 +172,22 @@ export function SimulationFiscalStatus({
{requestState === 'loading'
? 'A consultar parâmetros aprovados para o período e âmbito indicados…'
: requestState === 'error'
? 'Não foi possível consultar os parâmetros; o resultado permanece indeterminado.'
? 'Falha na consulta do sistema: não foi possível obter os parâmetros. O problema não é atribuído às respostas; estas foram preservadas. Tente novamente.'
: !periodStart || !periodEnd
? 'Indique um período exacto. Para periodicidade mensal ou trimestral, seleccione o mês ou trimestre.'
: `Período consultado: ${periodStart} a ${periodEnd}.`}
</p>
)}
{requestState === 'error' &&
scenarioMode === 'approved_parameters' && (
<button
type="button"
onClick={() => setRetry((value) => value + 1)}
className="rounded border border-amber-900 px-4 py-2 text-amber-950 underline focus-visible:outline-2 focus-visible:outline-offset-2"
>
Tentar novamente
</button>
)}
{scenarioMode === 'custom_planning' && (
<p role="note" className="text-sm">
Modo de planeamento separado: pressupostos do utilizador,
@@ -172,6 +200,22 @@ export function SimulationFiscalStatus({
{salaryViewMessages[salaryView]}
</p>
)}
{diagnostics.length > 0 && <div role="alert" className="space-y-3 rounded border border-amber-700 p-3">
<h4 className="font-semibold">IVA — taxa geral: condições não satisfeitas</h4>
<p>Não foi possível resolver este parâmetro. As respostas foram preservadas; esta consulta não executa o cálculo do imposto.</p>
{diagnostics.map((detail, index) => <div key={index}>
<ul className="list-disc pl-5">{detail.issues.map((issue, issueIndex) => <li key={issueIndex}>
{ivaIssueText(issue)}{' '}
{issue.field && <a className="underline" href={`#field-${issue.field}`} onClick={(event) => { if (onCorrect) { event.preventDefault(); onCorrect(issue.field!); } }}>Rever {issue.label.toLowerCase()}</a>}
{issue.category === 'system_configuration' && issue.field === 'fiscal_operation' && <a className="ml-2 underline" href="#published-iva">Abrir IVA — taxa geral</a>}
</li>)}</ul>
<details className="mt-2"><summary className="cursor-pointer underline">Ver fundamento</summary>
<p>IVA — taxa geral · versão {detail.rule_version}. Vigência: {detail.validity.start} a {detail.validity.end ?? 'sem data final indicada'}.</p>
<p>{detail.source.title} — {detail.source.diploma}, {detail.source.article}</p>
{detail.source.url?.startsWith('https://') && <a className="underline" href={detail.source.url} target="_blank" rel="noreferrer">Consultar fonte</a>}
</details>
</div>)}
</div>}
<ul className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
{fiscalComponentCodes.map((code) => {
const selected =
@@ -197,13 +241,13 @@ export function SimulationFiscalStatus({
</p>
) : parameters.length === 0 ? (
<p className="text-sm">
Indeterminado — nenhum parâmetro publicado
{code === 'IVA' ? 'Indeterminado — não existe uma versão elegível visível nesta consulta. Consulte o diagnóstico do período acima, quando disponível. Sem esse diagnóstico, não é possível distinguir ausência de cobertura de indisponibilidade da regra ou da fonte; não se atribui o problema às suas respostas.' : <>Indeterminado — nenhum parâmetro publicado
aplicável. Faltam parâmetros aprovados para{' '}
{code}, no período {periodStart} a{' '}
{periodEnd} e no âmbito indicado. A consulta
não identifica um conjunto obrigatório de
parâmetros; não é possível determinar o
imposto nem substituir as lacunas por zero.
imposto nem substituir as lacunas por zero.</>}
</p>
) : (
<div className="space-y-2 text-sm">
@@ -213,7 +257,7 @@ export function SimulationFiscalStatus({
className="border-t pt-2"
>
<strong>
{parameter.parameter_code}
{parameter.parameter_code === 'IVA_GENERAL_RATE' ? 'IVA — taxa geral' : parameter.parameter_code}
</strong>
{parameter.status === 'resolved' ? (
<>
@@ -224,10 +268,10 @@ export function SimulationFiscalStatus({
</p>
<p>
Regra{' '}
{parameter.rule_code} v
{parameter.parameter_code === 'IVA_GENERAL_RATE' ? 'IVA — taxa geral' : parameter.rule_code} v
{parameter.rule_version}
; âmbito{' '}
{parameter.scope_key}
{parameter.parameter_code === 'IVA_GENERAL_RATE' ? 'operação interna geral sobre base líquida em MZN' : parameter.scope_key}
</p>
<p>
Vigência:{' '}
@@ -265,13 +309,13 @@ export function SimulationFiscalStatus({
target="_blank"
rel="noreferrer"
>
Consultar fundamento
Ver fundamento
</a>
)}
</>
) : (
<p>
{labels[parameter.status] ??
{(parameter.parameter_code === 'IVA_GENERAL_RATE' && diagnostics.length ? 'Consulte acima cada condição e o fundamento da versão.' : labels[parameter.status]) ??
'Indeterminado — não foi possível validar o parâmetro.'}
</p>
)}

Some files were not shown because too many files have changed in this diff Show More