Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a76369eb86 | ||
|
|
57fc914821 | ||
|
|
0b0cf76137 | ||
|
|
3f5430c3ce | ||
|
|
0d7b70459d | ||
|
|
f3ec686f8c | ||
|
|
bd5166bd11 | ||
|
|
d3fe8a02ec | ||
|
|
f5b1d67ee5 | ||
|
|
90179149c7 | ||
|
|
364be835fe | ||
|
|
4a4d121275 | ||
|
|
480effa5fc | ||
|
|
226fd308dd | ||
|
|
2ced7b790c | ||
|
|
d186f94206 | ||
|
|
83d2f97d82 | ||
|
|
37085b40c1 | ||
|
|
325d58b694 | ||
|
|
0fafd348e7 | ||
|
|
13f68f1bd8 | ||
|
|
c22003a3da | ||
|
|
a690c665fb | ||
|
|
59ab4fe956 | ||
|
|
e69ab1499f | ||
|
|
3258a25f76 | ||
|
|
0e7d51f18f | ||
|
|
fd341b627e | ||
|
|
1350b4df73 |
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ class OpportunityController extends Controller
|
||||
$term = $validated['q'];
|
||||
$query->where(fn ($q) => $q
|
||||
->whereLike('title', "%{$term}%")
|
||||
->orWhereLike('description', "%{$term}%")
|
||||
->orWhereLike('summary', "%{$term}%")
|
||||
->orWhereLike('reference', "%{$term}%")
|
||||
->orWhereLike('province', "%{$term}%")
|
||||
@@ -54,7 +55,7 @@ class OpportunityController extends Controller
|
||||
}
|
||||
|
||||
if (! empty($validated['maturity_level'])) {
|
||||
$query->where('maturity_level', $validated['maturity_level']);
|
||||
\App\Services\OpportunityMaturity::constrainSearch($query, 'historical:'.$validated['maturity_level']);
|
||||
}
|
||||
|
||||
$results = $query->orderByDesc('is_featured')
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ class InvestmentDiscoveryController extends Controller
|
||||
|
||||
return Inertia::render('investment/catalogue', ['filters' => $filters, 'catalogueUrl' => $request->routeIs('investment.education') ? '/educacao' : '/oportunidades', 'references' => $catalogue->references(false, $filters),
|
||||
'territorySelection' => app(InvestmentGeography::class)->selection($filters),
|
||||
'guides' => app(\App\Services\SectorInvestment::class)->guides(\App\Models\Sector::where('slug', $filters['sector'] ?? '')->where('is_active', true)->whereNotNull('published_at')->first()),
|
||||
'results' => $catalogue->query($filters)->paginate(12)->withQueryString(),
|
||||
'favourites' => $request->user() ? DB::table('opportunity_favourites')->where('user_id', $request->user()->id)->pluck('opportunity_id') : [],
|
||||
'territorial' => $this->territorial($filters)]);
|
||||
@@ -53,7 +54,7 @@ class InvestmentDiscoveryController extends Controller
|
||||
if (! empty($filters['geo_location_id'])) {
|
||||
app(InvestmentGeography::class)->constrainDossiersToTerritories($query, app(InvestmentGeography::class)->descendants($filters['geo_location_id']));
|
||||
}
|
||||
foreach (['association_type', 'association_target', 'association_state', 'association_purpose', 'authority_id', 'chain_id', 'subsector_id', 'category', 'publication_state', 'zee_id', 'zfi_id', 'park_id', 'education', 'facets', 'sector', 'province', 'district', 'min_amount', 'max_amount', 'currency', 'before', 'maturity_level'] as $field) {
|
||||
foreach (['association_type', 'association_target', 'association_state', 'association_purpose', 'authority_id', 'chain_id', 'subsector_id', 'category', 'publication_state', 'zee_id', 'zfi_id', 'park_id', 'education', 'facets', 'sector', 'province', 'district', 'min_amount', 'max_amount', 'currency', 'before', 'maturity_level', 'preparation'] as $field) {
|
||||
if (isset($filters[$field]) && $filters[$field] !== '' && $filters[$field] !== false && $filters[$field] !== []) {
|
||||
return $query->whereRaw('false')->get();
|
||||
}
|
||||
@@ -75,9 +76,9 @@ class InvestmentDiscoveryController extends Controller
|
||||
return response()->json($catalogue->query($catalogue->filters($request))->paginate($request->integer('per_page', 12))->withQueryString());
|
||||
}
|
||||
|
||||
public function references(InvestmentCatalogue $catalogue): JsonResponse
|
||||
public function references(Request $request, InvestmentCatalogue $catalogue): JsonResponse
|
||||
{
|
||||
return response()->json($catalogue->references());
|
||||
return response()->json($catalogue->references(false, $catalogue->filters($request)));
|
||||
}
|
||||
|
||||
public function detail(Request $request, string $slug, InvestmentCatalogue $catalogue): Response
|
||||
@@ -106,6 +107,7 @@ class InvestmentDiscoveryController extends Controller
|
||||
unset($returnFilters['zone_id'], $returnFilters['from_dossier']);
|
||||
|
||||
return Inertia::render('investment/detail', ['record' => $record, 'context' => $catalogue->context($record->id), 'zone_origin' => $origin,
|
||||
'guides' => app(\App\Services\SectorInvestment::class)->guides($record->sector),
|
||||
'dossierOrigin' => $dossierOrigin ? ['id' => $dossierOrigin->id, 'title' => $dossierOrigin->title] : null,
|
||||
'returnUrl' => $returnFilters === [] ? '/oportunidades' : '/oportunidades?'.http_build_query($returnFilters)]);
|
||||
}
|
||||
@@ -131,7 +133,7 @@ class InvestmentDiscoveryController extends Controller
|
||||
{
|
||||
$filters = $catalogue->filters($request);
|
||||
$context = $this->territorial($filters)->map(fn ($d) => ['id' => 'dossier:'.$d->id, 'title' => $d->title, 'kind' => $d->cluster_id ? 'Cluster' : ($d->corridor_id ? 'Corredor' : 'Projeto âncora'), 'url' => '/carteira/'.$d->id]);
|
||||
$opportunityOnlyFilters = array_filter(Arr::only($filters, ['association_type', 'association_target', 'association_state', 'association_purpose', 'authority_id', 'chain_id', 'subsector_id', 'category', 'publication_state', 'education', 'sector', 'facets', 'project_id', 'cluster_id', 'corridor_id', 'min_amount', 'max_amount', 'currency', 'before', 'maturity_level']), fn ($value) => $value !== null && $value !== '' && $value !== false && $value !== []);
|
||||
$opportunityOnlyFilters = array_filter(Arr::only($filters, ['association_type', 'association_target', 'association_state', 'association_purpose', 'authority_id', 'chain_id', 'subsector_id', 'category', 'publication_state', 'education', 'sector', 'facets', 'project_id', 'cluster_id', 'corridor_id', 'min_amount', 'max_amount', 'currency', 'before', 'maturity_level', 'preparation']), fn ($value) => $value !== null && $value !== '' && $value !== false && $value !== []);
|
||||
if ($opportunityOnlyFilters === [] && ! in_array($filters['dimension'] ?? null, ['cluster', 'corridor'], true)) {
|
||||
$zoneFilters = $filters;
|
||||
if (in_array($filters['dimension'] ?? null, ['zee', 'zfi'], true)) {
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -16,4 +16,9 @@ class InvestmentLegalSource extends Model
|
||||
use HasUuids;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['fiscal_acceptance' => 'array'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ class InvestmentCatalogue
|
||||
public function filters(Request $request): array
|
||||
{
|
||||
$filters = $request->validate(['q' => ['nullable', 'string', 'max:120'], 'sector' => ['nullable', 'string', 'max:80'],
|
||||
'preparation' => ['nullable', \Illuminate\Validation\Rule::in(array_column(OpportunityMaturity::searchOptions(), 'value'))],
|
||||
'opportunity_id' => ['nullable', 'integer', 'min:1'],
|
||||
'province_id' => ['nullable', 'uuid'], 'district_id' => ['nullable', 'uuid'], 'admin_post_id' => ['nullable', 'uuid'], 'locality_id' => ['nullable', 'uuid'],
|
||||
'include_national' => ['nullable', 'boolean'], 'territory_basis' => ['nullable', 'in:all,direct,context'],
|
||||
@@ -40,6 +41,17 @@ class InvestmentCatalogue
|
||||
'publication_state' => ['nullable', 'in:published'],
|
||||
'per_page' => ['nullable', 'integer', 'between:1,50'], 'page' => ['nullable', 'integer', 'min:1']]);
|
||||
app(InvestmentGeography::class)->selection($filters);
|
||||
$aliases = ['park' => 'park_id', 'cluster' => 'cluster_id', 'corridor' => 'corridor_id', 'project' => 'project_id'];
|
||||
$alias = $aliases[$filters['association_type'] ?? ''] ?? null;
|
||||
if ($alias && ! empty($filters['association_target']) && empty($filters['association_purpose']) && empty($filters['association_state'])) {
|
||||
if (! empty($filters[$alias]) && $filters[$alias] !== $filters['association_target']) {
|
||||
throw \Illuminate\Validation\ValidationException::withMessages([$alias => 'Foram indicadas duas selecções diferentes para o mesmo filtro. Escolha apenas uma.']);
|
||||
}
|
||||
if (\Illuminate\Support\Str::isUuid($filters['association_target'])) {
|
||||
$filters[$alias] = $filters['association_target'];
|
||||
unset($filters['association_type'], $filters['association_target']);
|
||||
}
|
||||
}
|
||||
|
||||
return $filters;
|
||||
}
|
||||
@@ -51,6 +63,7 @@ class InvestmentCatalogue
|
||||
public function query(array $filters): Builder
|
||||
{
|
||||
$query = Opportunity::published()->with('sector:id,name,slug');
|
||||
$associated = $this->publicAssociatedIds($filters);
|
||||
if (($filters['territory_basis'] ?? 'all') === 'direct') {
|
||||
$query->where(fn ($q) => $q->whereNotNull('opportunities.primary_territory_id')->orWhereExists(fn ($l) => $l->selectRaw('1')->from('opportunity_locations')->whereColumn('opportunity_id', 'opportunities.id')));
|
||||
} elseif (($filters['territory_basis'] ?? 'all') === 'context') {
|
||||
@@ -92,15 +105,17 @@ class InvestmentCatalogue
|
||||
}
|
||||
foreach (['zee_id' => 'zee', 'zfi_id' => 'zfi', 'park_id' => 'industrial_park'] as $field => $category) {
|
||||
if (! empty($filters[$field])) {
|
||||
$query->whereIn('opportunities.id', app(InvestmentZones::class)->links()
|
||||
$legacy = app(InvestmentZones::class)->links()->where('zl.association_type', '<>', 'proximity')
|
||||
->join('investment_item_opportunities as mapping', 'mapping.item_id', '=', 'zl.item_id')
|
||||
->join('investment_zones as z', 'z.id', '=', 'zl.zone_id')->where('z.category', $category)
|
||||
->where('zl.zone_id', $filters[$field])->select('mapping.opportunity_id'));
|
||||
->where('zl.zone_id', $filters[$field])->select('mapping.opportunity_id');
|
||||
$query->where(fn ($matches) => $matches->whereIn('opportunities.id', $legacy)
|
||||
->orWhereIn('opportunities.id', $associated[$field] ?? []));
|
||||
}
|
||||
}
|
||||
if (! empty($filters['q'])) {
|
||||
$term = '%'.$filters['q'].'%';
|
||||
$query->where(fn ($q) => $q->where('title', 'ilike', $term)->orWhere('summary', 'ilike', $term)->orWhere('reference', 'ilike', $term)
|
||||
$query->where(fn ($q) => $q->where('title', 'ilike', $term)->orWhere('description', 'ilike', $term)->orWhere('summary', 'ilike', $term)->orWhere('reference', 'ilike', $term)
|
||||
->orWhere('province', 'ilike', $term)->orWhereHas('sector', fn ($s) => $s->where('name', 'ilike', $term))
|
||||
->orWhereExists(fn ($g) => $g->selectRaw('1')->from('geo_locations as search_location')->whereNull('search_location.deleted_at')->where('search_location.data_classification', 'public')
|
||||
->where('search_location.name', 'ilike', $term)->whereIn('search_location.id', app(InvestmentGeography::class)->publicReferences()->select('id'))
|
||||
@@ -116,11 +131,21 @@ class InvestmentCatalogue
|
||||
if (! empty($filters['sector'])) {
|
||||
SectorOrganization::constrain($query, $filters['sector']);
|
||||
}
|
||||
foreach (['province', 'district', 'maturity_level'] as $field) {
|
||||
foreach (['province', 'district'] as $field) {
|
||||
if (! empty($filters[$field])) {
|
||||
$query->where($field, $filters[$field]);
|
||||
}
|
||||
}
|
||||
if (! empty($filters['preparation'])) {
|
||||
OpportunityMaturity::constrainSearch($query, $filters['preparation']);
|
||||
} elseif (! empty($filters['maturity_level'])) {
|
||||
// Legacy links predate the eight-stage catalogue: retain their original meaning.
|
||||
OpportunityMaturity::constrainSearch($query, match ((int) $filters['maturity_level']) {
|
||||
7 => 'stage:implementation',
|
||||
8 => 'stage:operational',
|
||||
default => 'historical:'.$filters['maturity_level'],
|
||||
});
|
||||
}
|
||||
foreach (self::RELATIONS as $relation => [$foreign, $table]) {
|
||||
if (! empty($filters[$foreign])) {
|
||||
$locationIds = $relation === 'locations'
|
||||
@@ -141,13 +166,15 @@ class InvestmentCatalogue
|
||||
continue;
|
||||
}
|
||||
if (in_array($relation, ['clusters', 'corridors'], true)) {
|
||||
$query->whereExists(fn ($sub) => $sub->selectRaw('1')->from('opportunity_'.$relation.' as link')
|
||||
$query->where(fn ($matches) => $matches->whereExists(fn ($sub) => $sub->selectRaw('1')->from('opportunity_'.$relation.' as link')
|
||||
->whereColumn('link.opportunity_id', 'opportunities.id')->where('link.'.$foreign, $filters[$foreign])
|
||||
->whereIn('link.'.$foreign, $this->publicStructureIds($relation, $foreign)));
|
||||
->whereIn('link.'.$foreign, $this->publicStructureIds($relation, $foreign)))
|
||||
->orWhereIn('opportunities.id', $associated[$foreign] ?? []));
|
||||
} else {
|
||||
$query->whereExists(fn ($sub) => $sub->selectRaw('1')->from('opportunity_'.$relation.' as link')->join($table.' as target', 'target.id', '=', 'link.'.$foreign)
|
||||
$query->where(fn ($matches) => $matches->whereExists(fn ($sub) => $sub->selectRaw('1')->from('opportunity_'.$relation.' as link')->join($table.' as target', 'target.id', '=', 'link.'.$foreign)
|
||||
->whereColumn('link.opportunity_id', 'opportunities.id')->where('link.'.$foreign, $filters[$foreign])->whereNull('target.deleted_at')
|
||||
->whereIn('target.id', InvestmentPortfolioController::visible()->whereNotNull($foreign)->pluck($foreign)));
|
||||
->whereIn('target.id', InvestmentPortfolioController::visible()->whereNotNull($foreign)->pluck($foreign)))
|
||||
->orWhereIn('opportunities.id', $associated[$foreign] ?? []));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,7 +214,9 @@ class InvestmentCatalogue
|
||||
* @return array<string,mixed> */
|
||||
public function references(bool $management = false, array $territoryFilters = []): array
|
||||
{
|
||||
$path = app(InvestmentGeography::class)->selection($territoryFilters);
|
||||
$result = ['classification' => app(OpportunityClassification::class)->options(),
|
||||
'preparationOptions' => OpportunityMaturity::searchOptions(),
|
||||
'authorities' => DB::table('authorities')->where('status', 'published')->get(['id', 'label_pt_mz']),
|
||||
'categories' => OpportunityManagement::CATEGORIES,
|
||||
'options' => DB::table('investment_reference_options')->when(! $management, fn ($q) => $q->where('active', true))->orderBy('sort_order')->orderBy('label')->get(),
|
||||
@@ -205,18 +234,67 @@ class InvestmentCatalogue
|
||||
$query->whereIn('id', InvestmentPortfolioController::visible()->whereNotNull($foreign)->pluck($foreign));
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
$result[$relation] = $query->orderBy('name')->get(['id', 'name']);
|
||||
}
|
||||
|
||||
$zones = app(InvestmentZones::class)->visible();
|
||||
if (! $management && $territoryFilters !== []) {
|
||||
$zones = app(InvestmentZones::class)->search($territoryFilters);
|
||||
// Opportunity words/modalities are not names/categories of zones.
|
||||
$zones = app(InvestmentZones::class)->search(\Illuminate\Support\Arr::only($territoryFilters,
|
||||
['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();
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function publicAssociatedIds(array $filters): array
|
||||
{
|
||||
$kinds = ['park_id' => ['park'], 'zee_id' => ['zone'], 'zfi_id' => ['zone'],
|
||||
'cluster_id' => ['cluster'], 'corridor_id' => ['corridor'], 'project_id' => ['project']];
|
||||
$selected = array_filter(array_intersect_key($filters, $kinds));
|
||||
foreach (['zee_id' => 'zee', 'zfi_id' => 'zfi', 'park_id' => 'industrial_park'] as $key => $category) {
|
||||
if (isset($selected[$key]) && ! app(InvestmentZones::class)->visible()->whereKey($selected[$key])->where('category', $category)->exists()) unset($selected[$key]);
|
||||
}
|
||||
if ($selected === []) return [];
|
||||
$ids = [];
|
||||
Opportunity::published()->select('opportunities.*')->chunkById(200, function ($records) use ($selected, $kinds, &$ids) {
|
||||
foreach ($records as $record) {
|
||||
foreach (app(OpportunityClassification::class)->publicRows($record) as $row) {
|
||||
if (in_array($row['purpose'], ['proximity', 'administrative_boundary'], true) || $row['state'] === 'rejected') continue;
|
||||
foreach ($selected as $field => $target) {
|
||||
if ($row['target_id'] === $target && in_array($row['target_type'], $kinds[$field], true)) $ids[$field][] = $record->id;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return $ids;
|
||||
}
|
||||
|
||||
private function publicStructureIds(string $relation, string $foreign): \Illuminate\Database\Query\Builder
|
||||
{
|
||||
$table = self::RELATIONS[$relation][1];
|
||||
@@ -329,11 +407,11 @@ class InvestmentCatalogue
|
||||
DB::raw("CASE WHEN o.primary_territory_id=g.id OR EXISTS (SELECT 1 FROM opportunity_locations direct WHERE direct.opportunity_id=o.id AND direct.geo_location_id=g.id) THEN 'território declarado da oportunidade' ELSE 'contexto do projeto associado; implantação da oportunidade por confirmar' END AS basis")])
|
||||
->map(fn ($area) => ['geometry' => json_decode($area->geometry, true), 'title' => $area->title.' — '.$area->name.' ('.$area->basis.')',
|
||||
'source_reference' => $area->source_system, 'source_date' => json_decode($area->properties ?? '{}', true)['source_date'] ?? null,
|
||||
'url' => '/oportunidades/'.$area->slug])->all();
|
||||
'url' => '/oportunidades/'.$area->slug.($filters === [] ? '' : '?'.http_build_query($filters))])->all();
|
||||
$located = 0;
|
||||
foreach ($records as $record) {
|
||||
$properties = ['id' => $record->id, 'title' => $record->title, 'category' => $record->sector?->name,
|
||||
'location' => trim($record->province.' '.$record->district), 'url' => '/oportunidades/'.$record->slug];
|
||||
'location' => trim($record->province.' '.$record->district), 'url' => '/oportunidades/'.$record->slug.($filters === [] ? '' : '?'.http_build_query($filters))];
|
||||
foreach ($relations as $relation => $groups) {
|
||||
$properties[$relation] = ($groups->get($record->id) ?? collect())->pluck('name')->unique()->values()->all();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -21,7 +22,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'];
|
||||
|
||||
public function __construct(private readonly VirtualDesk $desk) {}
|
||||
|
||||
@@ -34,8 +35,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 +72,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 +144,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 +201,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 +214,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
|
||||
@@ -235,7 +270,8 @@ final class InvestorContext
|
||||
abort_if(array_diff(array_keys($input), ['version', 'selection', 'declarations', 'replace_fields', 'preview_hash', 'operation_key', 'confirm', '_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',
|
||||
'selection.*' => 'nullable|string|max:100', 'declarations' => 'nullable|array:project_title,project_summary,currency,expected_start_date,investor_origin,person_type,entity_stage,tax_residency_country,fiscal_residency_country,project_type,maturity_stage', 'declarations.project_title' => 'nullable|string|max:200', 'declarations.project_summary' => 'nullable|string|max:5000',
|
||||
'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();
|
||||
}
|
||||
|
||||
@@ -299,7 +335,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,11 +350,16 @@ 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;
|
||||
$payload = $this->desk->decode($case->payload);
|
||||
$results = $payload['investor_path']['results'] ?? [];
|
||||
$requests = DB::table('desk_requests')->where('case_id', $id)->orderBy('id')->get(['id', 'status', 'protocol', 'version'])->all();
|
||||
$steps = $dependencyChanged ? array_values(array_unique([...(array_intersect($changed, ['opportunity', 'sector']) ? ['S02', 'S03', 'S04', 'S08', 'S12', 'S13', 'S14', 'S15', 'S17'] : []), ...(array_intersect($changed, ['territory', 'park']) ? ['S08', 'S09', 'S10', 'S11', 'S17'] : [])])) : [];
|
||||
if ($conditionsChanged) {
|
||||
$steps = array_values(array_unique([...$steps, 'S02', 'S03', 'S08', 'S12', 'S13', 'S17']));
|
||||
}
|
||||
$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(),
|
||||
@@ -362,7 +403,7 @@ final class InvestorContext
|
||||
$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];
|
||||
'dependency_hash' => $this->dependencyHash(['selection' => $plan['selection'], 'fields' => Arr::except($plan['fields'], ['project_title', 'project_summary'])]), 'updated_at' => now()->toIso8601String(), 'updated_by' => $actor->id];
|
||||
if ($plan['dependency_changed']) {
|
||||
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 +425,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) {
|
||||
@@ -404,10 +447,19 @@ final class InvestorContext
|
||||
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']);
|
||||
$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,
|
||||
'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)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ final class LegacyContent
|
||||
}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
public function article(string $slug = 'agronegocio'): array
|
||||
public function article(string $slug = 'agronegocio', bool $includeRelated = true): array
|
||||
{
|
||||
abort_unless($this->available(), 404);
|
||||
$identity = config('legacy_content.articles.'.$slug);
|
||||
@@ -80,13 +80,26 @@ final class LegacyContent
|
||||
}
|
||||
preg_match_all('~href="([^"]+)"~i', $localHtml, $existingLinks);
|
||||
$alreadyLinked = array_map(fn ($href) => html_entity_decode($href, ENT_QUOTES | ENT_HTML5, 'UTF-8'), $existingLinks[1]);
|
||||
$relatedArticles = array_values(array_filter($this->related($identity['group'], $slug), fn ($item) => !in_array($item['href'], $alreadyLinked, true)));
|
||||
$relatedArticles = [];
|
||||
if ($includeRelated) {
|
||||
foreach ($this->related($identity['group'], $slug) as $item) {
|
||||
if (in_array($item['href'], $alreadyLinked, true)) continue;
|
||||
try {
|
||||
$relatedArticles[] = $this->article($item['slug'], false);
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $error) {
|
||||
if ($error->getStatusCode() !== 404) throw $error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ['id' => (int) $row->resource_id, 'document_id' => $row->id, 'version' => (int) $row->metadata_version,
|
||||
'sha256' => trim($row->sha256), 'title' => $source['title'], 'author' => $source['author'],
|
||||
'legacy_id' => $source['legacy_id'], 'legacy_category' => $source['legacy_category'],
|
||||
'published_at' => $source['published_at'], 'modified_at' => $source['modified_at'], 'source_url' => $source['source_url'],
|
||||
'href' => '/'.$slug, 'html' => $localHtml,
|
||||
'href' => '/'.$slug, 'html' => $localHtml, 'summary' => self::excerpt($localHtml),
|
||||
'opportunities_href' => $sector ? '/oportunidades?sector='.rawurlencode($sector->slug) : null,
|
||||
'opportunities_label' => $sector ? 'Ver oportunidades de '.$sector->name : null,
|
||||
'has_opportunities' => count($opportunities) > 0,
|
||||
'landing_href' => '/explorar/'.$identity['group'], 'start_href' => '/investimentos/a-partir-de/'.$slug,
|
||||
'group' => config('legacy_content.groups.'.$identity['group']),
|
||||
'related_articles' => $relatedArticles,
|
||||
@@ -101,6 +114,22 @@ final class LegacyContent
|
||||
'consultation_state' => 'public', 'legal_state' => 'not_applicable', 'calculation_use' => false];
|
||||
}
|
||||
|
||||
/** An excerpt of the displayed edition, never a claim about returns or availability. */
|
||||
public static function excerpt(string $html): string
|
||||
{
|
||||
$plain = html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
||||
if (str_contains($plain, 'Hotelaria e resorts') && str_contains($plain, 'Ecoturismo')
|
||||
&& str_contains($plain, 'Infraestruturas turísticas') && str_contains($plain, 'Oportunidades Regionais')) {
|
||||
return 'Conheça os temas de turismo e hospitalidade abordados no guia: hotelaria, ecoturismo, infraestruturas de apoio e referências regionais. Use este enquadramento para preparar a sua ideia de investimento.';
|
||||
}
|
||||
preg_match_all('~<p\b[^>]*>(.*?)</p>~isu', $html, $paragraphs);
|
||||
foreach ([...($paragraphs[1] ?? []), $html] as $paragraph) {
|
||||
$text = trim(preg_replace('/\s+/u', ' ', html_entity_decode(strip_tags($paragraph), ENT_QUOTES | ENT_HTML5, 'UTF-8')));
|
||||
if (mb_strlen($text) >= 60) return \Illuminate\Support\Str::limit($text, 320);
|
||||
}
|
||||
return trim(html_entity_decode(strip_tags($html), ENT_QUOTES | ENT_HTML5, 'UTF-8'));
|
||||
}
|
||||
|
||||
/** Resolve a library identity, preserving the already stored source URLs and versions. */
|
||||
/** @return array<string, mixed> */
|
||||
public function articleForResource(int $resourceId): array
|
||||
|
||||
@@ -383,7 +383,7 @@ class OpportunityClassification
|
||||
$matches = $matches && collect($rows)->contains(fn ($r) => $r['authority_id'] === $filters['authority_id']);
|
||||
}
|
||||
if (! empty($filters['subsector_id'])) {
|
||||
$matches = $matches && (($record->workflow_context['publication']['taxonomy']['subsector_id'] ?? '') === $filters['subsector_id'] || collect($rows)->contains(fn ($r) => $r['target_type'] === 'activity' && $r['target_id'] === $filters['subsector_id']));
|
||||
$matches = $matches && (in_array($filters['subsector_id'], array_intersect_key($record->workflow_context['publication']['taxonomy'] ?? [], array_flip(['subsector_id','economic_activity_id'])), true) || collect($rows)->contains(fn ($r) => $r['target_type'] === 'activity' && $r['target_id'] === $filters['subsector_id']));
|
||||
}
|
||||
if ($matches) {
|
||||
$ids[] = $record->id;
|
||||
|
||||
@@ -24,6 +24,32 @@ final class OpportunityMaturity
|
||||
|
||||
public const LEGACY_LABELS = [1 => 'Ideia identificada', 'Pré-estudo disponível', 'Estudo de viabilidade em curso', 'Projecto estruturado', 'Pronto para investimento', 'Em negociação ou implementação'];
|
||||
|
||||
/** Public selection is explicit; historical numbers never stand for current stages. */
|
||||
public static function searchOptions(): array
|
||||
{
|
||||
$options = [];
|
||||
foreach (self::STAGES as $code => $label) {
|
||||
$options[] = ['value' => 'stage:'.$code, 'label' => $label, 'group' => 'Etapas de preparação'];
|
||||
}
|
||||
foreach (self::LEGACY_LABELS as $level => $label) {
|
||||
$options[] = ['value' => 'historical:'.$level, 'label' => $label.' — registo histórico', 'group' => 'Classificações históricas'];
|
||||
}
|
||||
return $options;
|
||||
}
|
||||
|
||||
public static function constrainSearch(\Illuminate\Database\Eloquent\Builder $query, string $selection): void
|
||||
{
|
||||
[$kind, $value] = explode(':', $selection, 2);
|
||||
if ($kind === 'stage') {
|
||||
$query->where('workflow_context->publication->maturity->catalogue_version', self::VERSION)
|
||||
->where('workflow_context->publication->maturity->stage', $value);
|
||||
} else {
|
||||
$query->where(fn ($q) => $q->where('workflow_context->publication->maturity->catalogue_version', self::LEGACY)
|
||||
->orWhere(fn ($old) => $old->whereNull('workflow_context->publication->maturity->catalogue_version')->whereNull('workflow_context->publication->maturity->stage')))
|
||||
->where('maturity_level', (int) $value);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $data
|
||||
* @return array<string,mixed> */
|
||||
public function normalize(array $data): array
|
||||
|
||||
@@ -37,19 +37,7 @@ final class SectorInvestment
|
||||
public function publicView(Sector $sector): array
|
||||
{
|
||||
$query = SectorOrganization::opportunities($sector);
|
||||
$articles = [];
|
||||
if (app(LegacyContent::class)->available()) {
|
||||
foreach (self::CONTENT[$sector->slug] ?? [] as $slug) {
|
||||
try {
|
||||
$a = app(LegacyContent::class)->article($slug);
|
||||
$articles[] = ['title' => $a['title'], 'href' => $a['href'], 'image' => $a['image'], 'sha256' => $a['sha256'], 'scope' => $slug === 'tech-fintech' && $sector->slug !== 'fintech' ? 'Especialização Fintech; não representa todo o setor.' : 'Conteúdo educativo relacionado; não é uma oportunidade concreta.'];
|
||||
} catch (HttpException $e) {
|
||||
if ($e->getStatusCode() !== 404) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$articles = $this->guides($sector);
|
||||
$relations = [];
|
||||
foreach (['projects' => 'project_id', 'clusters' => 'cluster_id', 'corridors' => 'corridor_id'] as $table => $foreign) {
|
||||
$ids = DB::table('opportunity_'.$table)->whereIn('opportunity_id', (clone $query)->select('id'))->pluck($foreign);
|
||||
@@ -74,6 +62,26 @@ final class SectorInvestment
|
||||
'content_notice' => $sector->slug === 'mercado-financeiro' ? 'O artigo geral de Mercado Financeiro permanece vazio e não foi publicado. A preparação de negócios neste setor está disponível.' : null];
|
||||
}
|
||||
|
||||
/** Published guides for an explicit sector; never inferred from proximity. */
|
||||
public function guides(?Sector $sector): array
|
||||
{
|
||||
if (! $sector) return [];
|
||||
$articles = [];
|
||||
if (app(LegacyContent::class)->available()) {
|
||||
foreach (self::CONTENT[$sector->slug] ?? [] as $slug) {
|
||||
try {
|
||||
$a = app(LegacyContent::class)->article($slug, false);
|
||||
$articles[] = \Illuminate\Support\Arr::only($a, ['title', 'href', 'image', 'sha256', 'summary', 'author', 'opportunities_href', 'opportunities_label', 'has_opportunities']);
|
||||
} catch (HttpException $e) {
|
||||
if ($e->getStatusCode() !== 404) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $articles;
|
||||
}
|
||||
|
||||
public function attach(User $actor, string $id, array $input): void
|
||||
{
|
||||
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'sector_slug' => 'required|string|max:150'])->validate();
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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')))
|
||||
|
||||
@@ -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
@@ -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
@@ -78,7 +78,7 @@ return [
|
||||
|
|
||||
*/
|
||||
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
'locale' => env('APP_LOCALE', 'pt'),
|
||||
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
|
||||
@@ -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),
|
||||
];
|
||||
@@ -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', '')))))),
|
||||
|
||||
+68
@@ -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');
|
||||
}
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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."
|
||||
|
||||
|
@@ -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.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,84 @@
|
||||
{
|
||||
"branch": "codex/educational-content-cards",
|
||||
"base": "0e7d51f18fb424857fdaad48498a2fc43c044749",
|
||||
"scope": "New QA delta; separate Tempo review before main",
|
||||
"files": [
|
||||
{
|
||||
"path": "app/Http/Controllers/InvestmentDiscoveryController.php",
|
||||
"sha256": "693d1217ead894747c266a7295774b26a8b987f8faf06003ed60a6d3e15aca6c"
|
||||
},
|
||||
{
|
||||
"path": "app/Services/LegacyContent.php",
|
||||
"sha256": "e856ba2a042e971933a401fd9544a50f6bc5c1541c28062719d840a8277da7b8"
|
||||
},
|
||||
{
|
||||
"path": "app/Services/SectorInvestment.php",
|
||||
"sha256": "a0ee1c9bf864ef765ed70fc1bcdc839781d0bed7d705d991c801bfaa9bf7e4a0"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/unification/traceability-and-manual.md",
|
||||
"sha256": "bbbb79e84cae659d1921dfa4440e3fcc192521e0976cace0fa2694828219ec00"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv",
|
||||
"sha256": "420369f562508f68ea50e78d386721d6b62fa730893c4e6ccd06817f4dfb8a27"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/v2/auditoria-portal/backlog.md",
|
||||
"sha256": "d77e4931e8f1a0fcbf0f1d80be83a54b22682ffdc0286852d3ad167652056156"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv",
|
||||
"sha256": "757d457a295eb9a516abd1eec6002a52aa1d9a35faea291046e3c99681a1cdcb"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/educational-article-card.tsx",
|
||||
"sha256": "03a298406702cd3211c44fad1fdf79d455ad029d997d417a416f450d6d420b6e"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/educational-guides.tsx",
|
||||
"sha256": "2a2a52e50694e336f05544d51fc776a3bbc2b06d2ab61a1b3cdf1d261afca474"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/sector-entry.tsx",
|
||||
"sha256": "39004abf04f096b987d852d834ce9b27fdf557d2283db3b5868678d3ff0bff1a"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/investment/catalogue.tsx",
|
||||
"sha256": "ced5e8d4b2910aeeaeb709d6ff0eb2d492f96d9b671d6ff832eea66d022a1f70"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/investment/detail.tsx",
|
||||
"sha256": "719b3c008887676dd0b69ffe48bd5a64426de92362d92da8ad39412a578c51bc"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/portal/legacy.tsx",
|
||||
"sha256": "dcd8462e940614f5935efa19d9c9444fe3432fbc704e9c05144508d5c944c90d"
|
||||
},
|
||||
{
|
||||
"path": "tests/Feature/EducationalGuidesTest.php",
|
||||
"sha256": "aa9e3f62890f1d75f639ad4e7e05713c18112ec96a7fd49c864e5e007807d3cf"
|
||||
}
|
||||
],
|
||||
"build_manifest_sha256": "89c3ce0c67d9faf8cb3c5f7e9d74b3ad5d0ad70dff81ca2e37ca5cac7a471620",
|
||||
"migrations": [],
|
||||
"imports": [],
|
||||
"publication": false,
|
||||
"remote_operations": [],
|
||||
"ai": false,
|
||||
"evidence": {
|
||||
"backend": "1 test, 8 assertions; exact isolated QA guard and transactions",
|
||||
"types": "passed",
|
||||
"build": "passed",
|
||||
"desktop_width": 2048,
|
||||
"mobile_width": 390,
|
||||
"mobile_document_width": 375,
|
||||
"keyboard": "visible focus and Enter opened guide",
|
||||
"visual_data": "Read-only copy of existing public Tourism article/image; not synthetic",
|
||||
"real_library": "Normal sector -> local image 25 and source summary -> normal article /turismo -> library resource 10; existing isolated QA; no import"
|
||||
},
|
||||
"limitations": [
|
||||
"Tempo visual review pending before main integration; prior accepted dossier proof retained, not repeated"
|
||||
],
|
||||
"rollback": "Restore previous sources/build through existing deployment chain, preserving database/uploads/permissions and later data"
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
{
|
||||
"branch": "codex/opportunity-search",
|
||||
"base_commit": "1350b4df73e70ab35a28d923402ded7d836ade38",
|
||||
"date": "2026-10-20",
|
||||
"scope": "QA only; review before main integration",
|
||||
"qa_url": "http://127.0.0.1:18570/oportunidades",
|
||||
"files": [
|
||||
{
|
||||
"path": "app/Http/Controllers/Api/V1/OpportunityController.php",
|
||||
"sha256": "04fd696ce73fae8660491976781ef1bd96898a30af96363460427e7a9d139fe1"
|
||||
},
|
||||
{
|
||||
"path": "app/Http/Controllers/InvestmentDiscoveryController.php",
|
||||
"sha256": "a7301ce6c6285d8ec374b7013b8f0a0bb9951da83625acc47da223d2c4275bbf"
|
||||
},
|
||||
{
|
||||
"path": "app/Services/InvestmentCatalogue.php",
|
||||
"sha256": "4b44e18ca12c890e0220913c972ca6092e29f8975dfbc6d292b579b95f1c5e21"
|
||||
},
|
||||
{
|
||||
"path": "app/Services/OpportunityClassification.php",
|
||||
"sha256": "f9a24067807fa620142839ab9873dde465457fcfc61f501d1c8309534a6da175"
|
||||
},
|
||||
{
|
||||
"path": "app/Services/OpportunityMaturity.php",
|
||||
"sha256": "1cd73acb486d8e3ffa52c7419e48b9d69b450bb343f04664cd1a2b51e3317246"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/unification/traceability-and-manual.md",
|
||||
"sha256": "5596aa8edbbeff34a13613ec11f00f49c62e76a8e2185e5762d972fc70b667e7"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/v2/auditoria-portal/backlog-priorizado.csv",
|
||||
"sha256": "5d1de3418c5b021a0520bc6494a6a5efcef665ad1d20a45b11ae6aa702bedb01"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/v2/auditoria-portal/backlog.md",
|
||||
"sha256": "303b53346af2dad908f9d40d02c14a8f245155897dacf8b2538cdaae5a92bea7"
|
||||
},
|
||||
{
|
||||
"path": "docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv",
|
||||
"sha256": "4bbf47f7b5bfe54d2dd3b891eca3c04555329e92064c3bc6722a60c2f8eab4ce"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/catalogue-filter-context.tsx",
|
||||
"sha256": "b155d5e812b52fc52677cb9a8d44068f64821732c90a82dabb521eed44af047e"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/catalogue-navigation.tsx",
|
||||
"sha256": "88eccc6a9c2803fd26b678081f5da244a9c4ac9227664e7d0f1b57050b437239"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/country-select.tsx",
|
||||
"sha256": "53ef3742e4c55668d0ad85139be742405f5d1524196b3b1b3f3700c7bc311742"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/investment-map-panel.tsx",
|
||||
"sha256": "cfdb99b7dfa405d740f68cecef2b0df38488d2f63dc623da47a000037f0ec01e"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/investment-territory.tsx",
|
||||
"sha256": "35cef961efb51709cba271556f5e7c717b673ebbca480b680e37654c38cde324"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/opportunity-classification.tsx",
|
||||
"sha256": "fb4d843c8c056f10c34baa5ab9f2ba324e9796456a20455303b7a3b0747ee6cd"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/opportunity-search-filters.tsx",
|
||||
"sha256": "85e48e41537a5932e0398b7bc4bddd5322431ae517a91de2d53e83ef7927e54d"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/search-feedback.tsx",
|
||||
"sha256": "23ed85878e5b55a40e01928236f870f6dff4c2a7e19547beaa81a0a7d59c9c06"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/components/search-state.tsx",
|
||||
"sha256": "45cc210783625bcf96712064a0e7dd97f5681c499cdf60cedbe0e4127cba7aab"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/lib/catalogue-context.ts",
|
||||
"sha256": "c499ebda27b7672b55ea3e3da98c63c7d2d2cd490dcde77df3cc38e09792af73"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/lib/portal-navigation.ts",
|
||||
"sha256": "0d3bae6d726538b064cd992e6d433245321951c18198de5db540993232e6daaf"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/investment/business-marketplace.tsx",
|
||||
"sha256": "f45a56b250bbb5da19613c6b7ad94ba89ea501929f97636c797d92b4a2251b51"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/investment/catalogue.tsx",
|
||||
"sha256": "9ea5742950e2737bebb7bd7f04ee24129c7f9917c1986cf070a3e282e7a22981"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/investment/detail.tsx",
|
||||
"sha256": "c40926782a183fcde27415e0eda276f73189f071ba6ce715e79d007eb6388634"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/investment/shared.tsx",
|
||||
"sha256": "f71d13f0e0b5b0e152cf262f02b9e0770a58d32472ac878aa0adbca53d2edf3f"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/investment/zone.tsx",
|
||||
"sha256": "8cd64fb62af5e2d4582c46298568429e61e45247fca1b072287ad35ae07d76db"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/investment/zones.tsx",
|
||||
"sha256": "5fecb308c3800f6e2ee266fd8559d2b14bea3214f2ffbeb4b651b86f2bed7ea7"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/portal/services.tsx",
|
||||
"sha256": "6f26cace5d1808f6d563d0d8b0ebed6b44748abed7205e465d9658f8b9cb2d17"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/portfolio/index.tsx",
|
||||
"sha256": "2662cfe05c387cb876f8c352bd83a5a1d68056c615333e238ecb4e7b5489eb00"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/portfolio/shared.tsx",
|
||||
"sha256": "27b9d1c8dbced48532e7a57ce184810a32e3dea7cdc3a37a549dc7fbe8c04426"
|
||||
},
|
||||
{
|
||||
"path": "resources/js/pages/portfolio/show.tsx",
|
||||
"sha256": "51883858d09c6b07f281426fa9ec149c4de9f12286cc88b285caee01e892c892"
|
||||
},
|
||||
{
|
||||
"path": "tests/Feature/OpportunitySearchTest.php",
|
||||
"sha256": "b2d57a838849a7d1c9e5b43c3f471d3e5d78c59a7cdb8e0f81f74de5d886d7d3"
|
||||
},
|
||||
{
|
||||
"path": "tests/Support/OpportunitySearchFixture.php",
|
||||
"sha256": "46804da2f403c031ad206211db07f41f3f20dbf4f789f838e278e3f241d021f1"
|
||||
},
|
||||
{
|
||||
"path": "tests/frontend/portal-navigation.test.mjs",
|
||||
"sha256": "f19ff2d17a385ab1f28c65f69233a13d88d6b8b6ec7285848fcd2a945b883ff9"
|
||||
}
|
||||
],
|
||||
"build_manifest_sha256": "da35948622e0754e4ecc25247b8926dfcafa63c4fcb78a94da91a3c6e8b47451",
|
||||
"migrations": [],
|
||||
"data_operations": [],
|
||||
"remote_operations": [],
|
||||
"new_dependencies": [],
|
||||
"permissions_changes": false,
|
||||
"proofs": "outputs/opportunity-search-20261020/receipt.json",
|
||||
"pending": [
|
||||
"Tempo review before main integration",
|
||||
"Natural-language interpretation not implemented; evidence_only remains separate",
|
||||
"Eight contractual gaps and P7 unchanged"
|
||||
],
|
||||
"rollback": "Restore previous source/build using existing release chain; keep database, uploads, permissions and later data.",
|
||||
"hash_basis": "Git index bytes; line endings normalized by repository attributes. Manifest itself excluded."
|
||||
}
|
||||
@@ -1286,3 +1286,758 @@ Interfaces QA: [atribuir interesses](http://127.0.0.1:18555/management/interests
|
||||
| P6 | Conferência do delta de conteúdos/consumidores após P3–P5; reutilizar provas actuais. Vínculo com dossier legítimo quando disponível. | 2–4 h |
|
||||
|
||||
**Saldo de disponibilização do produto existente: 11–20 h**, condicionado ao lote P4 e às pré-condições do destino; não inclui espera por decisão/acesso nem desenvolvimento dos contratos especializados. Os 27–46 h são referência inicial do conjunto, não saldo. Não há base para estimar de forma responsável em bloco o desenvolvimento das oito modalidades: concluir por modalidade exige contrato explícito, âmbito sem acesso indevido, gravação/recuperação e submissão/revisão compatíveis. Recuperação e cessão exigem primeiro identificar a operação concreta; não constituem automaticamente novos módulos. **P7 permanece fechado e separado**, sem bloquear P1–P6. Incrementos 36–39 e instalação remota continuam para decisão expressa. Importação privada e publicação mantêm-se separadas.
|
||||
|
||||
|
||||
## P1/P2 — prova complementar de actualização, 20/10/2026
|
||||
|
||||
Mantém-se a revisão QA `6508c44b9fc1` (56 fontes/177 assets, hashes conferidos). Origem `167b5db97653`; não reconstruir nem repetir provas aceites. Pela interface e login/MFA sintético: interesse actualizado de Recebido para Em análise, nota preservada no histórico após recarregar; pedido do Balcão actualizado de Submetido para Em análise pelo analista atribuído, resposta fundamentada e histórico recuperados. Reutilizadas as provas de atribuição, recuperação noutra sessão e recusa de outros registos. Zero alterações remotas, importações, publicações ou grants reais. Contas sintéticas novamente retiradas da allowlist HTTP.
|
||||
|
||||
Limite concreto: o agendamento por calendário não foi certificado — a entrada automatizada não actualizou o estado React e o validador existente recusou a falta de prazo. Esse erro ainda aparece em inglês. Não inferir falha de gravação do estado/nota, nem declarar o calendário aprovado. A relação entre próxima acção e prazo já existia; não foi criada exigência organizacional.
|
||||
|
||||
P1: 13 das 21 modalidades reconciliadas, oito lacunas de contrato/âmbito individualizadas na tabela anterior; não encerrar todos os contratos especializados. P2: composição QA concluída; sem nova alteração de código. P7 assistência fechada e separada. Saldo P1–P6: 11–20 h para disponibilização do recorte existente (P3 3–5, P4 4–8 condicionado ao lote, P5 2–3, P6 2–4); desenvolvimento especializado sem estimativa fechada e sem bloquear os restantes componentes. O delta de actualização agora concluído não justifica repetir nem voltar a somar horas de P1/P2. Manifesto actualizado apenas nas provas, mesma revisão de fontes.
|
||||
|
||||
|
||||
## P3/P4 preparados e proposta P5/P6 — 20/10/2026
|
||||
|
||||
Revisão QA `3fd1f15bd01c`, sucessora de `6508c44b9fc1`: 56 fontes e 177 assets do build preservado. Duas fontes PHP corrigidas: mensagens do prazo em português e limite funcional no MNE reutilizando OpportunityCanonicalContract::deliveryBlockers. ExistingBusinessIntake não consultava esse limite; agora submissão e publicação de financiamento/concessão/fornecimento não são habilitadas por correspondência de acesso. A preparação permanece guardável. Cinco modalidades verificadas por transacção/rollback: capital_increase, financial_investor, financing_request, concession_opportunity, supply_opportunity. Nenhum módulo experimental foi ligado. As seis categorias base mantêm os bloqueios anteriores. As oito lacunas contratuais e P7 permanecem separados.
|
||||
|
||||
Calendário: mensagem de falta de prazo traduzida; edição por teclado, gravação e recuperação de 21/10/2026 com a próxima acção passaram no navegador QA. Não se alterou o componente de data: o preenchimento automático isolado não actualizava o estado React, enquanto a edição por teclado o actualiza. Prova anterior de actualização e limites reutilizada; sem repetir importações, scans, restauros ou percursos aceites. Sintaxe PHP passou; hashes dos 177 assets preservados, sem recompilação desnecessária por alteração exclusivamente PHP.
|
||||
|
||||
P3, manifesto privado: 30 fichas com identidade e hash actual do QA técnico, secções da fonte e relações; dois originais identificados por SHA256, versão e correspondência por bytes no clone do destino. As fichas continuam privadas; IDs de actores/organizações QA não são transportáveis. Definições project-change e project-follow-up extraídas de config/virtual-desk.php, incluindo campos e hash, comparadas com as versões do clone. Não substituir project-preparation v5 nem reassociar pedidos existentes. Nenhuma importação foi executada. O manifesto de identidades não é ainda um payload de aplicação autenticada.
|
||||
|
||||
P4, lista nominal: 27 entradas de referência (quatro edições de catálogo, 12 sectores e 11 actividades) e 24 referências territoriais com origem/identidade/versão e correspondências no clone. São inventário para reconciliação; **zero referências novas autorizadas para escrita e zero geometrias novas ou substituídas incluídas**. Não transportar catálogos de exemplo ou fonte pendente apenas por existirem localmente. Referências existentes são reutilizáveis por identidade/edição; divergências ficam por item e nunca se resolve por coincidência de ID local. A lista completa está no MANIFESTO-P4-REFERENCIAS-GEOMETRIAS.json. A base é um clone local, não prova fresca do servidor.
|
||||
|
||||
| Ordem P5/P6 | Operação e condição verificável | Reversão / limite |
|
||||
|---|---|---|
|
||||
| 1 — preflight por leitura | Conferir revisão/hash dos ficheiros nominais, schema usado por P1/P2 e P3, flags, operador/MFA, scanner Unix, worker e armazenamento. Comparar cada identidade/edição/hash dos manifestos com o destino actual. Parar apenas a operação divergente e apresentar diferença. | Não executar pacote de migrations. Esta candidata contém zero migrations. Não usar o clone como prova de estado remoto fresco. |
|
||||
| 2 — backup fresco | Suspender escritas HTTP, tarefas agendadas e consumo de filas no recorte durante cópia consistente da base e ficheiros privados; registar inventário, hashes, UID/GID/modos. Incluir configuração e chaves de desencriptação no envelope protegido com recuperação fora do perfil Windows. Retomar só após fecho consistente. | Reutilizar prova aceite de restauro; nesta execução não criar backup nem repetir restauro. Recuperação total é contingência, não reversão normal. |
|
||||
| 3 — código/build | Guardar cópia anterior por caminho/hash; aplicar apenas payload da revisão identificada, incluindo um build; efectuar limpeza/recriação de caches compatível com destino e processo existente. | Repor só ficheiros ainda com hash aplicado. Ficheiro modificado posteriormente gera conflito para reconciliação; nunca sobrescrever. |
|
||||
| 4 — dados privados P3 | Produzir prévia fresca pelo importador existente; reusar originais por hash, confirmar scan real no destino, resolver custódia/permissões por identidade autorizada, criar apenas fichas inexistentes e referências compatíveis. Registar journal de IDs/criação/hash/versão/execução. | Não publicar recursos nem oportunidades; preservar edições posteriores. Sem apagar ficheiros partilhados, histórico ou relações. Corrigir/desactivar só registos deste lote pela recuperação suportada; versões usadas por dossiers permanecem. |
|
||||
| 5 — serviços internos | VirtualDeskInstallationController, fluxo autenticado de instalação: registar apenas project-change/project-follow-up ausentes; comparar hashes se já existirem. Versionar sem substituir versões anteriores. Activação interna do serviço deve constar da decisão de disponibilização; não equivale a publicação de conteúdo. | Conservar versões referidas por pedidos posteriores. Retirar da escolha para novos pedidos apenas por mecanismo suportado; não eliminar versões ou pedidos. Se esse mecanismo não estiver disponível, conservar definição instalada e reverter só apresentação/código. |
|
||||
| 6 — referências P4 | Nesta prévia não há operação nova de escrita de geometrias/catálogos. Revalidar correspondências e reutilizar destino. Nova edição real exige identificação nominal e revisão expressa do manifesto. | Nenhuma geometria existente é substituída. Não converter desconhecido em confirmado. |
|
||||
| 7 — verificações do delta | Entrada autorizada; guardar/recuperar próxima acção e data; limites MNE sem activar contratos; fichas privadas/originais e duas definições recuperáveis; pesquisa/filtros/contexto apenas onde alterados. Vínculo positivo com dossier legítimo quando existir. | Provas aceites reutilizadas; não criar processo fictício no destino. Se faltar dossier legítimo, registar essa única prova pendente, sem bloquear os restantes componentes. |
|
||||
| 8 — fecho | Retomar escritas/worker e entregar estado por componente com URLs, alterações nominais e impedimentos. Conservar manifesto de execução e reversão. | Reversão normal conserva dados posteriores e estruturas/definições aditivas utilizadas, sem down migrations nem dump antigo sobre dados recentes. |
|
||||
|
||||
**Saldo delimitado revisto: 7–13 h** — P3 2–4 h (staging privado/resolução de identidades/aplicação nominal); P4 1–2 h (reconciliação fresca da lista, sem novas geometrias); P5 2–3 h; P6 2–4 h. O calendário, tradução e protecção do limite funcional foram concluídos nesta passagem e estão contabilizados como trabalho realizado, não como horas adicionais futuras. Esta faixa substitui 11–20 h para este recorte delimitado; não cobre concluir todas as modalidades nem um futuro lote geográfico não identificado. Não inclui espera por acesso/decisão. P1/P2 ficam preparados em QA com os limites acima; P3/P4 preparados por leitura, não importados. Instalação, incrementos 36–39 e activação interna das duas definições precisam da decisão expressa identificada. Sem alterações remotas ou publicação nesta execução.
|
||||
|
||||
|
||||
## Pacote executável P3/P4/P5/P6 — c11a0c74766c, 20/10/2026
|
||||
|
||||
Staging privado concluído: 30 fichas actuais sem actores QA, dois originais com hash, duas definições internas. Destino conferido por SSH só leitura em 2026-10-20T08:37:24Z: fontes sem divergência, fichas/originais/duas definições ausentes, project-preparation v5 preservado. 27 referências e 25 territórios reconciliados (inclui Inhambane); zero escrita de catálogo/geometria. Aplicação QA 3fd1f15bd01c; pacote c11a0c74766c, com adaptador de prévia/aplicação autenticada, backup consistente, instalação nominal e reversão de apresentação. Após uso só o manifesto de build anterior é reposto automaticamente; 56 fontes compatíveis, todos os assets, dados, definições usadas e histórico permanecem. O novo adaptador passou sintaxe/integridade/bindings, sem repetir importação; aplicação desse adaptador ainda não ensaiada. Saldo de aplicação/verificação 4–7 h; oito lacunas/P7 separados. Decisão pendente: executar revisão exacta, inclusão expressa 36–39/P3 e activação interna das duas definições. Não publicar fichas/documentos nem conceder novos papéis. Ver proposta única na mesma Page e RECIBO-PACOTE-EXECUTAVEL.json. Sem alteração remota nesta passagem.
|
||||
|
||||
|
||||
## Pacote executável P3/P4/P5/P6 — 82dab3719ffc, 20/10/2026
|
||||
|
||||
Staging privado concluído: 30 fichas actuais sem actores QA, dois originais com hash, duas definições internas. Destino conferido por SSH só leitura em 2026-10-20T08:37:24Z: fontes sem divergência, fichas/originais/duas definições ausentes, project-preparation v5 preservado. 27 referências e 25 territórios reconciliados (inclui Inhambane); zero escrita de catálogo/geometria. Aplicação QA 3fd1f15bd01c; pacote 82dab3719ffc, com adaptador de prévia/aplicação autenticada, backup consistente, instalação nominal e reversão de apresentação. Após uso só o manifesto de build anterior é reposto automaticamente; 56 fontes compatíveis, todos os assets, dados, definições usadas e histórico permanecem. O adaptador foi aplicado apenas em clone isolado: 30 identidades/30 localizações/31 suportes/11 relações, login e MFA sintéticos normais; rollback antes do commit e recuperação após commit comprovados, repetição sem escritas. UUID de auditoria e recibo transaccional corrigidos; aplicação de 56 fontes inalterada. As questões de acesso público e diferenças visuais/login continuam separadas e abertas. Saldo de aplicação/verificação 4–7 h; oito lacunas/P7 separados. Decisão pendente: executar revisão exacta, inclusão expressa 36–39/P3 e activação interna das duas definições. Não publicar fichas/documentos nem conceder novos papéis. Ver proposta única na mesma Page e RECIBO-PACOTE-EXECUTAVEL.json. Sem alteração remota nesta passagem.
|
||||
|
||||
|
||||
## Pacote executável P3/P4/P5/P6 — 312965f2c653, 20/10/2026
|
||||
|
||||
Staging privado concluído: 30 fichas actuais sem actores QA, dois originais com hash, duas definições internas. Destino conferido por SSH só leitura em 2026-10-20T08:37:24Z: fontes sem divergência, fichas/originais/duas definições ausentes, project-preparation v5 preservado. 27 referências e 25 territórios reconciliados (inclui Inhambane); zero escrita de catálogo/geometria. Aplicação QA 3fd1f15bd01c; pacote 312965f2c653, com adaptador de prévia/aplicação autenticada, backup consistente, instalação nominal e reversão de apresentação. Após uso só o manifesto de build anterior é reposto automaticamente; 56 fontes compatíveis, todos os assets, dados, definições usadas e histórico permanecem. O adaptador foi aplicado apenas em clone isolado: 30 identidades/30 localizações/31 suportes/11 relações, login e MFA sintéticos normais; rollback antes do commit e recuperação após commit comprovados, repetição sem escritas. UUID de auditoria e recibo transaccional corrigidos; aplicação de 56 fontes inalterada. As questões de acesso público e diferenças visuais/login continuam separadas e abertas. Saldo de aplicação/verificação 4–7 h; oito lacunas/P7 separados. Decisão pendente: executar revisão exacta, inclusão expressa 36–39/P3 e activação interna das duas definições. Não publicar fichas/documentos nem conceder novos papéis. Ver proposta única na mesma Page e RECIBO-PACOTE-EXECUTAVEL.json. Sem alteração remota nesta passagem.
|
||||
|
||||
|
||||
## Execução autorizada 312965f2c653 em homologação — 2026-10-20T10:01:37Z
|
||||
|
||||
Autorização expressa da Tempo nesta conversa; aplicada exclusivamente em teste-investidor.tempo.co.mz. 56 fontes/177 assets (233 caminhos) conferidos; backup fresco consistente de 171 tabelas, privados, fontes/build e configuração, seis artefactos cifrados e recuperação existente preservada. Não foi repetido restauro aceite.
|
||||
|
||||
30 fichas privadas, 30 localizações, 31 suportes, 11 relações; dois originais internos com scan real limpo (uma tentativa cada), sem publicar. project-change/project-follow-up v1 internos activados; project-preparation v5 preservado. Reutilizadas 27 referências/25 territórios; zero escritas de catálogos/geometrias, migrations ou permissões. 160 tabelas integralmente iguais; registos anteriores nas tabelas acrescentadas conferidos por hash; 361 ficheiros anteriores fora do delta intactos. HTTP/fila activos. Recibo e acessos: outputs/investmoz-revisao-integrada-QA-20261019/p1-p2/modalidades-e-interface/RECIBO-EXECUCAO-312965.json.
|
||||
|
||||
Saldo: execução delimitada P1–P6 concluída; não somar outra vez as 4–7 h já executadas. Mantêm-se vínculo positivo com dossier legítimo, dados em falta/revisão independente, oito lacunas contratuais, capacidades por desenvolver, P7 e SuiteCRM. Reversão disponível só da apresentação: manter 56 fontes compatíveis, ambos os builds, dados, definições usadas e histórico. Não repor dump antigo sobre dados posteriores. As questões de autenticação prematura e diferenças visuais/login continuam separadas, com análise retomada; sem alteração remota adicional.
|
||||
|
||||
|
||||
### Retoma separada de autenticação e apresentação após 312965
|
||||
|
||||
Diagnóstico apenas em clone isolado: com a reserva ligada, GET /documentos devolve 302 para login; desligada só no processo QA, /documentos e /oportunidades devolvem 200 anónimo, enquanto /management/portal-services e /minhas-oportunidades continuam 302. A reserva global tem origem na decisão expressa da Tempo, não constitui uma nova obrigação pública de MFA e não foi desactivada no destino. Questão permanece aberta para distinguir claramente reserva do ambiente e percurso público.
|
||||
|
||||
Manifestos de build local principal e QA instalada são diferentes; app.tsx/layout AuthSimple iguais, login.tsx difere no reaproveitamento do email. O ecrã local já aberto conserva título Laravel e rodapé anterior; não foi recarregada ou alterada a base local activa. Comparação visual por composição/configuração/sessão continua aberta. Nenhuma correcção adicional de código foi incluída silenciosamente no pacote. Prova: RETOMADA-AUTH-VISUAL-QA.json na mesma pasta do recibo; sem novo backlog.
|
||||
|
||||
|
||||
## Consulta pública e coerência visual em QA — b0fdd083ca56, 20/10/2026
|
||||
|
||||
Origem: pedido expresso da Tempo de 20/10/2026. A reserva do ambiente permanece obrigatória; a dependência de uma identidade do portal para qualquer consulta era escolha de implementação. O middleware global confundia ambas. Modo `perimeter` preparado e demonstrado apenas no QA loopback: autorização do ambiente pelo servidor, consulta pública sem conta e autenticação nas operações identificadas. Ausência de autorização e cabeçalho HTTP forjado recusados. O modo `reviewers` continua como valor predefinido compatível até configurar uma reserva independente no destino por decisão expressa. Não basta activar a flag nem aceitar um cabeçalho do cliente.
|
||||
|
||||
Base visual: composição aprovada 3fd1f15bd01c, preservando início, navegação, editorial e rodapé. Builds diferentes e conteúdo/configuração em execução explicam a divergência observada com o local; não se copiou `.env` ou segredos. Login/registo ainda continham textos ingleses e logótipo Laravel. QA utiliza a marca Tempo existente, textos portugueses, formulário sem salto de foco e aviso da reserva. A ligação de entrada na ficha pública passa pelo fluxo existente `/participar/entrar`, conservando URL/contexto.
|
||||
|
||||
Provas: 13 fronteiras de acesso e seis verificações adicionais passaram; TypeScript e build com código 0. No navegador: consulta anónima → rascunho sintético → autenticação → mesma oportunidade, nome, país, sector e mensagem recuperados. Paragem antes do envio final; zero manifestações desta conta. Registo normal por HTTP em QA, sem confirmação artificial de email/MFA e sem email externo. Gestão privada continua protegida; a conta de participação não recebe permissões administrativas. Imagens antes/depois de início, login e registo guardadas no mesmo conjunto de provas. Não repetir importação, recibos completos, scans ou restauros.
|
||||
|
||||
Revisão QA `b0fdd083ca56`: 10 fontes e 177 ficheiros de build no manifesto `MANIFESTO-PUBLICO-VISUAL-QA.json`; pacote sem ambiente, chaves, actores, dados ou harness. QA: http://127.0.0.1:18561/ ; /oportunidades ; /login ; /register. Só acessível nesta máquina. Homologação remota mantém 312965f2c653; não instalada esta correcção e nenhuma abertura externa. Para futura disponibilização será necessário definir e conferir o perímetro independente, antes de activar `perimeter`. Oito lacunas contratuais, vínculo documental positivo ao dossier e P7 separados. Reversão desta preparação: restaurar as dez fontes/build anteriores e manter o modo reservado anterior; nenhum schema ou dado de negócio alterado por este delta.
|
||||
|
||||
|
||||
## Proposta consolidada após reconciliação dos programadores — 423bd512d8c5, 20/10/2026
|
||||
|
||||
Preservada b0fdd083ca56 integralmente por hash, sem a renomear nem substituir. É revisão de conteúdo do pacote, não commit Git. Origin/main foi consultado nesta passagem: 507387502c6ef887433e8e9b5cf20ea39d4f17d4, com dois commits posteriores ao HEAD local a128c5a1. A nova composição mantém a aplicação aprovada 3fd1f15bd01c/312965 e acrescenta 97a08df2 (cabeçalho/menu responsivo, institucional/contacto, rodapé e formulário) e 50738750 (compilação/carregamento diferido dos exportadores). As alterações de 14ad2f60, cb438b2b, becb211f e 9bc450e8 já estavam incorporadas na base e foram conservadas. 6e57d1f6 e a128c5a1 são documentação/reconciliação, não implementação de autenticação.
|
||||
|
||||
Inventário: 760 ficheiros runtime do Git presentes, 677 exactos após normalizar finais de linha e 83 sobreposições QA enumeradas. Presença não comprova funcionamento de todos os componentes. Cinco conflitos de composição resolvidos; preservados menus novos, editorial, email institucional configurável, acesso/contexto e corpo dos exportadores aceites. A refactorização dos programadores só deslocou o corpo de geração; foi conservada com o corpo QA aceite. Ferramentas de arranque local e respectivo comando/composer dev-check não precisam de instalação no servidor. Não aplicar checkout/reset/pull nem substituir uma árvore inteira.
|
||||
|
||||
Identidade: a expressão anterior «marca Tempo» referia o logótipo da proprietária usado na autenticação, não autorização para mudar a identidade do produto. A composição agora identifica explicitamente InvesteMoz, propriedade da Tempo, Lda.; reutiliza o logótipo existente apenas para a proprietária. Não foi inventado outro logótipo de produto nem substituído um exemplar aprovado. Não foi localizado outro logótipo InvesteMoz entre os recursos públicos do repositório.
|
||||
|
||||
Reserva exacta: Nginx HTTPS com auth_basic independente, ficheiro protegido /etc/nginx/private/investmoz-homologation.htpasswd, snippet nominal e marcador FastCGI INVESTMOZ_HOMOLOGATION_ACCESS=granted só após autorização do ambiente. INV...HOMOLOGATION_ONLY=true mantido e ACCESS_MODE=perimeter. Document root /var/www/html/teste-investidor.tempo.co.mz/public, socket /run/php/php8.4-fpm.sock, vhost /etc/nginx/sites-available/teste-investidor.tempo.co.mz.conf. Configuração completa e hashes no pacote. /deploy e assets igualmente reservados; compatibilizar o cliente dos programadores antes da troca. Credenciais independentes por revisor autorizado, transmitidas por via protegida, sem reutilizar palavras-passe do portal ou alterar papéis/MFA.
|
||||
|
||||
Novas provas: oito casos HTTPS/Nginx/PHP-FastCGI passaram (401 sem autorização, 200 para convidado do portal autorizado no ambiente, 302 na operação privada), três passos da reversão da reserva e quatro casos do contacto com transporte simulado. Navegador: abrir/fechar/Escape do menu, navegação para contacto, erro do formulário e identidade em início/login/registo. Build/TypeScript passaram. Reutilizadas as 19 provas b0 e anteriores; sem nova importação, publicação, scan ou restauro. Clone Windows não comprova por si a instalação Linux. Última evidência remota continua a captura aceite de 20/10/2026; tentativa SSH desta passagem não autenticou (verificação da chave do host), sem ignorar essa verificação. Preflight fresco obrigatório antes da execução, com comparação também dos fontes que alimentam o build e detecção de ficheiros adicionais. Qualquer divergência bloqueia a troca e deve ser reconciliada.
|
||||
|
||||
Proposta única 423bd512d8c5: 40 fontes/recursos estáticos e 187 ficheiros de build, configurações e preflight somente leitura; zero migrations/importação/publicação. Reversão deste delta por ficheiro/hash e sequência da reserva, mantendo as 56 fontes compatíveis e dados introduzidos por 312965 e posteriores. Não é reposição integral da aplicação; não repor dump antigo. Oito lacunas contratuais, vínculo positivo ao dossier, geometrias e P7 separados. Estimativa operacional 90–150 minutos com preflight limpo e credenciais prontas, sem desenvolvimento das modalidades pendentes. Sem alterações remotas nesta passagem.
|
||||
|
||||
|
||||
## Preflight SSH e compatibilidade da reserva — 20/10/2026
|
||||
|
||||
Revisão 423bd512d8c5 preservada, ZIP SHA256 43b68aaf44daec2db0251f43a8ae254f038fbc0bb0157839327190f578d5d1af. Esta leitura substitui a limitação SSH do registo anterior: 669 verificações do preflight exacto passaram, incluindo 665 entradas de fontes; 125 ficheiros de dependências/base adicionais coincidem com a base aceite. HEAD remoto 9bc450e8; origin/main permanece 50738750. Build servido e vhost coincidem com as pré-condições do manifesto. Não apareceu conflito novo de código dos programadores.
|
||||
|
||||
Preservar um comando adicional de diagnóstico idêntico à base aceite, 149 auxiliares gerados (11 diferenças de comentários e três auxiliares antigos sem algumas rotas; nenhuma operação nova desconhecida) e 566 recursos adicionais do build. Não apagar nem regenerar no servidor. A aplicação recebe o build QA já aceite, não uma compilação remota.
|
||||
|
||||
Compatibilidade /deploy: ensaio isolado com o webhook efectivo confirmou 401 com HMAC válido mas sem credencial ambiental; 403 com credencial ambiental e HMAC inválido; 202 com ambos válidos, despacho interceptado, zero comandos reais e zero chamadas remotas. Nginx QA reposto. Não repetir as provas funcionais aceites.
|
||||
|
||||
O script remoto já recusa árvore Git alterada antes de fetch/reset/migrations; o estado failed resulta dessa guarda. A primeira leitura por palavras-chave sobre reset era incompleta: nenhuma guarda nova é necessária. Conservar script/serviço/HMAC. Não chamar /deploy para instalar esta revisão: o fluxo genérico inclui migrations automáticas, fora deste manifesto nominal de zero migrations.
|
||||
|
||||
Execução proposta: após decisão, backup fresco consistente; criar credenciais ambientais por via protegida; configurar no webhook Gitea existente Authorization Header = Basic <base64(utilizador:segredo ambiental)> ANTES de ligar a reserva, sem mudar URL/eventos/ramo/Secret HMAC; conferir novamente as pré-condições; aplicar nominalmente 40 fontes/recursos e 187 ficheiros de build; configuração Nginx exacta com /deploy igualmente reservado. Não usar Test Delivery ou push para testar, pois pode disparar o deploy genérico. O cabeçalho é configuração operacional, não credencial de conta do portal, e não entra em chat, URL ou recibo.
|
||||
|
||||
Avaliadores introduzem credencial ambiental e navegam no conteúdo público sem conta InvesteMoz. Operações identificadas continuam a exigir login/políticas/MFA aplicáveis. A futura abertura pública retirará a reserva por decisão própria, sem retirar a protecção privada ou a assinatura/guarda de /deploy. Identidade preservada: InvesteMoz, propriedade da Tempo, Lda.
|
||||
|
||||
Reversão: repor selectivamente o delta por hash e a reserva anterior, e só depois o cabeçalho anterior do cliente, sem alterar o Secret HMAC. Preservar alterações posteriores, 56 fontes compatíveis, dados, relações, definições e assets. Não é reposição integral da aplicação nem restauro de dump. Nenhuma alteração remota efectuada. Execução continua sujeita à decisão da Tempo; estimativa operacional 90–150 min com acessos prontos, excluindo esperas/divergências. Oito lacunas contratuais, vínculo documental legítimo, geometrias e P7 separados.
|
||||
|
||||
Provas: RECIBO-PREFLIGHT-423bd512d8c5.json, PREFLIGHT-EXACTO-423bd512d8c5.json e PROVA-COMPATIBILIDADE-DEPLOY-QA.json na pasta da mesma proposta. O manifesto/ZIP congelados não foram alterados; este recibo actualiza o preflight e a ordem operacional.
|
||||
|
||||
|
||||
## Execução autorizada 423bd512d8c5 — acesso pendente em 20/10/2026
|
||||
|
||||
A Tempo autorizou a execução nominal e a reserva independente. O preflight fresco passou (669 verificações). Staging protegido contém as mesmas 40 fontes/recursos e 187 ficheiros de build, sem alterar o ZIP. Dois hashes APR1 ambientais preparados em staging, sem activar a reserva. Credencial de Nala preparada em ficheiro local privado, restrito ao titular Windows/SYSTEM; nenhum valor em relatórios ou repositório.
|
||||
|
||||
Impedimento: o Gitea recusa a autenticação e o Authorization Header do webhook existente ainda não foi configurado. Não activar a reserva antes desta dependência. Pedido de autenticação/configuração local pendente; não falta nova autorização. Aplicação anterior preservada, sem manutenção, instalação, backup consistente iniciado, migrations, importação ou publicação. Retomar com acesso Gitea, backup fresco consistente e execução nominal já autorizada. Provas de consulta sem conta, conta simples e retoma por realizar após instalação. Disponibilização automática por Git não implementada e separada. Estado: ESTADO-EXECUCAO-AUTORIZADA-423.json na pasta da mesma proposta.
|
||||
|
||||
|
||||
## Disponibilização nominal 423bd512d8c5 — 20/10/2026
|
||||
|
||||
Executada exclusivamente na homologação reservada por autorização expressa da Tempo. Gitea: utilizador confirmou gravação do Authorization Header; presença Basic e Secret HMAC conferida sem expor valores. Não foi chamado /deploy, Test Delivery ou fluxo Git genérico. Disponibilização automática por Git não implementada e separada.
|
||||
|
||||
Backup fresco consistente concluído: manutenção HTTP, fila suspensa, bloqueios SHARE nas 171 tabelas públicas, hashes dos ficheiros estáveis antes/depois. Seis artefactos cifrados e conferidos: base, ficheiros privados/públicos de armazenamento e configuração/chaves da aplicação, fontes/build, configuração de servidor, ACL Linux e metadados. Envelope portátil de recuperação existente preservado, independente do perfil Windows; proprietário/grupo/permissões/ACL/xattrs registados. Não repetido o restauro integral aceite. Cópia anterior do delta e diário de hashes sob custódia root no destino.
|
||||
|
||||
Aplicadas 40 fontes/recursos e 187 ficheiros de build, exactamente pelos hashes do pacote congelado. Manifesto efectivamente servido: 41e9bdbf13dcd95865eefa8af25e2b1e40a5c607d293ad4755152dcea83f9d5d. HEAD Git permanece 9bc450e8 e não identifica sozinho a composição servida. Restantes fontes, comando adicional, 149 auxiliares gerados e todos os assets anteriores não substituídos pelo manifesto preservados (741 ficheiros antigos inventariados, zero perdas/alterações imprevistas). Vhost/snippet correspondem aos bytes do pacote; apenas cinco chaves nominais alteradas; restantes valores .env e proprietário/permissões preservados. Nginx validado antes de reload; PHP-FPM/scanner/base não reiniciados. Manutenção terminada e fila no estado anterior activo. Migrations registadas 88 antes/depois; zero migrations, importação, publicação, alterações de contas/grants/MFA.
|
||||
|
||||
13 verificações HTTPS do delta passaram: sem credencial ambiental, início/documentos/build e /deploy recusados (401); com credencial ambiental e sem conta do portal, início, documentos, oportunidades, sobre e contacto 200; áreas privadas redireccionam para login; manifesto HTTP igual ao instalado. Navegador: utilizador concluiu a entrada ambiental; início e os 29 documentos abrem com Entrar visível, sem conta InvesteMoz; Continuar no meu Balcão redireccionou para login. Retoma após login real e consulta com conta simples aguardam a sessão apropriada; não certificar esses dois passos pela prova anónima. Sem actores sintéticos ou impersonação no destino.
|
||||
|
||||
Credencial ambiental entregue por ficheiro local privado, restrito ao titular Windows/SYSTEM; não consta do repositório, Page ou recibos. Abertura pública continua separada e retirará a reserva por decisão própria, mantendo protecções privadas e /deploy.
|
||||
|
||||
Reversão aprovada: modo reviewers enquanto reserva independente permanece; repor selectivamente apresentação/rotas e configuração deste delta apenas se os hashes ainda coincidirem; preservar alterações posteriores; só depois repor reserva/cabeçalho anteriores. Manter 56 fontes compatíveis, dados/relações/definições e assets posteriores. Não é reposição integral nem restauro de dump. Oito lacunas contratuais, dossier legítimo, geometrias, P7 e automatização Git separados. Instalação concluída; saldo desta execução: 15–30 min de verificação com sessão real adequada, excluindo esperas; nenhum novo desenvolvimento incluído. Provas e recibo: RECIBO-EXECUCAO-423bd512d8c5.json e PROVA-HTTP/CONFIGURACAO/PRESERVACAO-DESTINO-423.json na mesma pasta da proposta.
|
||||
|
||||
|
||||
## Conferência da sessão real 423bd512d8c5 — 20/10/2026
|
||||
|
||||
Depois da entrada ambiental, o navegador apresentou início e biblioteca com 29 documentos sem conta do portal. Continuar no meu Balcão conduziu a /login. O titular concluiu a autenticação real e regressou automaticamente a /balcao-virtual/painel, com o título Balcão Virtual — acompanhamento e conta visível Osmane Nala. A página inicial voltou a abrir nessa sessão autenticada, sem pedir nova credencial. Esta evidência encerra a retoma no delta; não é prova de uma conta simples, pois a conta usada tem funções já autorizadas.
|
||||
|
||||
Não pedir novas contas para encerrar a instalação nem certificar a conta simples sem a observar. Única conferência específica restante deste recorte: consulta com uma conta simples real adequada, 5–10 min quando disponível, excluindo esperas. Não houve criação de conta, impersonação ou alteração de MFA/grants. Credencial ambiental, conta InvesteMoz e conta Gitea são independentes; para o trabalho corrente no portal, usar nala@tempo.co.mz. Não voltar a pedir autenticação nesta passagem.
|
||||
|
||||
Os seis directórios novos da instalação foram alinhados com o proprietário do repositório (mario:www-data, 0755), comprovadamente ausentes no arquivo fresco; permissões dos directórios existentes intactas. Sem alteração do pacote ou revisão. A sessão SSH encerrou pelo limite anterior; não foi reaberta apenas para actualizar o diário operacional remoto. Recibo final e provas actualizados localmente e na mesma Page. Revisão efectivamente servida continua 423bd512d8c5. Git automático, oito lacunas contratuais, dossier legítimo, geometrias e P7 separados.
|
||||
|
||||
|
||||
## Consulta pública sem autenticação ambiental — 20/10/2026
|
||||
|
||||
Decisão expressa da Tempo: retirar a autenticação ambiental das páginas públicas, preservando login, permissões e MFA das operações privadas e a protecção própria de /deploy. Esta decisão substitui a exigência de credencial ambiental para consultar os conteúdos públicos nesta homologação; não publica novos conteúdos nem altera contas ou funções.
|
||||
|
||||
Executado apenas o delta de configuração: removida a reserva Basic global do vhost; Basic mantido explicitamente na localização exacta /deploy com o mesmo htpasswd, e HMAC/Secret/cabeçalho Gitea preservados. Mantidos noindex/nofollow/noarchive e private/no-store no Nginx. INVESTMOZ_HOMOLOGATION_ONLY=false desactiva a reserva global do portal e a indicação desactualizada de ambiente reservado. O modo anterior permanece configurado mas inactivo para eventual reversão; marcador FastCGI de acesso ambiental retirado. Nenhuma alteração de código/build: revisão servida 423bd512d8c5, manifesto SHA256 41e9bdbf13dcd95865eefa8af25e2b1e40a5c607d293ad4755152dcea83f9d5d. Apenas reload Nginx após nginx -t; sem manutenção, migrations, imports, publicação ou reinício de base/fila/scanner.
|
||||
|
||||
Backup fresco apenas da configuração alterada, com hashes revalidados e metadados Linux, cifrado sob a custódia e envelope portátil existentes. Valores secretos não constam dos recibos. Cópias anteriores root-only no destino. Reversão: restaurar primeiro o vhost anterior condicionado aos hashes, validar/recarregar para repor a barreira ambiental; depois repor a flag anterior com conferência de conflitos/cache. Preservar edições posteriores e dados; não restaurar base antiga nem build.
|
||||
|
||||
16 verificações HTTPS passaram com CookieJar novo e sem Authorization na consulta: início, pesquisa /oportunidades?q=agronegocio (termo recuperado), documentos, ficha pública /documentos/13, artigo /agronegocio, sobre e contacto respondem 200, com componentes públicos e utilizador do portal ausente. Seis áreas privadas (/management/portal-services, /management/profile-install, /minhas-oportunidades, /investimentos, /investidor/manifestacoes e /balcao-virtual/painel) redireccionam para o login do portal sem desafio ambiental. /deploy continua 401 sem Basic e 403 com Basic válido mas HMAC inválido em JSON válido; serviço de deploy não disparado. Uma primeira sondagem com corpo vazio produziu 400 (JSON inválido); corrigida somente a sondagem negativa, sem alterar a aplicação ou repetir as restantes provas aceites.
|
||||
|
||||
A ausência do bloqueio é distinta de apenas esconder uma caixa do navegador: os componentes públicos efectivamente renderizam sem formulário de login e a pesquisa conserva o termo. Login num formulário InvesteMoz continua correcto nas operações privadas; se surgir na consulta pública, é falha a registar. Conferência visual em janela InPrivate nova solicitada ao utilizador, ainda pendente neste registo. Não declarar concluída essa parte pela prova HTTP isolada.
|
||||
|
||||
Recibo: RECIBO-CONFIGURACAO-PUBLICA-20261020.json; prova: PROVA-CONSULTA-SEM-CREDENCIAL-20261020.json, na pasta da mesma proposta. Oito lacunas contratuais, conta simples específica, dossier legítimo, geometrias, P7 e disponibilização automática por Git continuam separados. Não houve novas contas, concessões, desactivação de MFA ou publicação adicional.
|
||||
|
||||
|
||||
## Fecho da consulta pública sem credencial — 20/10/2026
|
||||
|
||||
O utilizador confirmou em janela Edge InPrivate nova: início, pesquisa /oportunidades?q=agronegocio e documentos abrem sem qualquer pedido de credenciais («ja funciona nestes 3»). Confirmação visual manual, distinta da prova automática HTTPS. A prova automática foi alargada apenas aos três consumidores adicionais afectados — zonas, sector Turismo e legislação — e passou em 19 verificações totais. Não repetidos scans, restauros, importações ou percursos anteriores.
|
||||
|
||||
Este delta de configuração fica concluído. Consulta pública não requer credencial ambiental nem conta InvesteMoz; seis operações privadas verificadas continuam a pedir login do portal, com permissões/MFA inalterados. /deploy conserva Basic + HMAC, com recusas 401/403 comprovadas e sem disparar o serviço. Prova de limites confirma que apenas /deploy contém Basic, o antigo include global e marcador FastCGI foram removidos, outros valores .env e proprietário/permissões estão intactos, webhook inalterado. Revisão de código/build continua 423bd512d8c5. Documentação anterior que exige credencial para a consulta pública fica superada por esta decisão expressa da Tempo.
|
||||
|
||||
Recibo e prova finais: RECIBO-CONFIGURACAO-PUBLICA-20261020.json e PROVA-CONSULTA-SEM-CREDENCIAL-20261020.json. Sem pendência neste recorte. Conta simples específica, oito lacunas contratuais, dossier legítimo, geometrias, P7 e automatização Git mantêm os saldos separados anteriores. Não se declara a aplicação inteira concluída por este ajuste.
|
||||
|
||||
|
||||
## Git main automático — 20/10/2026 — 8cf820ef
|
||||
|
||||
Disponibilização automática activa em main; revisão servida 8cf820ef497218d9adebe6a6a022b4c80d694dc3. Push real recebido pelo webhook às 17:28:44 e instalado às 17:29:56 (Maputo), sem despacho manual. Preservadas composição 423bd512d8c5 e alterações dos programadores até 02d4e6b2; conflito do menu resolvido mantendo layout novo, identidade InvesteMoz e administração delimitada. Build, TypeScript, 47 testes PHP, 156 frontend e 3106 asserções de schema passaram; nove testes Linux do executor e ensaio do webhook também passaram. Demonstração visual: Consultar oportunidades. 171 tabelas e 88 migrations permanecem; zero migrations no destino, importações, publicação ou novos grants. .env com bytes/proprietário/modo intactos; armazenamento partilhado e 869 assets anteriores preservados. Consulta pública 200 sem credenciais, gestão 302 para login e /deploy 401 sem Basic. Fila serial, recusa de drift e de alterações de migrations, releases separadas, logs/recibos protegidos e reversão de código/build com dados posteriores conservados. Oito lacunas contratuais, dossier legítimo, geometrias, conta simples específica e P7 mantêm saldo separado.
|
||||
|
||||
Procedimento dos programadores: trabalhar numa branch, rever segundo as designações da Tempo e integrar em `main`. O webhook existente dispara a verificação e compilação da revisão completa. Confirmar o commit em https://teste-investidor.tempo.co.mz/build/revision.json. Uma falha conserva a versão servida e deixa recibo/log protegido; não editar a pasta servida para a contornar.
|
||||
|
||||
O executor genérico com migrations automáticas foi substituído. O contrato nominal de 88 fontes de migrations fica protegido fora do Git; uma alteração exige compatibilidade/decisão específica. A CI usa PostGIS oficial fixado por digest, apenas em loopback 55441, base descartável `*_test` e identidade conferida. Não usa contas ou dados da aplicação. A guarda nominal e os testes de lifecycle existentes são preservados; não se contornam as instalações autenticadas.
|
||||
|
||||
Recuperação: parar a path unit, aguardar o executor e usar `python3 /var/www/deploy/investmoz-pipeline.py --config /etc/investmoz-deploy/delivery.json --rollback` como operador existente. Conferem-se hashes e saúde; a release anterior fica servida e o worker recebe reinício gracioso. Mantêm-se base, uploads, configuração e dados posteriores. Não é restauro integral. O checkout físico anterior e a cópia cifrada de configuração/metadados Linux permanecem disponíveis; a recuperação portátil existente foi reutilizada. Não foi repetido o restauro integral aceite.
|
||||
|
||||
As correcções do ensaio limitaram-se à composição: diagnóstico mais recente dos programadores, teste de exemplo sem migração inútil, fixtures unitárias independentes das flags do destino, expectativas dos menus/relatórios já aceites e guarda do cookie fora do navegador, com prova de CSRF preservado. Nenhuma nova regra organizacional.
|
||||
|
||||
Provas: outputs/investmoz-git-delivery-20261020/RECIBO-DISPONIBILIZACAO-GIT-MAIN.json; PROVA-ENSAIO-COMPLETO.json; PROVA-CONTROLO-FINAL-LINUX.json; PROVA-GITEA-EVENTO-E-INSTALACAO.json; PROVA-VISUAL-GIT-MAIN.png; deploy/homologacao/README.md; /build/revision.json. Manual versionado: `deploy/homologacao/README.md`. Esta entrega encerra o fluxo automático, não os restantes componentes da aplicação. As alterações documentais anteriores no checkout local foram preservadas.
|
||||
|
||||
|
||||
## Pesquisa transversal QA — 20/10/2026
|
||||
|
||||
Origem: instruções expressas da Tempo nesta conversa. A classificação multidimensional, dados, permissões, histórico e URLs mantêm-se. Os ajustes de interface/consulta são implementação do pedido, não novas regras organizacionais. Branch `codex/opportunity-search`, base `1350b4df`, QA HTTP `http://127.0.0.1:18570`. Não integrar em `main` antes da revisão: a integração desencadeia disponibilização automática.
|
||||
|
||||
**Causa reproduzida de 11 versus 0:** em `/oportunidades?sector=turismo` no destino, o selector provincial apresentava 11 opções através de `SearchState` como “11 resultados encontrados”, enquanto o paginador devolvia zero oportunidades públicas. São universos diferentes. Agora o primeiro diz “Província: 11 opções disponíveis”; o catálogo usa apenas o total do seu paginador. Não foram alterados dados para obter concordância. Foram ainda retirados o resumo global duplicado desta página, sugestões com critérios incompletos e resultados de parcerias que tinham pesquisa própria.
|
||||
|
||||
| Ecrã/URL | Problema | Correcção | Prova | Pendência/limite |
|
||||
|---|---|---|---|---|
|
||||
| Oportunidades — /oportunidades | Filtros duplicados, códigos internos, maturidades cruzadas, contagens concorrentes e mapa em grelha. | Filtro único, Mais filtros, preparação explícita, moeda obrigatória para montantes, pedidos canceláveis, estados e contexto únicos. | 6 testes PHP/112 asserções; navegador: parque, erro/repetição, moeda, vazio, retorno, hierarquia e teclado. | Revisão visual da Tempo; NLP separado. |
|
||||
| Negócios — /negocios | Classificação técnica visível, estados de auditoria e contagem territorial sem âmbito. | Sector básico, critérios especializados recolhidos em Mais filtros, estado de auditoria retirado do controlo público, contagem identificada e filtros removíveis. | HTTP/Inertia: universo results.meta.total=0; selector partilhado e código inspeccionados. | Nenhum contrato foi habilitado; oito lacunas contratuais mantidas fora deste lote. |
|
||||
| Sectores — /sectores; /sectores/{slug} | Navegação relacionada dispersa e contagens genéricas sem nome do universo. | Contagem identificada como Sectores; navegação comum preserva a entrada por sector e Fintech como especialização. | Inspecção de PublicPortalController, SectorOrganization e portalNavigation; testes de navegação. | Sem pesquisa autónoma no catálogo de sectores: não foi inventado um novo filtro. |
|
||||
| Localizações — /oportunidades?view=map | Mapa e contagem de oportunidades podiam usar universos diferentes sem explicação. | Mesmos filtros e permissões; locais com coordenadas e oportunidades sem coordenadas apresentados separadamente; fichas de contexto identificadas. | Teste de associação/lista/exportação; mapa do serviço exclui proximidade e preserva oportunidade sem coordenadas. | Nenhuma coordenada ou geometria criada. |
|
||||
| Carteira — /carteira | Menu local repetido, ausência de retorno explícito e de distinção entre ficha e pesquisa associada. | Contagem Fichas da carteira, chips removíveis, menu local redundante retirado, Consultar ficha e Ver oportunidades associadas; retorno com query local. | HTTP/Inertia de vazio; navegador de vazio; teste de contexto e navegação. | Provas anteriores de edição/publicação conservadas; não repetidas. |
|
||||
| Zonas e parques — /zonas; /zonas/{id} | Destinos repetidos, vazio atribuído indevidamente a falta de cadastro, retorno sem filtros. | Destinos relacionados recolhidos; vazio neutro; consultar ficha separado de oportunidades associadas; retorno e exportação preservam filtros. | HTTP/Inertia de vazio; inspecção da query/exportação e helpers de retorno. | Disponibilidade/incentivos continuam sujeitos aos dados de cada ficha. |
|
||||
| Clusters — /clusters → /carteira?kind=cluster | Mesmo padrão da carteira; não é um catálogo independente de oportunidades. | Reutiliza carteira, contagem nomeada e tipo de ficha removível; URL anterior preservada. | Redireccionamento verificado no teste PHP; consumidor comum inspeccionado. | Nenhuma equivalência entre implantação e associação criada. |
|
||||
| Corredores — /corredores → /carteira?kind=corridor | Mesmo padrão da carteira e risco de confundir ficha territorial com oportunidade. | Reutiliza carteira e ligações com finalidade explícita; URL anterior preservada. | Redireccionamento verificado no teste PHP; consumidor comum inspeccionado. | Nenhuma alteração de relações ou dados. |
|
||||
| Educação — /educacao | Herda formulário e contagens concorrentes das oportunidades. | Reutiliza integralmente a pesquisa corrigida com âmbito educação mantido. | HTTP/Inertia: results.total=0 com texto inexistente; filtros e componente comuns. | Gestão de procura/capacidade permanece separada e protegida; sem alteração de permissões. |
|
||||
| Biblioteca — /documentos | Dois resumos de contagem, filtros activos sem remoção individual, controlos sem opções e rodapé local repetido. | 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. | Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva. |
|
||||
| Legislação — /legislacao | Mesmo padrão da biblioteca; não deve expor um universo concorrente. | Reutiliza a correcção da biblioteca e preserva filtros jurídicos/documentais pertinentes. | HTTP/Inertia: resources.total=0; consumidor e query pública partilhados. | Situação jurídica, consulta e cálculo continuam independentes. |
|
||||
|
||||
|
||||
### Utilização
|
||||
|
||||
1. Em oportunidades, preencher palavras, sector, modalidade e localização até ao nível conhecido. Usar “Mais filtros” para estruturas, actividades e cadeias. As opções territoriais não são a contagem das oportunidades.
|
||||
2. Para limites financeiros, escolher a moeda. Sem moeda, a pesquisa pede esse campo; não assume MZN nem converte montantes. Preparação tem etapas actuais e classificações históricas separadas.
|
||||
3. Retirar um critério pelo seu botão/ligação ou usar “Limpar filtros”. Alterar o pai territorial limpa os descendentes incompatíveis. Abrir a ficha e usar “Voltar à pesquisa” recupera os critérios.
|
||||
4. “Consultar ficha” consulta a estrutura territorial/documental; “Ver oportunidades associadas” pesquisa vínculos publicados. Proximidade ou o mesmo distrito não demonstram pertença. No mapa, registos sem coordenadas continuam identificados na lista, sem posições inventadas.
|
||||
5. Durante o pedido aparece “A pesquisar…”. Um erro apresenta “Tentar novamente” e preserva critérios; não aparece como resultado vazio. Pedidos anteriores são cancelados/ignorados ao mudar os filtros. Os estados têm `status`/`alert` e `aria-live`.
|
||||
|
||||
### Compatibilidade e provas
|
||||
|
||||
- A pesquisa antiga `maturity_level=1..6` conserva a classificação histórica, sem misturar a posição com a etapa actual. As posições 7/8, exclusivas das oito etapas, correspondem explicitamente a implementação/operacional. Novas selecções usam código de etapa ou classificação histórica explícita, sem reescrever os registos.
|
||||
- Balama real, ID `ed4d2fba-95d5-42e3-8ca7-1f520de1036a`: leitura pública do destino devolveu zero oportunidades associadas. Na cópia QA, essa ficha real está em rascunho; não foi publicada. A prova positiva usa um parque e oportunidades **SINTÉTICO QA**, incluindo associação preliminar, proximidade, território comum, outro parque e rascunho privado. Só a associação pública correspondente entra no filtro.
|
||||
- QA isolado `opportunity_search_20261020_test`, PostgreSQL local porta 55441; guarda impede testes na base activa. Testes transaccionais revertem apenas fixtures próprias; não foi executado `migrate:fresh`.
|
||||
- Seis testes PHP, 112 asserções: associação, confidencialidade, modalidades, maturidades distintas, moeda/combinações/vazio, referências, lista/exportação e HTTP dos consumidores/aliases. Seis testes frontend: navegação, flags, destinos únicos, quatro entradas e retorno local com query. Build e TypeScript conferidos no recibo final.
|
||||
- Navegador: consulta → ficha → retorno; parque isolado; validação de moeda → vazio; falha HTTP sintética de um único pedido → nova tentativa bem-sucedida; Inhambane/Vilankulo → Gaza limpa distrito e postos. A falha existe apenas no router privado do servidor QA, fora do pacote.
|
||||
- Móvel: dimensão real 390 px, largura de conteúdo 375 px, sem transbordo horizontal; teclado abre/fecha Mais filtros e Descoberta. A capacidade automática de viewport não aplicou a dimensão; a prova utilizou o modo responsivo do navegador. Não se atribui essa limitação à aplicação.
|
||||
- Antes/depois e estados: artefactos locais `opportunity-search-artifacts/{before-search.png,before-search-ax.txt,qa-after.png,qa-mobile.png,qa-error.png,qa-zero.png,backend-tests.log}`. As capturas do destino e QA usam acervos diferentes; os totais entre ambientes não são uma comparação de dados.
|
||||
- Inventário de reutilização de `investment-territory.tsx`: oportunidades, negócios, carteira, zonas, educação, análise educativa, formulário/recursos/editor de oportunidades, estruturas, associações territoriais, contexto do investimento, pedido do Balcão, terra/DUAT, formulários públicos e assistência existente. A alteração comum identifica o âmbito da contagem; não demonstra de novo edição/autorização desses consumidores privados. `CountrySelect` também identifica Países; `InvestmentMapPanel` identifica o universo cartográfico.
|
||||
- “Catatálogo” não foi encontrado nas fontes actuais inspeccionadas. O título servido usa “Catálogo”. O menu local redundante da carteira que exibia “InvestMoz” foi retirado; a identidade comum continua InvesteMoz, propriedade da Tempo.
|
||||
|
||||
### Assistência à pesquisa — saldo separado
|
||||
|
||||
`config/investmoz_ai.php`, `AiGateway` e `AssistiveRules` mostram pesquisa lexical/evidências e regras sobre campos estruturados, com `model=null`, sem pesquisa semântica. Não interpretam a frase “turismo em Inhambane até 500 mil USD” como filtros. `evidence_only` não comprova NLP. Nenhum serviço externo, custo ou envio de dados foi activado.
|
||||
|
||||
Incremento ainda necessário: interpretação delimitada de palavras/sector/geografia/montante/moeda; prévia editável dos filtros; pergunta apenas quando ambíguo; confirmação antes de executar exactamente a query manual; ausência de resultados sem alargamento silencioso. Estimativa de engenharia: 6–10 h para um interpretador local delimitado e casos de ambiguidade/testes; compreensão aberta com modelo externo exige proposta e decisão próprias. Esta estimativa não inclui as oito lacunas contratuais nem P7 e não bloqueia as correcções manuais.
|
||||
|
||||
### Integração e reversão
|
||||
|
||||
Rever a branch e o manifesto nominal; reconciliar novos commits dos programadores antes da integração. Após decisão da Tempo, a cadeia Git existente compila a revisão aprovada de fontes e build. Esta execução não envia para `main`, não escreve no servidor e não altera conteúdos, schema, contas, papéis, MFA ou configuração.
|
||||
|
||||
Não há migrations, importação ou publicação neste pacote. A reversão repõe as fontes/build da revisão anterior através da cadeia existente, preservando a base, uploads, referências, permissões e dados posteriores. Os helpers de contexto só transportam query local; não aceitam URL externa como destino de retorno. O manifesto lista fontes e hash do build QA; não inclui `.env`, credenciais, dumps, vendor/node_modules ou fixtures persistentes.
|
||||
|
||||
Conferência final dos consumidores: negócios também conferido no navegador com pesquisa vazia; retirado o resumo local repetido, conservando o resumo partilhado. Fintech é identificado como especialização do Mercado Financeiro também nesse selector. Os cartões de zonas traduzem categoria, desenvolvimento, disponibilidade e estado de actividade, preservando os códigos e significados persistidos. A vista combinada não repete na lista de não localizados a ficha que já aparece na lista principal.
|
||||
|
||||
|
||||
## Guias educativos relacionados — 20/10/2026
|
||||
|
||||
Delta independente de `0e7d51f18fb4`, que já foi integrado e confirmado na homologação. Este incremento permanece na branch `codex/educational-content-cards`, em QA, sujeito a revisão própria. Origem: pedido expresso da Tempo; nenhuma nova regra organizacional.
|
||||
|
||||
O componente `EducationalArticleCard` usa imagem pública autorizada, título original, resumo da edição apresentada, autoria e «Ler o guia». Turismo descreve hotelaria, ecoturismo, infraestruturas e referências regionais presentes no artigo, sem repetir metas financeiras como promessa. Outros artigos usam um excerto do texto efectivo. A apresentação não altera originais, fontes, autoria, direitos, versões, publicação ou scan. A imagem continua sujeita aos filtros públicos existentes. A acção secundária só surge quando a consulta de oportunidades públicas do sector tem resultados; não se infere disponibilidade por menção no artigo.
|
||||
|
||||
Consumidores: sector (`SectorEntry`), exploração editorial e leituras relacionadas (`portal/legacy`), catálogo de oportunidades com sector explícito (incluindo vista territorial/mapa) e ficha de oportunidade com sector. Um filtro de localização sem sector não determina um artigo; não se inventaram associações territoriais. Nos restantes catálogos territoriais não foi encontrado um cartão editorial equivalente a substituir. Não se alteraram cartões de anúncios, documentos ou entidades que não sejam recomendações de artigos.
|
||||
|
||||
Após a leitura, mantém-se «Iniciar ou continuar uma ideia», com `/investimentos/a-partir-de/{slug}` e o contexto autorizado do dossier quando existente. Não há novo percurso, conta ou privilégio. As provas anteriores de gravação documental mantêm-se; não foram repetidas.
|
||||
|
||||
Provas novas: `EducationalGuidesTest`, isolado por guarda QA, transacções e armazenamento fictício do teste, 1 teste / 8 asserções (excerto, autoria, destino, percurso, ausência de resultados, fonte não publicada e scan pendente). TypeScript, sintaxe PHP e build passaram. Capturas a 2048 px e 390 px; a 390 px, conteúdo com 375 px, sem transbordo horizontal, foco visível e Enter no botão «Ler o guia». Imagem real carregada com 900 px de largura natural. A prévia HTTP `http://127.0.0.1:18570/qa/guias/turismo` usa uma cópia por leitura do artigo/imagem já públicos no destino; não usa texto ou imagem sintéticos nem escreve registos. O router privado e a cópia ficam fora do Git. O destino não foi alterado.
|
||||
|
||||
Limitação: o artigo real não existe na base QA actual. A prévia permite conferir cartão e leitura; não comprova recuperação desse artigo num dossier autenticado nessa base. O URL original do percurso foi preservado e conferido pelo teste. Não se importaram dados ou fabricaram resultados para preencher essa lacuna. Capturas em `outputs/educational-guides-20261020/desktop.jpg` e `mobile.jpg` (ficheiros locais de prova).
|
||||
|
||||
Gestão: editar e publicar pelos ecrãs existentes da biblioteca/editorial; o resumo acompanha a edição apresentada. A acção de oportunidades é calculada pelos registos publicados, não pelo editor. Disponibilização futura: revisão própria e integração em `main` pelo processo automático, após decisão. Sem migrations, importação ou publicação neste delta. Reversão pelas fontes/build anteriores, conservando base, ficheiros, permissões e dados posteriores. IA, oito lacunas contratuais e P7 separados.
|
||||
|
||||
|
||||
### 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.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2806,3 +2806,203 @@ Interfaces QA: [atribuir interesses](http://127.0.0.1:18555/management/interests
|
||||
| P6 | Conferência do delta de conteúdos/consumidores após P3–P5; reutilizar provas actuais. Vínculo com dossier legítimo quando disponível. | 2–4 h |
|
||||
|
||||
**Saldo de disponibilização do produto existente: 11–20 h**, condicionado ao lote P4 e às pré-condições do destino; não inclui espera por decisão/acesso nem desenvolvimento dos contratos especializados. Os 27–46 h são referência inicial do conjunto, não saldo. Não há base para estimar de forma responsável em bloco o desenvolvimento das oito modalidades: concluir por modalidade exige contrato explícito, âmbito sem acesso indevido, gravação/recuperação e submissão/revisão compatíveis. Recuperação e cessão exigem primeiro identificar a operação concreta; não constituem automaticamente novos módulos. **P7 permanece fechado e separado**, sem bloquear P1–P6. Incrementos 36–39 e instalação remota continuam para decisão expressa. Importação privada e publicação mantêm-se separadas.
|
||||
|
||||
|
||||
## P1/P2 — prova complementar de actualização, 20/10/2026
|
||||
|
||||
Mantém-se a revisão QA `6508c44b9fc1` (56 fontes/177 assets, hashes conferidos). Origem `167b5db97653`; não reconstruir nem repetir provas aceites. Pela interface e login/MFA sintético: interesse actualizado de Recebido para Em análise, nota preservada no histórico após recarregar; pedido do Balcão actualizado de Submetido para Em análise pelo analista atribuído, resposta fundamentada e histórico recuperados. Reutilizadas as provas de atribuição, recuperação noutra sessão e recusa de outros registos. Zero alterações remotas, importações, publicações ou grants reais. Contas sintéticas novamente retiradas da allowlist HTTP.
|
||||
|
||||
Limite concreto: o agendamento por calendário não foi certificado — a entrada automatizada não actualizou o estado React e o validador existente recusou a falta de prazo. Esse erro ainda aparece em inglês. Não inferir falha de gravação do estado/nota, nem declarar o calendário aprovado. A relação entre próxima acção e prazo já existia; não foi criada exigência organizacional.
|
||||
|
||||
P1: 13 das 21 modalidades reconciliadas, oito lacunas de contrato/âmbito individualizadas na tabela anterior; não encerrar todos os contratos especializados. P2: composição QA concluída; sem nova alteração de código. P7 assistência fechada e separada. Saldo P1–P6: 11–20 h para disponibilização do recorte existente (P3 3–5, P4 4–8 condicionado ao lote, P5 2–3, P6 2–4); desenvolvimento especializado sem estimativa fechada e sem bloquear os restantes componentes. O delta de actualização agora concluído não justifica repetir nem voltar a somar horas de P1/P2. Manifesto actualizado apenas nas provas, mesma revisão de fontes.
|
||||
|
||||
|
||||
## P3/P4 preparados e proposta P5/P6 — 20/10/2026
|
||||
|
||||
Revisão QA `3fd1f15bd01c`, sucessora de `6508c44b9fc1`: 56 fontes e 177 assets do build preservado. Duas fontes PHP corrigidas: mensagens do prazo em português e limite funcional no MNE reutilizando OpportunityCanonicalContract::deliveryBlockers. ExistingBusinessIntake não consultava esse limite; agora submissão e publicação de financiamento/concessão/fornecimento não são habilitadas por correspondência de acesso. A preparação permanece guardável. Cinco modalidades verificadas por transacção/rollback: capital_increase, financial_investor, financing_request, concession_opportunity, supply_opportunity. Nenhum módulo experimental foi ligado. As seis categorias base mantêm os bloqueios anteriores. As oito lacunas contratuais e P7 permanecem separados.
|
||||
|
||||
Calendário: mensagem de falta de prazo traduzida; edição por teclado, gravação e recuperação de 21/10/2026 com a próxima acção passaram no navegador QA. Não se alterou o componente de data: o preenchimento automático isolado não actualizava o estado React, enquanto a edição por teclado o actualiza. Prova anterior de actualização e limites reutilizada; sem repetir importações, scans, restauros ou percursos aceites. Sintaxe PHP passou; hashes dos 177 assets preservados, sem recompilação desnecessária por alteração exclusivamente PHP.
|
||||
|
||||
P3, manifesto privado: 30 fichas com identidade e hash actual do QA técnico, secções da fonte e relações; dois originais identificados por SHA256, versão e correspondência por bytes no clone do destino. As fichas continuam privadas; IDs de actores/organizações QA não são transportáveis. Definições project-change e project-follow-up extraídas de config/virtual-desk.php, incluindo campos e hash, comparadas com as versões do clone. Não substituir project-preparation v5 nem reassociar pedidos existentes. Nenhuma importação foi executada. O manifesto de identidades não é ainda um payload de aplicação autenticada.
|
||||
|
||||
P4, lista nominal: 27 entradas de referência (quatro edições de catálogo, 12 sectores e 11 actividades) e 24 referências territoriais com origem/identidade/versão e correspondências no clone. São inventário para reconciliação; **zero referências novas autorizadas para escrita e zero geometrias novas ou substituídas incluídas**. Não transportar catálogos de exemplo ou fonte pendente apenas por existirem localmente. Referências existentes são reutilizáveis por identidade/edição; divergências ficam por item e nunca se resolve por coincidência de ID local. A lista completa está no MANIFESTO-P4-REFERENCIAS-GEOMETRIAS.json. A base é um clone local, não prova fresca do servidor.
|
||||
|
||||
| Ordem P5/P6 | Operação e condição verificável | Reversão / limite |
|
||||
|---|---|---|
|
||||
| 1 — preflight por leitura | Conferir revisão/hash dos ficheiros nominais, schema usado por P1/P2 e P3, flags, operador/MFA, scanner Unix, worker e armazenamento. Comparar cada identidade/edição/hash dos manifestos com o destino actual. Parar apenas a operação divergente e apresentar diferença. | Não executar pacote de migrations. Esta candidata contém zero migrations. Não usar o clone como prova de estado remoto fresco. |
|
||||
| 2 — backup fresco | Suspender escritas HTTP, tarefas agendadas e consumo de filas no recorte durante cópia consistente da base e ficheiros privados; registar inventário, hashes, UID/GID/modos. Incluir configuração e chaves de desencriptação no envelope protegido com recuperação fora do perfil Windows. Retomar só após fecho consistente. | Reutilizar prova aceite de restauro; nesta execução não criar backup nem repetir restauro. Recuperação total é contingência, não reversão normal. |
|
||||
| 3 — código/build | Guardar cópia anterior por caminho/hash; aplicar apenas payload da revisão identificada, incluindo um build; efectuar limpeza/recriação de caches compatível com destino e processo existente. | Repor só ficheiros ainda com hash aplicado. Ficheiro modificado posteriormente gera conflito para reconciliação; nunca sobrescrever. |
|
||||
| 4 — dados privados P3 | Produzir prévia fresca pelo importador existente; reusar originais por hash, confirmar scan real no destino, resolver custódia/permissões por identidade autorizada, criar apenas fichas inexistentes e referências compatíveis. Registar journal de IDs/criação/hash/versão/execução. | Não publicar recursos nem oportunidades; preservar edições posteriores. Sem apagar ficheiros partilhados, histórico ou relações. Corrigir/desactivar só registos deste lote pela recuperação suportada; versões usadas por dossiers permanecem. |
|
||||
| 5 — serviços internos | VirtualDeskInstallationController, fluxo autenticado de instalação: registar apenas project-change/project-follow-up ausentes; comparar hashes se já existirem. Versionar sem substituir versões anteriores. Activação interna do serviço deve constar da decisão de disponibilização; não equivale a publicação de conteúdo. | Conservar versões referidas por pedidos posteriores. Retirar da escolha para novos pedidos apenas por mecanismo suportado; não eliminar versões ou pedidos. Se esse mecanismo não estiver disponível, conservar definição instalada e reverter só apresentação/código. |
|
||||
| 6 — referências P4 | Nesta prévia não há operação nova de escrita de geometrias/catálogos. Revalidar correspondências e reutilizar destino. Nova edição real exige identificação nominal e revisão expressa do manifesto. | Nenhuma geometria existente é substituída. Não converter desconhecido em confirmado. |
|
||||
| 7 — verificações do delta | Entrada autorizada; guardar/recuperar próxima acção e data; limites MNE sem activar contratos; fichas privadas/originais e duas definições recuperáveis; pesquisa/filtros/contexto apenas onde alterados. Vínculo positivo com dossier legítimo quando existir. | Provas aceites reutilizadas; não criar processo fictício no destino. Se faltar dossier legítimo, registar essa única prova pendente, sem bloquear os restantes componentes. |
|
||||
| 8 — fecho | Retomar escritas/worker e entregar estado por componente com URLs, alterações nominais e impedimentos. Conservar manifesto de execução e reversão. | Reversão normal conserva dados posteriores e estruturas/definições aditivas utilizadas, sem down migrations nem dump antigo sobre dados recentes. |
|
||||
|
||||
**Saldo delimitado revisto: 7–13 h** — P3 2–4 h (staging privado/resolução de identidades/aplicação nominal); P4 1–2 h (reconciliação fresca da lista, sem novas geometrias); P5 2–3 h; P6 2–4 h. O calendário, tradução e protecção do limite funcional foram concluídos nesta passagem e estão contabilizados como trabalho realizado, não como horas adicionais futuras. Esta faixa substitui 11–20 h para este recorte delimitado; não cobre concluir todas as modalidades nem um futuro lote geográfico não identificado. Não inclui espera por acesso/decisão. P1/P2 ficam preparados em QA com os limites acima; P3/P4 preparados por leitura, não importados. Instalação, incrementos 36–39 e activação interna das duas definições precisam da decisão expressa identificada. Sem alterações remotas ou publicação nesta execução.
|
||||
|
||||
|
||||
## Pacote executável P3/P4/P5/P6 — c11a0c74766c, 20/10/2026
|
||||
|
||||
Staging privado concluído: 30 fichas actuais sem actores QA, dois originais com hash, duas definições internas. Destino conferido por SSH só leitura em 2026-10-20T08:37:24Z: fontes sem divergência, fichas/originais/duas definições ausentes, project-preparation v5 preservado. 27 referências e 25 territórios reconciliados (inclui Inhambane); zero escrita de catálogo/geometria. Aplicação QA 3fd1f15bd01c; pacote c11a0c74766c, com adaptador de prévia/aplicação autenticada, backup consistente, instalação nominal e reversão de apresentação. Após uso só o manifesto de build anterior é reposto automaticamente; 56 fontes compatíveis, todos os assets, dados, definições usadas e histórico permanecem. O novo adaptador passou sintaxe/integridade/bindings, sem repetir importação; aplicação desse adaptador ainda não ensaiada. Saldo de aplicação/verificação 4–7 h; oito lacunas/P7 separados. Decisão pendente: executar revisão exacta, inclusão expressa 36–39/P3 e activação interna das duas definições. Não publicar fichas/documentos nem conceder novos papéis. Ver proposta única na mesma Page e RECIBO-PACOTE-EXECUTAVEL.json. Sem alteração remota nesta passagem.
|
||||
|
||||
|
||||
## Pacote executável P3/P4/P5/P6 — 82dab3719ffc, 20/10/2026
|
||||
|
||||
Staging privado concluído: 30 fichas actuais sem actores QA, dois originais com hash, duas definições internas. Destino conferido por SSH só leitura em 2026-10-20T08:37:24Z: fontes sem divergência, fichas/originais/duas definições ausentes, project-preparation v5 preservado. 27 referências e 25 territórios reconciliados (inclui Inhambane); zero escrita de catálogo/geometria. Aplicação QA 3fd1f15bd01c; pacote 82dab3719ffc, com adaptador de prévia/aplicação autenticada, backup consistente, instalação nominal e reversão de apresentação. Após uso só o manifesto de build anterior é reposto automaticamente; 56 fontes compatíveis, todos os assets, dados, definições usadas e histórico permanecem. O adaptador foi aplicado apenas em clone isolado: 30 identidades/30 localizações/31 suportes/11 relações, login e MFA sintéticos normais; rollback antes do commit e recuperação após commit comprovados, repetição sem escritas. UUID de auditoria e recibo transaccional corrigidos; aplicação de 56 fontes inalterada. As questões de acesso público e diferenças visuais/login continuam separadas e abertas. Saldo de aplicação/verificação 4–7 h; oito lacunas/P7 separados. Decisão pendente: executar revisão exacta, inclusão expressa 36–39/P3 e activação interna das duas definições. Não publicar fichas/documentos nem conceder novos papéis. Ver proposta única na mesma Page e RECIBO-PACOTE-EXECUTAVEL.json. Sem alteração remota nesta passagem.
|
||||
|
||||
|
||||
## Pacote executável P3/P4/P5/P6 — 312965f2c653, 20/10/2026
|
||||
|
||||
Staging privado concluído: 30 fichas actuais sem actores QA, dois originais com hash, duas definições internas. Destino conferido por SSH só leitura em 2026-10-20T08:37:24Z: fontes sem divergência, fichas/originais/duas definições ausentes, project-preparation v5 preservado. 27 referências e 25 territórios reconciliados (inclui Inhambane); zero escrita de catálogo/geometria. Aplicação QA 3fd1f15bd01c; pacote 312965f2c653, com adaptador de prévia/aplicação autenticada, backup consistente, instalação nominal e reversão de apresentação. Após uso só o manifesto de build anterior é reposto automaticamente; 56 fontes compatíveis, todos os assets, dados, definições usadas e histórico permanecem. O adaptador foi aplicado apenas em clone isolado: 30 identidades/30 localizações/31 suportes/11 relações, login e MFA sintéticos normais; rollback antes do commit e recuperação após commit comprovados, repetição sem escritas. UUID de auditoria e recibo transaccional corrigidos; aplicação de 56 fontes inalterada. As questões de acesso público e diferenças visuais/login continuam separadas e abertas. Saldo de aplicação/verificação 4–7 h; oito lacunas/P7 separados. Decisão pendente: executar revisão exacta, inclusão expressa 36–39/P3 e activação interna das duas definições. Não publicar fichas/documentos nem conceder novos papéis. Ver proposta única na mesma Page e RECIBO-PACOTE-EXECUTAVEL.json. Sem alteração remota nesta passagem.
|
||||
|
||||
|
||||
## Execução autorizada 312965f2c653 em homologação — 2026-10-20T10:01:37Z
|
||||
|
||||
Autorização expressa da Tempo nesta conversa; aplicada exclusivamente em teste-investidor.tempo.co.mz. 56 fontes/177 assets (233 caminhos) conferidos; backup fresco consistente de 171 tabelas, privados, fontes/build e configuração, seis artefactos cifrados e recuperação existente preservada. Não foi repetido restauro aceite.
|
||||
|
||||
30 fichas privadas, 30 localizações, 31 suportes, 11 relações; dois originais internos com scan real limpo (uma tentativa cada), sem publicar. project-change/project-follow-up v1 internos activados; project-preparation v5 preservado. Reutilizadas 27 referências/25 territórios; zero escritas de catálogos/geometrias, migrations ou permissões. 160 tabelas integralmente iguais; registos anteriores nas tabelas acrescentadas conferidos por hash; 361 ficheiros anteriores fora do delta intactos. HTTP/fila activos. Recibo e acessos: outputs/investmoz-revisao-integrada-QA-20261019/p1-p2/modalidades-e-interface/RECIBO-EXECUCAO-312965.json.
|
||||
|
||||
Saldo: execução delimitada P1–P6 concluída; não somar outra vez as 4–7 h já executadas. Mantêm-se vínculo positivo com dossier legítimo, dados em falta/revisão independente, oito lacunas contratuais, capacidades por desenvolver, P7 e SuiteCRM. Reversão disponível só da apresentação: manter 56 fontes compatíveis, ambos os builds, dados, definições usadas e histórico. Não repor dump antigo sobre dados posteriores. As questões de autenticação prematura e diferenças visuais/login continuam separadas, com análise retomada; sem alteração remota adicional.
|
||||
|
||||
|
||||
### Retoma separada de autenticação e apresentação após 312965
|
||||
|
||||
Diagnóstico apenas em clone isolado: com a reserva ligada, GET /documentos devolve 302 para login; desligada só no processo QA, /documentos e /oportunidades devolvem 200 anónimo, enquanto /management/portal-services e /minhas-oportunidades continuam 302. A reserva global tem origem na decisão expressa da Tempo, não constitui uma nova obrigação pública de MFA e não foi desactivada no destino. Questão permanece aberta para distinguir claramente reserva do ambiente e percurso público.
|
||||
|
||||
Manifestos de build local principal e QA instalada são diferentes; app.tsx/layout AuthSimple iguais, login.tsx difere no reaproveitamento do email. O ecrã local já aberto conserva título Laravel e rodapé anterior; não foi recarregada ou alterada a base local activa. Comparação visual por composição/configuração/sessão continua aberta. Nenhuma correcção adicional de código foi incluída silenciosamente no pacote. Prova: RETOMADA-AUTH-VISUAL-QA.json na mesma pasta do recibo; sem novo backlog.
|
||||
|
||||
|
||||
## Consulta pública e coerência visual em QA — b0fdd083ca56, 20/10/2026
|
||||
|
||||
Origem: pedido expresso da Tempo de 20/10/2026. A reserva do ambiente permanece obrigatória; a dependência de uma identidade do portal para qualquer consulta era escolha de implementação. O middleware global confundia ambas. Modo `perimeter` preparado e demonstrado apenas no QA loopback: autorização do ambiente pelo servidor, consulta pública sem conta e autenticação nas operações identificadas. Ausência de autorização e cabeçalho HTTP forjado recusados. O modo `reviewers` continua como valor predefinido compatível até configurar uma reserva independente no destino por decisão expressa. Não basta activar a flag nem aceitar um cabeçalho do cliente.
|
||||
|
||||
Base visual: composição aprovada 3fd1f15bd01c, preservando início, navegação, editorial e rodapé. Builds diferentes e conteúdo/configuração em execução explicam a divergência observada com o local; não se copiou `.env` ou segredos. Login/registo ainda continham textos ingleses e logótipo Laravel. QA utiliza a marca Tempo existente, textos portugueses, formulário sem salto de foco e aviso da reserva. A ligação de entrada na ficha pública passa pelo fluxo existente `/participar/entrar`, conservando URL/contexto.
|
||||
|
||||
Provas: 13 fronteiras de acesso e seis verificações adicionais passaram; TypeScript e build com código 0. No navegador: consulta anónima → rascunho sintético → autenticação → mesma oportunidade, nome, país, sector e mensagem recuperados. Paragem antes do envio final; zero manifestações desta conta. Registo normal por HTTP em QA, sem confirmação artificial de email/MFA e sem email externo. Gestão privada continua protegida; a conta de participação não recebe permissões administrativas. Imagens antes/depois de início, login e registo guardadas no mesmo conjunto de provas. Não repetir importação, recibos completos, scans ou restauros.
|
||||
|
||||
Revisão QA `b0fdd083ca56`: 10 fontes e 177 ficheiros de build no manifesto `MANIFESTO-PUBLICO-VISUAL-QA.json`; pacote sem ambiente, chaves, actores, dados ou harness. QA: http://127.0.0.1:18561/ ; /oportunidades ; /login ; /register. Só acessível nesta máquina. Homologação remota mantém 312965f2c653; não instalada esta correcção e nenhuma abertura externa. Para futura disponibilização será necessário definir e conferir o perímetro independente, antes de activar `perimeter`. Oito lacunas contratuais, vínculo documental positivo ao dossier e P7 separados. Reversão desta preparação: restaurar as dez fontes/build anteriores e manter o modo reservado anterior; nenhum schema ou dado de negócio alterado por este delta.
|
||||
|
||||
|
||||
## Proposta consolidada após reconciliação dos programadores — 423bd512d8c5, 20/10/2026
|
||||
|
||||
Preservada b0fdd083ca56 integralmente por hash, sem a renomear nem substituir. É revisão de conteúdo do pacote, não commit Git. Origin/main foi consultado nesta passagem: 507387502c6ef887433e8e9b5cf20ea39d4f17d4, com dois commits posteriores ao HEAD local a128c5a1. A nova composição mantém a aplicação aprovada 3fd1f15bd01c/312965 e acrescenta 97a08df2 (cabeçalho/menu responsivo, institucional/contacto, rodapé e formulário) e 50738750 (compilação/carregamento diferido dos exportadores). As alterações de 14ad2f60, cb438b2b, becb211f e 9bc450e8 já estavam incorporadas na base e foram conservadas. 6e57d1f6 e a128c5a1 são documentação/reconciliação, não implementação de autenticação.
|
||||
|
||||
Inventário: 760 ficheiros runtime do Git presentes, 677 exactos após normalizar finais de linha e 83 sobreposições QA enumeradas. Presença não comprova funcionamento de todos os componentes. Cinco conflitos de composição resolvidos; preservados menus novos, editorial, email institucional configurável, acesso/contexto e corpo dos exportadores aceites. A refactorização dos programadores só deslocou o corpo de geração; foi conservada com o corpo QA aceite. Ferramentas de arranque local e respectivo comando/composer dev-check não precisam de instalação no servidor. Não aplicar checkout/reset/pull nem substituir uma árvore inteira.
|
||||
|
||||
Identidade: a expressão anterior «marca Tempo» referia o logótipo da proprietária usado na autenticação, não autorização para mudar a identidade do produto. A composição agora identifica explicitamente InvesteMoz, propriedade da Tempo, Lda.; reutiliza o logótipo existente apenas para a proprietária. Não foi inventado outro logótipo de produto nem substituído um exemplar aprovado. Não foi localizado outro logótipo InvesteMoz entre os recursos públicos do repositório.
|
||||
|
||||
Reserva exacta: Nginx HTTPS com auth_basic independente, ficheiro protegido /etc/nginx/private/investmoz-homologation.htpasswd, snippet nominal e marcador FastCGI INVESTMOZ_HOMOLOGATION_ACCESS=granted só após autorização do ambiente. INV...HOMOLOGATION_ONLY=true mantido e ACCESS_MODE=perimeter. Document root /var/www/html/teste-investidor.tempo.co.mz/public, socket /run/php/php8.4-fpm.sock, vhost /etc/nginx/sites-available/teste-investidor.tempo.co.mz.conf. Configuração completa e hashes no pacote. /deploy e assets igualmente reservados; compatibilizar o cliente dos programadores antes da troca. Credenciais independentes por revisor autorizado, transmitidas por via protegida, sem reutilizar palavras-passe do portal ou alterar papéis/MFA.
|
||||
|
||||
Novas provas: oito casos HTTPS/Nginx/PHP-FastCGI passaram (401 sem autorização, 200 para convidado do portal autorizado no ambiente, 302 na operação privada), três passos da reversão da reserva e quatro casos do contacto com transporte simulado. Navegador: abrir/fechar/Escape do menu, navegação para contacto, erro do formulário e identidade em início/login/registo. Build/TypeScript passaram. Reutilizadas as 19 provas b0 e anteriores; sem nova importação, publicação, scan ou restauro. Clone Windows não comprova por si a instalação Linux. Última evidência remota continua a captura aceite de 20/10/2026; tentativa SSH desta passagem não autenticou (verificação da chave do host), sem ignorar essa verificação. Preflight fresco obrigatório antes da execução, com comparação também dos fontes que alimentam o build e detecção de ficheiros adicionais. Qualquer divergência bloqueia a troca e deve ser reconciliada.
|
||||
|
||||
Proposta única 423bd512d8c5: 40 fontes/recursos estáticos e 187 ficheiros de build, configurações e preflight somente leitura; zero migrations/importação/publicação. Reversão deste delta por ficheiro/hash e sequência da reserva, mantendo as 56 fontes compatíveis e dados introduzidos por 312965 e posteriores. Não é reposição integral da aplicação; não repor dump antigo. Oito lacunas contratuais, vínculo positivo ao dossier, geometrias e P7 separados. Estimativa operacional 90–150 minutos com preflight limpo e credenciais prontas, sem desenvolvimento das modalidades pendentes. Sem alterações remotas nesta passagem.
|
||||
|
||||
|
||||
## Preflight SSH e compatibilidade da reserva — 20/10/2026
|
||||
|
||||
Revisão 423bd512d8c5 preservada, ZIP SHA256 43b68aaf44daec2db0251f43a8ae254f038fbc0bb0157839327190f578d5d1af. Esta leitura substitui a limitação SSH do registo anterior: 669 verificações do preflight exacto passaram, incluindo 665 entradas de fontes; 125 ficheiros de dependências/base adicionais coincidem com a base aceite. HEAD remoto 9bc450e8; origin/main permanece 50738750. Build servido e vhost coincidem com as pré-condições do manifesto. Não apareceu conflito novo de código dos programadores.
|
||||
|
||||
Preservar um comando adicional de diagnóstico idêntico à base aceite, 149 auxiliares gerados (11 diferenças de comentários e três auxiliares antigos sem algumas rotas; nenhuma operação nova desconhecida) e 566 recursos adicionais do build. Não apagar nem regenerar no servidor. A aplicação recebe o build QA já aceite, não uma compilação remota.
|
||||
|
||||
Compatibilidade /deploy: ensaio isolado com o webhook efectivo confirmou 401 com HMAC válido mas sem credencial ambiental; 403 com credencial ambiental e HMAC inválido; 202 com ambos válidos, despacho interceptado, zero comandos reais e zero chamadas remotas. Nginx QA reposto. Não repetir as provas funcionais aceites.
|
||||
|
||||
O script remoto já recusa árvore Git alterada antes de fetch/reset/migrations; o estado failed resulta dessa guarda. A primeira leitura por palavras-chave sobre reset era incompleta: nenhuma guarda nova é necessária. Conservar script/serviço/HMAC. Não chamar /deploy para instalar esta revisão: o fluxo genérico inclui migrations automáticas, fora deste manifesto nominal de zero migrations.
|
||||
|
||||
Execução proposta: após decisão, backup fresco consistente; criar credenciais ambientais por via protegida; configurar no webhook Gitea existente Authorization Header = Basic <base64(utilizador:segredo ambiental)> ANTES de ligar a reserva, sem mudar URL/eventos/ramo/Secret HMAC; conferir novamente as pré-condições; aplicar nominalmente 40 fontes/recursos e 187 ficheiros de build; configuração Nginx exacta com /deploy igualmente reservado. Não usar Test Delivery ou push para testar, pois pode disparar o deploy genérico. O cabeçalho é configuração operacional, não credencial de conta do portal, e não entra em chat, URL ou recibo.
|
||||
|
||||
Avaliadores introduzem credencial ambiental e navegam no conteúdo público sem conta InvesteMoz. Operações identificadas continuam a exigir login/políticas/MFA aplicáveis. A futura abertura pública retirará a reserva por decisão própria, sem retirar a protecção privada ou a assinatura/guarda de /deploy. Identidade preservada: InvesteMoz, propriedade da Tempo, Lda.
|
||||
|
||||
Reversão: repor selectivamente o delta por hash e a reserva anterior, e só depois o cabeçalho anterior do cliente, sem alterar o Secret HMAC. Preservar alterações posteriores, 56 fontes compatíveis, dados, relações, definições e assets. Não é reposição integral da aplicação nem restauro de dump. Nenhuma alteração remota efectuada. Execução continua sujeita à decisão da Tempo; estimativa operacional 90–150 min com acessos prontos, excluindo esperas/divergências. Oito lacunas contratuais, vínculo documental legítimo, geometrias e P7 separados.
|
||||
|
||||
Provas: RECIBO-PREFLIGHT-423bd512d8c5.json, PREFLIGHT-EXACTO-423bd512d8c5.json e PROVA-COMPATIBILIDADE-DEPLOY-QA.json na pasta da mesma proposta. O manifesto/ZIP congelados não foram alterados; este recibo actualiza o preflight e a ordem operacional.
|
||||
|
||||
|
||||
## Execução autorizada 423bd512d8c5 — acesso pendente em 20/10/2026
|
||||
|
||||
A Tempo autorizou a execução nominal e a reserva independente. O preflight fresco passou (669 verificações). Staging protegido contém as mesmas 40 fontes/recursos e 187 ficheiros de build, sem alterar o ZIP. Dois hashes APR1 ambientais preparados em staging, sem activar a reserva. Credencial de Nala preparada em ficheiro local privado, restrito ao titular Windows/SYSTEM; nenhum valor em relatórios ou repositório.
|
||||
|
||||
Impedimento: o Gitea recusa a autenticação e o Authorization Header do webhook existente ainda não foi configurado. Não activar a reserva antes desta dependência. Pedido de autenticação/configuração local pendente; não falta nova autorização. Aplicação anterior preservada, sem manutenção, instalação, backup consistente iniciado, migrations, importação ou publicação. Retomar com acesso Gitea, backup fresco consistente e execução nominal já autorizada. Provas de consulta sem conta, conta simples e retoma por realizar após instalação. Disponibilização automática por Git não implementada e separada. Estado: ESTADO-EXECUCAO-AUTORIZADA-423.json na pasta da mesma proposta.
|
||||
|
||||
|
||||
## Disponibilização nominal 423bd512d8c5 — 20/10/2026
|
||||
|
||||
Executada exclusivamente na homologação reservada por autorização expressa da Tempo. Gitea: utilizador confirmou gravação do Authorization Header; presença Basic e Secret HMAC conferida sem expor valores. Não foi chamado /deploy, Test Delivery ou fluxo Git genérico. Disponibilização automática por Git não implementada e separada.
|
||||
|
||||
Backup fresco consistente concluído: manutenção HTTP, fila suspensa, bloqueios SHARE nas 171 tabelas públicas, hashes dos ficheiros estáveis antes/depois. Seis artefactos cifrados e conferidos: base, ficheiros privados/públicos de armazenamento e configuração/chaves da aplicação, fontes/build, configuração de servidor, ACL Linux e metadados. Envelope portátil de recuperação existente preservado, independente do perfil Windows; proprietário/grupo/permissões/ACL/xattrs registados. Não repetido o restauro integral aceite. Cópia anterior do delta e diário de hashes sob custódia root no destino.
|
||||
|
||||
Aplicadas 40 fontes/recursos e 187 ficheiros de build, exactamente pelos hashes do pacote congelado. Manifesto efectivamente servido: 41e9bdbf13dcd95865eefa8af25e2b1e40a5c607d293ad4755152dcea83f9d5d. HEAD Git permanece 9bc450e8 e não identifica sozinho a composição servida. Restantes fontes, comando adicional, 149 auxiliares gerados e todos os assets anteriores não substituídos pelo manifesto preservados (741 ficheiros antigos inventariados, zero perdas/alterações imprevistas). Vhost/snippet correspondem aos bytes do pacote; apenas cinco chaves nominais alteradas; restantes valores .env e proprietário/permissões preservados. Nginx validado antes de reload; PHP-FPM/scanner/base não reiniciados. Manutenção terminada e fila no estado anterior activo. Migrations registadas 88 antes/depois; zero migrations, importação, publicação, alterações de contas/grants/MFA.
|
||||
|
||||
13 verificações HTTPS do delta passaram: sem credencial ambiental, início/documentos/build e /deploy recusados (401); com credencial ambiental e sem conta do portal, início, documentos, oportunidades, sobre e contacto 200; áreas privadas redireccionam para login; manifesto HTTP igual ao instalado. Navegador: utilizador concluiu a entrada ambiental; início e os 29 documentos abrem com Entrar visível, sem conta InvesteMoz; Continuar no meu Balcão redireccionou para login. Retoma após login real e consulta com conta simples aguardam a sessão apropriada; não certificar esses dois passos pela prova anónima. Sem actores sintéticos ou impersonação no destino.
|
||||
|
||||
Credencial ambiental entregue por ficheiro local privado, restrito ao titular Windows/SYSTEM; não consta do repositório, Page ou recibos. Abertura pública continua separada e retirará a reserva por decisão própria, mantendo protecções privadas e /deploy.
|
||||
|
||||
Reversão aprovada: modo reviewers enquanto reserva independente permanece; repor selectivamente apresentação/rotas e configuração deste delta apenas se os hashes ainda coincidirem; preservar alterações posteriores; só depois repor reserva/cabeçalho anteriores. Manter 56 fontes compatíveis, dados/relações/definições e assets posteriores. Não é reposição integral nem restauro de dump. Oito lacunas contratuais, dossier legítimo, geometrias, P7 e automatização Git separados. Instalação concluída; saldo desta execução: 15–30 min de verificação com sessão real adequada, excluindo esperas; nenhum novo desenvolvimento incluído. Provas e recibo: RECIBO-EXECUCAO-423bd512d8c5.json e PROVA-HTTP/CONFIGURACAO/PRESERVACAO-DESTINO-423.json na mesma pasta da proposta.
|
||||
|
||||
|
||||
## Conferência da sessão real 423bd512d8c5 — 20/10/2026
|
||||
|
||||
Depois da entrada ambiental, o navegador apresentou início e biblioteca com 29 documentos sem conta do portal. Continuar no meu Balcão conduziu a /login. O titular concluiu a autenticação real e regressou automaticamente a /balcao-virtual/painel, com o título Balcão Virtual — acompanhamento e conta visível Osmane Nala. A página inicial voltou a abrir nessa sessão autenticada, sem pedir nova credencial. Esta evidência encerra a retoma no delta; não é prova de uma conta simples, pois a conta usada tem funções já autorizadas.
|
||||
|
||||
Não pedir novas contas para encerrar a instalação nem certificar a conta simples sem a observar. Única conferência específica restante deste recorte: consulta com uma conta simples real adequada, 5–10 min quando disponível, excluindo esperas. Não houve criação de conta, impersonação ou alteração de MFA/grants. Credencial ambiental, conta InvesteMoz e conta Gitea são independentes; para o trabalho corrente no portal, usar nala@tempo.co.mz. Não voltar a pedir autenticação nesta passagem.
|
||||
|
||||
Os seis directórios novos da instalação foram alinhados com o proprietário do repositório (mario:www-data, 0755), comprovadamente ausentes no arquivo fresco; permissões dos directórios existentes intactas. Sem alteração do pacote ou revisão. A sessão SSH encerrou pelo limite anterior; não foi reaberta apenas para actualizar o diário operacional remoto. Recibo final e provas actualizados localmente e na mesma Page. Revisão efectivamente servida continua 423bd512d8c5. Git automático, oito lacunas contratuais, dossier legítimo, geometrias e P7 separados.
|
||||
|
||||
|
||||
## Consulta pública sem autenticação ambiental — 20/10/2026
|
||||
|
||||
Decisão expressa da Tempo: retirar a autenticação ambiental das páginas públicas, preservando login, permissões e MFA das operações privadas e a protecção própria de /deploy. Esta decisão substitui a exigência de credencial ambiental para consultar os conteúdos públicos nesta homologação; não publica novos conteúdos nem altera contas ou funções.
|
||||
|
||||
Executado apenas o delta de configuração: removida a reserva Basic global do vhost; Basic mantido explicitamente na localização exacta /deploy com o mesmo htpasswd, e HMAC/Secret/cabeçalho Gitea preservados. Mantidos noindex/nofollow/noarchive e private/no-store no Nginx. INVESTMOZ_HOMOLOGATION_ONLY=false desactiva a reserva global do portal e a indicação desactualizada de ambiente reservado. O modo anterior permanece configurado mas inactivo para eventual reversão; marcador FastCGI de acesso ambiental retirado. Nenhuma alteração de código/build: revisão servida 423bd512d8c5, manifesto SHA256 41e9bdbf13dcd95865eefa8af25e2b1e40a5c607d293ad4755152dcea83f9d5d. Apenas reload Nginx após nginx -t; sem manutenção, migrations, imports, publicação ou reinício de base/fila/scanner.
|
||||
|
||||
Backup fresco apenas da configuração alterada, com hashes revalidados e metadados Linux, cifrado sob a custódia e envelope portátil existentes. Valores secretos não constam dos recibos. Cópias anteriores root-only no destino. Reversão: restaurar primeiro o vhost anterior condicionado aos hashes, validar/recarregar para repor a barreira ambiental; depois repor a flag anterior com conferência de conflitos/cache. Preservar edições posteriores e dados; não restaurar base antiga nem build.
|
||||
|
||||
16 verificações HTTPS passaram com CookieJar novo e sem Authorization na consulta: início, pesquisa /oportunidades?q=agronegocio (termo recuperado), documentos, ficha pública /documentos/13, artigo /agronegocio, sobre e contacto respondem 200, com componentes públicos e utilizador do portal ausente. Seis áreas privadas (/management/portal-services, /management/profile-install, /minhas-oportunidades, /investimentos, /investidor/manifestacoes e /balcao-virtual/painel) redireccionam para o login do portal sem desafio ambiental. /deploy continua 401 sem Basic e 403 com Basic válido mas HMAC inválido em JSON válido; serviço de deploy não disparado. Uma primeira sondagem com corpo vazio produziu 400 (JSON inválido); corrigida somente a sondagem negativa, sem alterar a aplicação ou repetir as restantes provas aceites.
|
||||
|
||||
A ausência do bloqueio é distinta de apenas esconder uma caixa do navegador: os componentes públicos efectivamente renderizam sem formulário de login e a pesquisa conserva o termo. Login num formulário InvesteMoz continua correcto nas operações privadas; se surgir na consulta pública, é falha a registar. Conferência visual em janela InPrivate nova solicitada ao utilizador, ainda pendente neste registo. Não declarar concluída essa parte pela prova HTTP isolada.
|
||||
|
||||
Recibo: RECIBO-CONFIGURACAO-PUBLICA-20261020.json; prova: PROVA-CONSULTA-SEM-CREDENCIAL-20261020.json, na pasta da mesma proposta. Oito lacunas contratuais, conta simples específica, dossier legítimo, geometrias, P7 e disponibilização automática por Git continuam separados. Não houve novas contas, concessões, desactivação de MFA ou publicação adicional.
|
||||
|
||||
|
||||
## Fecho da consulta pública sem credencial — 20/10/2026
|
||||
|
||||
O utilizador confirmou em janela Edge InPrivate nova: início, pesquisa /oportunidades?q=agronegocio e documentos abrem sem qualquer pedido de credenciais («ja funciona nestes 3»). Confirmação visual manual, distinta da prova automática HTTPS. A prova automática foi alargada apenas aos três consumidores adicionais afectados — zonas, sector Turismo e legislação — e passou em 19 verificações totais. Não repetidos scans, restauros, importações ou percursos anteriores.
|
||||
|
||||
Este delta de configuração fica concluído. Consulta pública não requer credencial ambiental nem conta InvesteMoz; seis operações privadas verificadas continuam a pedir login do portal, com permissões/MFA inalterados. /deploy conserva Basic + HMAC, com recusas 401/403 comprovadas e sem disparar o serviço. Prova de limites confirma que apenas /deploy contém Basic, o antigo include global e marcador FastCGI foram removidos, outros valores .env e proprietário/permissões estão intactos, webhook inalterado. Revisão de código/build continua 423bd512d8c5. Documentação anterior que exige credencial para a consulta pública fica superada por esta decisão expressa da Tempo.
|
||||
|
||||
Recibo e prova finais: RECIBO-CONFIGURACAO-PUBLICA-20261020.json e PROVA-CONSULTA-SEM-CREDENCIAL-20261020.json. Sem pendência neste recorte. Conta simples específica, oito lacunas contratuais, dossier legítimo, geometrias, P7 e automatização Git mantêm os saldos separados anteriores. Não se declara a aplicação inteira concluída por este ajuste.
|
||||
|
||||
|
||||
## Git main automático — 20/10/2026 — 8cf820ef
|
||||
|
||||
Disponibilização automática activa em main; revisão servida 8cf820ef497218d9adebe6a6a022b4c80d694dc3. Push real recebido pelo webhook às 17:28:44 e instalado às 17:29:56 (Maputo), sem despacho manual. Preservadas composição 423bd512d8c5 e alterações dos programadores até 02d4e6b2; conflito do menu resolvido mantendo layout novo, identidade InvesteMoz e administração delimitada. Build, TypeScript, 47 testes PHP, 156 frontend e 3106 asserções de schema passaram; nove testes Linux do executor e ensaio do webhook também passaram. Demonstração visual: Consultar oportunidades. 171 tabelas e 88 migrations permanecem; zero migrations no destino, importações, publicação ou novos grants. .env com bytes/proprietário/modo intactos; armazenamento partilhado e 869 assets anteriores preservados. Consulta pública 200 sem credenciais, gestão 302 para login e /deploy 401 sem Basic. Fila serial, recusa de drift e de alterações de migrations, releases separadas, logs/recibos protegidos e reversão de código/build com dados posteriores conservados. Oito lacunas contratuais, dossier legítimo, geometrias, conta simples específica e P7 mantêm saldo separado.
|
||||
|
||||
Procedimento dos programadores: trabalhar numa branch, rever segundo as designações da Tempo e integrar em `main`. O webhook existente dispara a verificação e compilação da revisão completa. Confirmar o commit em https://teste-investidor.tempo.co.mz/build/revision.json. Uma falha conserva a versão servida e deixa recibo/log protegido; não editar a pasta servida para a contornar.
|
||||
|
||||
O executor genérico com migrations automáticas foi substituído. O contrato nominal de 88 fontes de migrations fica protegido fora do Git; uma alteração exige compatibilidade/decisão específica. A CI usa PostGIS oficial fixado por digest, apenas em loopback 55441, base descartável `*_test` e identidade conferida. Não usa contas ou dados da aplicação. A guarda nominal e os testes de lifecycle existentes são preservados; não se contornam as instalações autenticadas.
|
||||
|
||||
Recuperação: parar a path unit, aguardar o executor e usar `python3 /var/www/deploy/investmoz-pipeline.py --config /etc/investmoz-deploy/delivery.json --rollback` como operador existente. Conferem-se hashes e saúde; a release anterior fica servida e o worker recebe reinício gracioso. Mantêm-se base, uploads, configuração e dados posteriores. Não é restauro integral. O checkout físico anterior e a cópia cifrada de configuração/metadados Linux permanecem disponíveis; a recuperação portátil existente foi reutilizada. Não foi repetido o restauro integral aceite.
|
||||
|
||||
As correcções do ensaio limitaram-se à composição: diagnóstico mais recente dos programadores, teste de exemplo sem migração inútil, fixtures unitárias independentes das flags do destino, expectativas dos menus/relatórios já aceites e guarda do cookie fora do navegador, com prova de CSRF preservado. Nenhuma nova regra organizacional.
|
||||
|
||||
Provas: outputs/investmoz-git-delivery-20261020/RECIBO-DISPONIBILIZACAO-GIT-MAIN.json; PROVA-ENSAIO-COMPLETO.json; PROVA-CONTROLO-FINAL-LINUX.json; PROVA-GITEA-EVENTO-E-INSTALACAO.json; PROVA-VISUAL-GIT-MAIN.png; deploy/homologacao/README.md; /build/revision.json. Manual versionado: `deploy/homologacao/README.md`. Esta entrega encerra o fluxo automático, não os restantes componentes da aplicação. As alterações documentais anteriores no checkout local foram preservadas.
|
||||
|
||||
|
||||
## Pesquisa transversal QA — 20/10/2026
|
||||
|
||||
Inventário finito AUD-SEARCH-T01–T11 na mesma matriz/CSV e comparação no manual. Correcções manuais na branch `codex/opportunity-search`, sem integração/remoto/publicação. Assistência NLP ainda não implementada: interpretador e prévia/confirmar da mesma query manual, 6–10 h para recorte local delimitado; modelo externo sujeito a decisão. As oito lacunas contratuais e P7 mantêm-se separados. Revisão Tempo pendente; as provas comuns não encerram requisitos de gestão ou publicação não alterados.
|
||||
|
||||
|
||||
## Guias educativos relacionados — delta QA após 0e7d51f18fb4
|
||||
|
||||
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
@@ -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."
|
||||
}
|
||||
@@ -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.',
|
||||
];
|
||||
@@ -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'],
|
||||
];
|
||||
+5
-409
@@ -7,413 +7,8 @@
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
/* Institutional shell adapted from Tempo; page content keeps its own theme. */
|
||||
.tempo-header {
|
||||
position: relative;
|
||||
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;
|
||||
}
|
||||
.tempo-logo-link {
|
||||
grid-area: 1 / 1;
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
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;
|
||||
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;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
color: #eee;
|
||||
transition:
|
||||
color 0.2s,
|
||||
background 0.2s;
|
||||
}
|
||||
.tempo-nav-trigger:hover,
|
||||
.tempo-nav-trigger[data-state='open'] {
|
||||
background: #ffffff0d;
|
||||
color: #fff;
|
||||
}
|
||||
.tempo-nav-trigger[data-current-group] {
|
||||
color: #fff;
|
||||
box-shadow: inset 0 -2px #ed1c24;
|
||||
}
|
||||
.tempo-nav-trigger[data-state='open'] > svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
.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 (min-width: 768px) {
|
||||
.tempo-section-inner {
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
padding: 0.25rem 1.25rem;
|
||||
}
|
||||
.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;
|
||||
min-height: 40px;
|
||||
align-items: center;
|
||||
gap: 0.4rem;
|
||||
padding: 0.4rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 0.8125rem;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
.tempo-utility-button:hover {
|
||||
background: #ffffff10;
|
||||
}
|
||||
.tempo-header-social {
|
||||
display: flex;
|
||||
width: 30px;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--social-color);
|
||||
transition:
|
||||
transform 0.2s,
|
||||
opacity 0.2s;
|
||||
border-radius: 0.4rem;
|
||||
}
|
||||
.tempo-header-social:hover {
|
||||
transform: translateY(-2px);
|
||||
opacity: 0.8;
|
||||
}
|
||||
.tempo-login-button {
|
||||
display: inline-flex;
|
||||
min-height: 40px;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
border: 1px solid #ffffff50;
|
||||
border-radius: 0.4rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
color: #fff;
|
||||
transition:
|
||||
background 0.2s,
|
||||
border-color 0.2s;
|
||||
}
|
||||
.tempo-login-button:hover {
|
||||
background: #ed1c24;
|
||||
border-color: #ed1c24;
|
||||
}
|
||||
.tempo-login-button > span {
|
||||
max-width: clamp(2rem, calc(100vw - 18rem), 6rem);
|
||||
}
|
||||
.tempo-header-actions {
|
||||
grid-area: 1 / 2;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
}
|
||||
.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;
|
||||
border: 1px solid #ed1c24;
|
||||
border-radius: 0.4rem;
|
||||
background: #ed1c24;
|
||||
color: white;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.tempo-interest-button:hover {
|
||||
background: #c9151c;
|
||||
}
|
||||
.tempo-panel-summary {
|
||||
grid-area: 2 / 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
gap: 0.1rem;
|
||||
min-height: 44px;
|
||||
font-size: 0.8125rem;
|
||||
line-height: 1.4;
|
||||
color: #fff;
|
||||
text-underline-offset: 3px;
|
||||
}
|
||||
.tempo-panel-summary:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.tempo-panel-count {
|
||||
color: #cbd5e1;
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
.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 (min-width: 640px) {
|
||||
.tempo-header-inner {
|
||||
padding-inline: 1.25rem;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
}
|
||||
@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;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.tempo-header-actions {
|
||||
grid-area: 1 / 7;
|
||||
}
|
||||
.tempo-section-bar {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
.tempo-section-inner {
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
gap: normal;
|
||||
max-width: none;
|
||||
margin: 0;
|
||||
padding-inline: 0;
|
||||
}
|
||||
.tempo-section-navigation {
|
||||
display: grid;
|
||||
grid-area: 1 / 2 / 2 / 5;
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
.tempo-section-navigation > ul {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: subgrid;
|
||||
gap: normal;
|
||||
}
|
||||
.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 :is(a, button):focus-visible {
|
||||
outline: 2px solid #fff;
|
||||
outline-offset: 4px;
|
||||
}
|
||||
@import './tempo-header.css';
|
||||
|
||||
.tempo-dropdown {
|
||||
z-index: 60;
|
||||
border: 1px solid #e2e8f0;
|
||||
@@ -441,7 +36,7 @@
|
||||
outline: none;
|
||||
}
|
||||
.tempo-footer {
|
||||
margin-top: 2rem;
|
||||
margin-top: 1.5rem;
|
||||
background: #050505;
|
||||
color: #fff;
|
||||
}
|
||||
@@ -475,7 +70,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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Link, usePage } from '@inertiajs/react';
|
||||
|
||||
const labels: Record<string, string> = {
|
||||
q: 'Pesquisa',
|
||||
query: 'Pesquisa',
|
||||
sector: 'Sector',
|
||||
kind: 'Tipo de ficha',
|
||||
modality: 'Modalidade',
|
||||
category: 'Categoria',
|
||||
physical_category: 'Tipo de espaço',
|
||||
legal_regime: 'Regime',
|
||||
development_status: 'Desenvolvimento',
|
||||
availability_status: 'Disponibilidade',
|
||||
province_id: 'Província',
|
||||
district_id: 'Distrito',
|
||||
admin_post_id: 'Posto administrativo',
|
||||
locality_id: 'Localidade',
|
||||
geo_location_id: 'Localização',
|
||||
territory_id: 'Território',
|
||||
province: 'Província',
|
||||
activity: 'Actividade',
|
||||
document_type: 'Tipo de documento',
|
||||
topic: 'Tema',
|
||||
issuer: 'Entidade emissora',
|
||||
language: 'Idioma',
|
||||
legal_status: 'Situação documental',
|
||||
source_confirmation: 'Fonte',
|
||||
stage: 'Etapa',
|
||||
module: 'Área',
|
||||
association_type: 'Associação',
|
||||
association_target: 'Referência',
|
||||
association_purpose: 'Relação',
|
||||
association_state: 'Confirmação',
|
||||
chain_id: 'Cadeia de valor',
|
||||
subsector_id: 'Actividade',
|
||||
authority_id: 'Entidade',
|
||||
};
|
||||
const territoryKeys = [
|
||||
'province_id',
|
||||
'district_id',
|
||||
'admin_post_id',
|
||||
'locality_id',
|
||||
];
|
||||
const values: Record<string, string> = {
|
||||
corridor: 'Corredores',
|
||||
cluster: 'Clusters',
|
||||
project: 'Projectos âncora',
|
||||
zee: 'ZEE',
|
||||
zfi: 'ZFI',
|
||||
industrial_park: 'Parque industrial',
|
||||
investment_project: 'Projecto de investimento',
|
||||
company_sale: 'Venda de empresa',
|
||||
company_purchase: 'Compra de empresa',
|
||||
real_estate: 'Imobiliário',
|
||||
land_right: 'Direitos fundiários',
|
||||
turismo: 'Turismo',
|
||||
agronegocio: 'Agronegócio',
|
||||
fintech: 'Fintech — especialização do Mercado Financeiro',
|
||||
'mercado-financeiro': 'Mercado Financeiro',
|
||||
legislation: 'Legislação',
|
||||
procedure: 'Procedimento oficial',
|
||||
official_form: 'Formulário oficial',
|
||||
guide: 'Guia',
|
||||
study: 'Estudo',
|
||||
unknown: 'Por confirmar',
|
||||
pt: 'Português',
|
||||
en: 'Inglês',
|
||||
confirmed: 'Confirmada',
|
||||
pending: 'Por confirmar',
|
||||
};
|
||||
/** Name each active criterion without exposing IDs or taxonomy codes. */
|
||||
export default function CatalogueFilterContext() {
|
||||
const page = usePage();
|
||||
const filters = page.props.filters as Record<string, unknown> | undefined;
|
||||
if (!filters) return null;
|
||||
const active = Object.entries(filters).filter(
|
||||
([key, value]) =>
|
||||
!['page', 'per_page', 'sort', 'view', 'publication_state'].includes(
|
||||
key,
|
||||
) &&
|
||||
value !== '' &&
|
||||
value != null &&
|
||||
value !== false &&
|
||||
(!Array.isArray(value) || value.length),
|
||||
);
|
||||
if (!active.length) return null;
|
||||
const describe = (key: string, value: unknown): string => {
|
||||
const path = page.props.territorySelection as
|
||||
| { id: string; name: string }[]
|
||||
| undefined;
|
||||
const place = path?.find((item) => item.id === value)?.name;
|
||||
if (place) return place;
|
||||
if (typeof value !== 'string') return 'seleccionado';
|
||||
return (
|
||||
values[value] ??
|
||||
(['q', 'query', 'province', 'topic', 'issuer'].includes(key)
|
||||
? value
|
||||
: 'seleccionado')
|
||||
);
|
||||
};
|
||||
const href = (remove?: string) => {
|
||||
const url = new URL(page.url, 'https://portal.invalid');
|
||||
const keys = remove
|
||||
? [
|
||||
remove,
|
||||
...territoryKeys
|
||||
.slice(territoryKeys.indexOf(remove) + 1)
|
||||
.filter(() => territoryKeys.includes(remove)),
|
||||
]
|
||||
: Object.keys(filters);
|
||||
for (const key of keys) {
|
||||
url.searchParams.delete(key);
|
||||
url.searchParams.delete(`${key}[]`);
|
||||
}
|
||||
url.searchParams.delete('page');
|
||||
return url.pathname + url.search;
|
||||
};
|
||||
return (
|
||||
<nav
|
||||
aria-label="Filtros activos"
|
||||
className="my-3 flex flex-wrap items-center gap-3"
|
||||
>
|
||||
<span className="font-semibold">Filtros activos:</span>
|
||||
{active.map(([key, value]) => (
|
||||
<Link
|
||||
key={key}
|
||||
href={href(key)}
|
||||
className="rounded border px-3 py-2 underline"
|
||||
aria-label={`Remover ${labels[key] ?? 'critério'}`}
|
||||
>
|
||||
{labels[key] ?? 'Critério seleccionado'}:{' '}
|
||||
{describe(key, value)} ×
|
||||
</Link>
|
||||
))}
|
||||
<Link href={href()} className="underline">
|
||||
Limpar filtros
|
||||
</Link>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -73,34 +73,32 @@ export default function CatalogueNavigation({
|
||||
],
|
||||
};
|
||||
return (
|
||||
<nav
|
||||
className="my-4 grid gap-3 rounded-xl border bg-white p-4 sm:grid-cols-2 lg:grid-cols-4"
|
||||
aria-label="Explorar dentro do catálogo"
|
||||
>
|
||||
{Object.entries(sections).map(([title, items]) => (
|
||||
<section key={title}>
|
||||
<h2 className="mb-2 font-semibold">{title}</h2>
|
||||
<ul className="space-y-2">
|
||||
{items.map(([label, href]) => (
|
||||
<li key={label}>
|
||||
<Link
|
||||
className="underline"
|
||||
href={withContext(href)}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
<p className="text-sm sm:col-span-2 lg:col-span-4">
|
||||
Filtros e vistas reutilizam os catálogos existentes.
|
||||
Comprar/vender continua no consumidor /negocios; não duplica
|
||||
anúncios nem unifica contagens. Selecionar um parque ou projeto
|
||||
específico usa os filtros existentes; a ficha relacionada não é
|
||||
uma oportunidade.
|
||||
</p>
|
||||
</nav>
|
||||
<details className="my-4 rounded-xl border bg-white p-4">
|
||||
<summary className="cursor-pointer font-semibold">
|
||||
Catálogos relacionados
|
||||
</summary>
|
||||
<nav
|
||||
className="my-4 grid gap-3 rounded-xl border bg-white p-4 sm:grid-cols-2 lg:grid-cols-4"
|
||||
aria-label="Explorar dentro do catálogo"
|
||||
>
|
||||
{Object.entries(sections).map(([title, items]) => (
|
||||
<section key={title}>
|
||||
<h2 className="mb-2 font-semibold">{title}</h2>
|
||||
<ul className="space-y-2">
|
||||
{items.map(([label, href]) => (
|
||||
<li key={label}>
|
||||
<Link
|
||||
className="underline"
|
||||
href={withContext(href)}
|
||||
>
|
||||
{label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
))}
|
||||
</nav>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,7 +50,14 @@ export default function CountrySelect({
|
||||
onChange={(event) => setSearch(event.target.value)}
|
||||
placeholder="Pesquisar nome ou código do país"
|
||||
/>
|
||||
{search.trim() && <p role="status" aria-live="polite">{matches.length ? `${matches.length} resultados encontrados` : "Não foram encontrados resultados para esta pesquisa"}</p>}
|
||||
{search.trim() && (
|
||||
<p role="status" aria-live="polite">
|
||||
Países:{' '}
|
||||
{matches.length
|
||||
? `${matches.length} opções disponíveis`
|
||||
: 'Nenhuma opção encontrada'}
|
||||
</p>
|
||||
)}
|
||||
<select
|
||||
disabled={disabled}
|
||||
id={id ?? generatedId}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,73 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
|
||||
export type EducationalArticle = {
|
||||
title: string;
|
||||
href: string;
|
||||
summary: string;
|
||||
author?: string;
|
||||
image: { href: string; alt: string } | null;
|
||||
opportunities_href?: string | null;
|
||||
opportunities_label?: string | null;
|
||||
has_opportunities?: boolean;
|
||||
};
|
||||
|
||||
export default function EducationalArticleCard({
|
||||
article,
|
||||
}: {
|
||||
article: EducationalArticle;
|
||||
}) {
|
||||
const focus =
|
||||
'focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-emerald-800';
|
||||
return (
|
||||
<article className="flex min-w-0 flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white text-slate-900 shadow-sm">
|
||||
{article.image && (
|
||||
<img
|
||||
className="aspect-[16/9] w-full object-cover"
|
||||
src={article.image.href}
|
||||
alt={article.image.alt}
|
||||
loading="lazy"
|
||||
width={800}
|
||||
height={450}
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-1 flex-col items-start gap-4 p-5 sm:p-6">
|
||||
<p className="text-sm font-medium text-emerald-800">
|
||||
Guia sectorial
|
||||
</p>
|
||||
<h3 className="text-2xl leading-tight font-bold">
|
||||
<Link href={article.href} className={focus}>
|
||||
{article.title}
|
||||
</Link>
|
||||
</h3>
|
||||
{article.summary && (
|
||||
<p className="text-base leading-relaxed text-slate-700">
|
||||
{article.summary}
|
||||
</p>
|
||||
)}
|
||||
{article.author && (
|
||||
<p className="text-sm text-slate-600">
|
||||
Por {article.author}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-auto flex flex-wrap items-center gap-x-5 gap-y-3 pt-2">
|
||||
<Link
|
||||
href={article.href}
|
||||
className={`inline-flex min-h-11 items-center rounded-lg bg-emerald-900 px-5 py-3 font-semibold text-white hover:bg-emerald-800 ${focus}`}
|
||||
>
|
||||
Ler o guia
|
||||
</Link>
|
||||
{article.has_opportunities &&
|
||||
article.opportunities_href && (
|
||||
<Link
|
||||
href={article.opportunities_href}
|
||||
className={`inline-flex min-h-11 items-center py-2 font-semibold text-emerald-900 underline underline-offset-4 ${focus}`}
|
||||
>
|
||||
{article.opportunities_label ??
|
||||
'Ver oportunidades'}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import EducationalArticleCard, {
|
||||
type EducationalArticle,
|
||||
} from './educational-article-card';
|
||||
|
||||
export default function EducationalGuides({
|
||||
articles,
|
||||
}: {
|
||||
articles: EducationalArticle[];
|
||||
}) {
|
||||
if (!articles.length) return null;
|
||||
return (
|
||||
<section
|
||||
aria-label="Guias para preparar o investimento"
|
||||
className="space-y-5"
|
||||
>
|
||||
<h2 className="text-2xl font-bold">Descubra onde investir</h2>
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{articles.map((article) => (
|
||||
<EducationalArticleCard
|
||||
key={article.href}
|
||||
article={article}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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,24 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
|
||||
type Snapshot = {context_revision:number;path?:{definition_version:number;steps:Array<{code:string;title:string;applicability:string;reason:string;state:string;context_review_required?:boolean}>}; zone?:{title:string;href:string;version:number|string;fields?:Record<string,string>;analysis?:{conditions?:Array<{id:string;label:string;knowledge:string;source_reference?:string;valid_from?:string;valid_until?:string}>;source?:{title:string;url:string;diploma?:string;article?:string;valid_from?:string;valid_until?:string}|null}}|null;fields?:Record<string,string>};
|
||||
export function InvestmentContextAssessment({raw,contextId}:{raw:unknown;contextId?:string|null}) {
|
||||
let snapshot:Snapshot|null=null;
|
||||
try {const value=JSON.parse(String(raw ?? ''));if(value.version===1) snapshot=value;} catch { /* Missing context remains explicit. */ }
|
||||
if(!snapshot) return null;
|
||||
const fields=snapshot.fields ?? {};const zone=snapshot.zone;
|
||||
const regime=zone?.fields?.investment_regime ?? fields.investment_regime;
|
||||
const names:Record<string,string>={zee:'ZEE',zfi:'ZFI',industrial_park:'Parque industrial'};
|
||||
return <section className="space-y-2 rounded border border-amber-400 p-3" aria-label="Aplicabilidade ao investimento">
|
||||
<h3 className="font-semibold">Aplicabilidade ao investimento · contexto v{snapshot.context_revision}</h3>
|
||||
<p>Origem do investidor: {{national:'nacional',foreign:'estrangeiro',mixed:'misto'}[fields.investor_origin] ?? 'informação em falta'}. Residência fiscal declarada: {fields.tax_residency_country || 'informação em falta'}. Estas condições são distintas; uma não determina a outra.</p>
|
||||
{zone ? <><p><strong>Potencialmente aplicável — análise específica de {names[regime ?? ''] ?? 'zona seleccionada'}.</strong> <Link className="underline" href={zone.href}>{zone.title} · versão {zone.version} · consultar ficha e fundamentos</Link>.</p>
|
||||
<p>A selecção identifica a referência consultada; não confirma o reconhecimento da entidade, actividade autorizada, licença ou acesso a benefícios. Consulte as condições documentadas na ficha e reúna a autorização ou evidência correspondente no dossier.</p>
|
||||
{zone.analysis?.source && <p>Fundamento cadastrado: <a className="underline" href={zone.analysis.source.url}>{zone.analysis.source.diploma || zone.analysis.source.title} {zone.analysis.source.article}</a>. Período cadastrado: {zone.analysis.source.valid_from || 'início por indicar'} a {zone.analysis.source.valid_until || 'sem termo indicado'}. A cobertura do investimento exige correspondência com estas condições.</p>}
|
||||
{zone.analysis?.conditions?.length ? <ul className="list-disc pl-5">{zone.analysis.conditions.map(condition=><li key={condition.id}><strong>{condition.label}</strong>: {condition.knowledge==='documented'?'condição documentada; cumprimento pelo investimento por confirmar':condition.knowledge==='disputed'?'condição com informação divergente':'informação em falta'}. {condition.source_reference && <>Fundamento: {condition.source_reference}.</>} {condition.valid_from && <>Desde {condition.valid_from}.</>} {condition.valid_until && <>Até {condition.valid_until}.</>}</li>)}</ul> : <p>Condições específicas: informação estruturada em falta nesta referência. A ficha e os documentos continuam consultáveis; não se presume cumprimento.</p>}
|
||||
<p role="status">Benefício fiscal deste enquadramento: indeterminado. Não existe neste consumidor uma regra executável ligada ao reconhecimento e condições da zona. No simulador, o IVA disponível corresponde apenas ao cenário geral sem benefício, sobre vendas declaradas compatíveis; não simula o regime especial. O cenário com benefício e o apuramento final estão indisponíveis.</p></>
|
||||
: <p>Enquadramento geral de preparação: sem ZEE, ZFI ou parque seleccionado. Isso não confirma a elegibilidade para uma operação fiscal. O cálculo mantém o âmbito e as exclusões da versão publicada.</p>}
|
||||
<p>Confirmado neste quadro significa apenas dado recuperado do dossier; declarações e referências não são decisões jurídicas. Requisitos específicos não documentados permanecem indeterminados.</p>
|
||||
{snapshot.path && <details><summary>Passos adaptados pelo percurso existente · versão {snapshot.path.definition_version}</summary><ul className="list-disc pl-5">{snapshot.path.steps.map(step=><li key={step.code}>{step.code} — {step.title}: {step.context_review_required?'informação alterada; rever fundamento':step.applicability==='not_needed_for_preparation'?'não aplicável à preparação declarada':step.applicability==='undetermined'?'informação em falta':['completed','approved'].includes(step.state)?'preparação confirmada no percurso; não é decisão fiscal':'potencialmente aplicável'}. {step.reason}</li>)}</ul></details>}
|
||||
{contextId && <Link className="underline" href={`/investimentos/${contextId}/contexto/editar`}>Alterar contexto e comparar impacto antes de guardar</Link>}
|
||||
</section>;
|
||||
}
|
||||
@@ -24,7 +24,9 @@ export default function InvestmentMapPanel({
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
const params = new URLSearchParams(JSON.parse(key));
|
||||
setPoints(null); setError(''); setMissing([]);
|
||||
setPoints(null);
|
||||
setError('');
|
||||
setMissing([]);
|
||||
setAreas([]);
|
||||
fetch(`/api/v1/investment/map?${params}`, {
|
||||
signal: controller.signal,
|
||||
@@ -46,13 +48,21 @@ export default function InvestmentMapPanel({
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!controller.signal.aborted) setError('Não foi possível concluir a pesquisa.');
|
||||
if (!controller.signal.aborted)
|
||||
setError('Não foi possível concluir a pesquisa.');
|
||||
});
|
||||
return () => controller.abort();
|
||||
}, [key, retry]);
|
||||
return (
|
||||
<section className="space-y-3">
|
||||
<SearchState loading={points === null && !error} error={error} total={(points?.length ?? 0) + missing.length} retry={() => setRetry(value => value + 1)} />
|
||||
<SearchState
|
||||
scope="Registos consultados pelo mapa"
|
||||
countLabel="registos, incluindo os que não têm coordenadas"
|
||||
loading={points === null && !error}
|
||||
error={error}
|
||||
total={(points?.length ?? 0) + missing.length}
|
||||
retry={() => setRetry((value) => value + 1)}
|
||||
/>
|
||||
<InteractiveInvestmentMap points={points} areas={areas} />
|
||||
<TerritoryGeometryPreview
|
||||
territoryId={
|
||||
|
||||
@@ -113,9 +113,7 @@ export function ProvinceSelect({
|
||||
})
|
||||
.catch(() => {
|
||||
if (!controller.signal.aborted)
|
||||
setError(
|
||||
'Não foi possível concluir a pesquisa.',
|
||||
);
|
||||
setError('Não foi possível concluir a pesquisa.');
|
||||
})
|
||||
.finally(() => {
|
||||
if (!controller.signal.aborted) setLoading(false);
|
||||
@@ -130,7 +128,13 @@ export function ProvinceSelect({
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchState loading={loading} error={error} total={options.length} retry={() => setRetry(value => value + 1)} />
|
||||
<SearchState
|
||||
scope="Localizações"
|
||||
loading={loading}
|
||||
error={error}
|
||||
total={options.length}
|
||||
retry={() => setRetry((value) => value + 1)}
|
||||
/>
|
||||
<input
|
||||
className={className}
|
||||
aria-label={`Pesquisar ${label}`}
|
||||
@@ -300,9 +304,7 @@ export function TerritoryLevel({
|
||||
setMore(Boolean(data.next_page_url));
|
||||
} catch {
|
||||
if (!controller.signal.aborted)
|
||||
setError(
|
||||
'Não foi possível concluir a pesquisa.',
|
||||
);
|
||||
setError('Não foi possível concluir a pesquisa.');
|
||||
} finally {
|
||||
if (!controller.signal.aborted) setLoading(false);
|
||||
}
|
||||
@@ -314,7 +316,15 @@ export function TerritoryLevel({
|
||||
}, [index, parent, page, enabled, endpoint, retry, search]);
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{enabled && <SearchState loading={loading} error={error} total={options.length} retry={() => setRetry(value => value + 1)} />}
|
||||
{enabled && (
|
||||
<SearchState
|
||||
scope={labels[index]}
|
||||
loading={loading}
|
||||
error={error}
|
||||
total={options.length}
|
||||
retry={() => setRetry((value) => value + 1)}
|
||||
/>
|
||||
)}
|
||||
<label htmlFor={id} className="font-medium">
|
||||
{labels[index]}
|
||||
</label>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -831,7 +831,11 @@ export function ClassificationSummary({
|
||||
<h2 className="text-xl font-bold">
|
||||
Associações territoriais e produtivas
|
||||
</h2>
|
||||
<p>{help}</p>
|
||||
<p>
|
||||
As relações preliminares permanecem por confirmar. Uma
|
||||
associação não comprova disponibilidade, incentivos ou direitos
|
||||
concedidos.
|
||||
</p>
|
||||
{Object.entries({
|
||||
territory: 'Enquadramento territorial',
|
||||
production: 'Organização produtiva',
|
||||
@@ -867,10 +871,7 @@ export function ClassificationSummary({
|
||||
</p>
|
||||
))
|
||||
) : (
|
||||
<p>
|
||||
Sem informação tipada publicada nesta dimensão;
|
||||
consulte também as relações anteriores abaixo.
|
||||
</p>
|
||||
<p>Sem associações publicadas neste grupo.</p>
|
||||
)}
|
||||
</section>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,480 @@
|
||||
import { Field, input, button, groupLabels } from '@/pages/investment/shared';
|
||||
import type { References } from '@/pages/investment/shared';
|
||||
import { TerritoryLevel, type TerritoryOption } from './investment-territory';
|
||||
|
||||
export const modalityLabels: Record<string, string> = {
|
||||
investment_project: 'Projecto de investimento',
|
||||
real_estate: 'Imobiliário',
|
||||
land_rights: 'DUAT e direitos fundiários',
|
||||
business_sale: 'Venda de negócio',
|
||||
business_partnership: 'Parceria empresarial',
|
||||
franchise: 'Franquia',
|
||||
concession: 'Concessão',
|
||||
financing: 'Financiamento',
|
||||
supply: 'Fornecimento',
|
||||
other: 'Outra modalidade',
|
||||
};
|
||||
type Values = Record<string, string | boolean | number[]>;
|
||||
const labels: Record<string, string> = {
|
||||
q: 'O que procura',
|
||||
sector: 'Sector',
|
||||
category: 'Modalidade da oportunidade',
|
||||
preparation: 'Estado de preparação',
|
||||
sort: 'Ordenar',
|
||||
min_amount: 'Investimento mínimo',
|
||||
max_amount: 'Investimento máximo',
|
||||
currency: 'Moeda',
|
||||
before: 'Data-alvo até',
|
||||
cluster_id: 'Cluster',
|
||||
corridor_id: 'Corredor',
|
||||
project_id: 'Projecto âncora',
|
||||
zee_id: 'ZEE',
|
||||
zfi_id: 'ZFI',
|
||||
park_id: 'Parque industrial',
|
||||
chain_id: 'Cadeia de valor',
|
||||
subsector_id: 'Actividade',
|
||||
include_national: 'Incluir âmbito nacional',
|
||||
education: 'Educação',
|
||||
province: 'Província indicada',
|
||||
district: 'Distrito indicado',
|
||||
dimension: 'Grupo de oportunidades',
|
||||
territory_basis: 'Localização considerada',
|
||||
geo_location_id: 'Localização adicional',
|
||||
maturity_level: 'Classificação histórica',
|
||||
association_type: 'Associação herdada',
|
||||
association_target: 'Referência herdada',
|
||||
association_purpose: 'Relação indicada',
|
||||
association_state: 'Critério herdado',
|
||||
authority_id: 'Entidade',
|
||||
publication_state: 'Critério herdado',
|
||||
};
|
||||
export default function OpportunitySearchFilters({
|
||||
data,
|
||||
references,
|
||||
path,
|
||||
change,
|
||||
territory,
|
||||
submit,
|
||||
clear,
|
||||
errors,
|
||||
}: {
|
||||
data: Values;
|
||||
references: References;
|
||||
path: TerritoryOption[];
|
||||
change: (values: Values) => void;
|
||||
territory: (path: TerritoryOption[]) => void;
|
||||
submit: () => void;
|
||||
clear: () => void;
|
||||
errors: Record<string, string>;
|
||||
}) {
|
||||
const select = (
|
||||
key: string,
|
||||
choices: { value: string; label: string }[],
|
||||
hint?: string,
|
||||
) => (
|
||||
<Field key={key} label={labels[key]}>
|
||||
<select
|
||||
className={input}
|
||||
value={String(data[key] ?? '')}
|
||||
onChange={(e) =>
|
||||
change({
|
||||
[key]: e.target.value,
|
||||
...(key === 'sector'
|
||||
? { subsector_id: '', chain_id: '' }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Todos</option>
|
||||
{choices.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{hint && <p className="text-sm text-slate-600">{hint}</p>}
|
||||
</Field>
|
||||
);
|
||||
const zoneChoices = (category: string) =>
|
||||
(references.zones ?? [])
|
||||
.filter((z) => z.category === category)
|
||||
.map((z) => ({ value: z.id, label: z.name }));
|
||||
const choicesFor = (key: string) => {
|
||||
if (key === 'sector')
|
||||
return references.sectors.map((s) => ({
|
||||
value: s.slug,
|
||||
label:
|
||||
s.slug === 'fintech'
|
||||
? 'Fintech — especialização do Mercado Financeiro'
|
||||
: s.name,
|
||||
}));
|
||||
if (key === 'category')
|
||||
return (references.categories ?? []).map((c) => ({
|
||||
value: c,
|
||||
label: modalityLabels[c] ?? 'Outra modalidade',
|
||||
}));
|
||||
if (key === 'preparation') return references.preparationOptions ?? [];
|
||||
if (key === 'dimension')
|
||||
return [
|
||||
{ value: 'cluster', label: 'Clusters' },
|
||||
{ value: 'corridor', label: 'Corredores' },
|
||||
{ value: 'zee', label: 'ZEE' },
|
||||
{ value: 'zfi', label: 'ZFI' },
|
||||
];
|
||||
if (['zee_id', 'zfi_id', 'park_id'].includes(key))
|
||||
return zoneChoices(
|
||||
(
|
||||
{
|
||||
zee_id: 'zee',
|
||||
zfi_id: 'zfi',
|
||||
park_id: 'industrial_park',
|
||||
} as Record<string, string>
|
||||
)[key],
|
||||
);
|
||||
const group = (
|
||||
{
|
||||
cluster_id: 'clusters',
|
||||
corridor_id: 'corridors',
|
||||
project_id: 'projects',
|
||||
} as const
|
||||
)[key as 'cluster_id'];
|
||||
if (group)
|
||||
return references[group].map((e) => ({
|
||||
value: e.id,
|
||||
label: e.name,
|
||||
}));
|
||||
if (key === 'chain_id' || key === 'subsector_id')
|
||||
return (
|
||||
references.classification?.targets[
|
||||
key === 'chain_id' ? 'chain' : 'activity'
|
||||
] ?? []
|
||||
).map((e) => ({ value: String(e.id), label: e.label }));
|
||||
return [];
|
||||
};
|
||||
const remove = (key: string) => {
|
||||
if (key === 'sector')
|
||||
change({ sector: '', subsector_id: '', chain_id: '' });
|
||||
else if (key === 'facets') change({ facets: [] });
|
||||
else change({ [key]: typeof data[key] === 'boolean' ? false : '' });
|
||||
};
|
||||
const active = Object.entries(data).filter(
|
||||
([k, v]) =>
|
||||
![
|
||||
'view',
|
||||
'sort',
|
||||
'province_id',
|
||||
'district_id',
|
||||
'admin_post_id',
|
||||
'locality_id',
|
||||
].includes(k) &&
|
||||
v !== '' &&
|
||||
v !== false &&
|
||||
v !== null &&
|
||||
(!Array.isArray(v) || v.length > 0) &&
|
||||
!(k === 'territory_basis' && v === 'all'),
|
||||
);
|
||||
return (
|
||||
<form
|
||||
className="space-y-5 rounded-xl border bg-white p-4 sm:p-6"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
submit();
|
||||
}}
|
||||
aria-label="Pesquisar oportunidades"
|
||||
>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<Field label="O que procura">
|
||||
<input
|
||||
className={input}
|
||||
value={String(data.q)}
|
||||
placeholder="Palavras do título ou da descrição"
|
||||
onChange={(e) => change({ q: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
{select('sector', choicesFor('sector'))}
|
||||
{select('category', choicesFor('category'))}
|
||||
</div>
|
||||
<fieldset className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<legend className="mb-2 font-semibold">Localização</legend>
|
||||
{(
|
||||
[
|
||||
'province_id',
|
||||
'district_id',
|
||||
'admin_post_id',
|
||||
'locality_id',
|
||||
] as const
|
||||
).map((key, index) => (
|
||||
<TerritoryLevel
|
||||
key={`${key}-${path[index - 1]?.id ?? ''}`}
|
||||
index={index}
|
||||
parent={path[index - 1]?.id ?? ''}
|
||||
selected={path[index]}
|
||||
endpoint="/api/v1/investment/territory-options"
|
||||
onChange={(value) =>
|
||||
territory([
|
||||
...path.slice(0, index),
|
||||
...(value ? [value] : []),
|
||||
])
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</fieldset>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<Field label="Estado de preparação">
|
||||
<select
|
||||
className={input}
|
||||
value={String(data.preparation ?? '')}
|
||||
onChange={(e) =>
|
||||
change({
|
||||
preparation: e.target.value,
|
||||
maturity_level: '',
|
||||
})
|
||||
}
|
||||
>
|
||||
<option value="">Todos os estados</option>
|
||||
{[
|
||||
'Etapas de preparação',
|
||||
'Classificações históricas',
|
||||
].map((group) => (
|
||||
<optgroup key={group} label={group}>
|
||||
{(references.preparationOptions ?? [])
|
||||
.filter((o) => o.group === group)
|
||||
.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</optgroup>
|
||||
))}
|
||||
</select>
|
||||
<p className="text-sm text-slate-600">
|
||||
Os registos históricos são apresentados separadamente e
|
||||
conservam a classificação indicada na ficha.
|
||||
</p>
|
||||
</Field>
|
||||
{(['min_amount', 'max_amount'] as const).map((key) => (
|
||||
<Field key={key} label={labels[key]}>
|
||||
<input
|
||||
className={input}
|
||||
type="number"
|
||||
min="0"
|
||||
step="0.01"
|
||||
value={String(data[key])}
|
||||
onChange={(e) => change({ [key]: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
))}
|
||||
{select(
|
||||
'currency',
|
||||
['MZN', 'USD', 'EUR', 'ZAR'].map((c) => ({
|
||||
value: c,
|
||||
label: c,
|
||||
})),
|
||||
'Seleccione uma moeda para comparar montantes. Não se efectuam conversões cambiais.',
|
||||
)}
|
||||
</div>
|
||||
<details>
|
||||
<summary className="cursor-pointer rounded border p-3 font-semibold focus-visible:outline-2">
|
||||
Mais filtros
|
||||
</summary>
|
||||
<div className="mt-4 grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{[
|
||||
'corridor_id',
|
||||
'cluster_id',
|
||||
'zee_id',
|
||||
'zfi_id',
|
||||
'park_id',
|
||||
'project_id',
|
||||
'subsector_id',
|
||||
'chain_id',
|
||||
].map((key) => select(key, choicesFor(key)))}
|
||||
<Field label="Data-alvo até">
|
||||
<input
|
||||
className={input}
|
||||
type="date"
|
||||
value={String(data.before)}
|
||||
onChange={(e) => change({ before: e.target.value })}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
<p className="my-3 text-sm">
|
||||
Estes filtros procuram associações documentadas. Estar perto
|
||||
de um parque ou no mesmo distrito não constitui associação,
|
||||
nem confirma incentivos ou disponibilidade.
|
||||
</p>
|
||||
<label className="flex gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={Boolean(data.include_national)}
|
||||
onChange={(e) =>
|
||||
change({ include_national: e.target.checked })
|
||||
}
|
||||
/>
|
||||
Incluir oportunidades de âmbito nacional
|
||||
</label>
|
||||
<details className="mt-3">
|
||||
<summary className="cursor-pointer font-medium">
|
||||
Outras características
|
||||
</summary>
|
||||
<div className="mt-3 grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{[
|
||||
'education',
|
||||
'infrastructure',
|
||||
'labour',
|
||||
'qualification',
|
||||
'support',
|
||||
'impact',
|
||||
'partnership',
|
||||
]
|
||||
.filter((group) =>
|
||||
references.options.some(
|
||||
(o) => o.group === group,
|
||||
),
|
||||
)
|
||||
.map((group) => (
|
||||
<fieldset
|
||||
key={group}
|
||||
className="rounded border p-3"
|
||||
>
|
||||
<legend>{groupLabels[group]}</legend>
|
||||
{references.options
|
||||
.filter((o) => o.group === group)
|
||||
.map((option) => (
|
||||
<label
|
||||
key={option.id}
|
||||
className="flex gap-2 py-1"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={
|
||||
Array.isArray(
|
||||
data.facets,
|
||||
) &&
|
||||
data.facets.includes(
|
||||
option.id,
|
||||
)
|
||||
}
|
||||
onChange={(e) => {
|
||||
const selected =
|
||||
Array.isArray(
|
||||
data.facets,
|
||||
)
|
||||
? data.facets
|
||||
: [];
|
||||
change({
|
||||
facets: e.target
|
||||
.checked
|
||||
? [
|
||||
...selected,
|
||||
option.id,
|
||||
]
|
||||
: selected.filter(
|
||||
(id) =>
|
||||
id !==
|
||||
option.id,
|
||||
),
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{option.label}
|
||||
</label>
|
||||
))}
|
||||
</fieldset>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</details>
|
||||
<section
|
||||
aria-label="Filtros activos"
|
||||
className="flex flex-wrap gap-2"
|
||||
>
|
||||
{path.map((node, index) => (
|
||||
<button
|
||||
className="rounded border px-3 py-2"
|
||||
type="button"
|
||||
key={node.id}
|
||||
aria-label={`Remover localização ${node.name}`}
|
||||
onClick={() => territory(path.slice(0, index))}
|
||||
>
|
||||
{node.name} ×
|
||||
</button>
|
||||
))}
|
||||
{active.flatMap(([key, value]) =>
|
||||
key === 'facets' && Array.isArray(value)
|
||||
? value.map((id) => (
|
||||
<button
|
||||
key={`facet-${id}`}
|
||||
type="button"
|
||||
className="rounded border px-3 py-2"
|
||||
onClick={() =>
|
||||
change({
|
||||
facets: value.filter((x) => x !== id),
|
||||
})
|
||||
}
|
||||
>
|
||||
{' '}
|
||||
{references.options.find((o) => o.id === id)
|
||||
?.label ?? 'Critério adicional'}{' '}
|
||||
×
|
||||
</button>
|
||||
))
|
||||
: [
|
||||
<button
|
||||
key={key}
|
||||
type="button"
|
||||
className="rounded border px-3 py-2 text-left"
|
||||
aria-label={`Remover ${labels[key] ?? 'critério adicional'}`}
|
||||
onClick={() => remove(key)}
|
||||
>
|
||||
{labels[key] ?? 'Critério adicional'}:{' '}
|
||||
{typeof value === 'boolean'
|
||||
? 'Sim'
|
||||
: (choicesFor(key).find(
|
||||
(o) => o.value === String(value),
|
||||
)?.label ??
|
||||
([
|
||||
'q',
|
||||
'min_amount',
|
||||
'max_amount',
|
||||
'currency',
|
||||
'before',
|
||||
'province',
|
||||
'district',
|
||||
].includes(key)
|
||||
? String(value)
|
||||
: 'seleccionado'))}{' '}
|
||||
×
|
||||
</button>,
|
||||
],
|
||||
)}
|
||||
</section>
|
||||
{Object.entries(errors).map(([key, value]) => (
|
||||
<p key={key} role="alert">
|
||||
{value}
|
||||
</p>
|
||||
))}
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
{select('sort', [
|
||||
{ value: 'recent', label: 'Mais recentes' },
|
||||
{ value: 'title', label: 'Título' },
|
||||
{
|
||||
value: 'amount_asc',
|
||||
label: 'Montante crescente, por moeda',
|
||||
},
|
||||
{
|
||||
value: 'amount_desc',
|
||||
label: 'Montante decrescente, por moeda',
|
||||
},
|
||||
])}
|
||||
<button className={button}>Pesquisar</button>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded border px-4 py-2 underline"
|
||||
onClick={clear}
|
||||
>
|
||||
Limpar filtros
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -3,14 +3,16 @@ 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 +54,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 +75,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}
|
||||
@@ -111,6 +124,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 +135,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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,14 +1,45 @@
|
||||
import { router, usePage } from '@inertiajs/react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import CatalogueFilterContext from './catalogue-filter-context';
|
||||
// Shared feedback for existing Inertia search consumers; form state is never reset.
|
||||
const collections = ['results','records','resources','partnerships','opportunities','interests','investors','leads','articles','services','items','entries','documents','requests','batches','users','assignments','zones','matches','businesses'];
|
||||
const collections = [
|
||||
'results',
|
||||
'records',
|
||||
'resources',
|
||||
'partnerships',
|
||||
'opportunities',
|
||||
'interests',
|
||||
'investors',
|
||||
'leads',
|
||||
'articles',
|
||||
'services',
|
||||
'items',
|
||||
'entries',
|
||||
'documents',
|
||||
'requests',
|
||||
'batches',
|
||||
'users',
|
||||
'assignments',
|
||||
'zones',
|
||||
'matches',
|
||||
'businesses',
|
||||
];
|
||||
function count(props: Record<string, unknown>): number | null {
|
||||
if (props.demands && props.capacities) return Number((props.demands as {total:number}).total) + Number((props.capacities as {total:number}).total);
|
||||
if (props.plan && Array.isArray((props.plan as {rows:unknown[]}).rows)) return (props.plan as {rows:unknown[]}).rows.length;
|
||||
if (props.demands && props.capacities)
|
||||
return (
|
||||
Number((props.demands as { total: number }).total) +
|
||||
Number((props.capacities as { total: number }).total)
|
||||
);
|
||||
if (props.plan && Array.isArray((props.plan as { rows: unknown[] }).rows))
|
||||
return (props.plan as { rows: unknown[] }).rows.length;
|
||||
if (typeof props.total === 'number') return props.total;
|
||||
if (typeof props.match === 'boolean' && Array.isArray(props.articles)) return props.match ? props.articles.length : 0;
|
||||
if (typeof props.match === 'boolean' && Array.isArray(props.articles))
|
||||
return props.match ? props.articles.length : 0;
|
||||
for (const key of collections) {
|
||||
const value = props[key] as {total?:number; meta?:{total?:number}; data?:unknown[]} | unknown[] | undefined;
|
||||
const value = props[key] as
|
||||
| { total?: number; meta?: { total?: number }; data?: unknown[] }
|
||||
| unknown[]
|
||||
| undefined;
|
||||
if (!value) continue;
|
||||
if (Array.isArray(value)) return value.length;
|
||||
if (typeof value.total === 'number') return value.total;
|
||||
@@ -19,28 +50,140 @@ function count(props: Record<string, unknown>): number | null {
|
||||
}
|
||||
export default function SearchFeedback() {
|
||||
const page = usePage();
|
||||
const [state, setState] = useState<'idle'|'loading'|'error'>('idle');
|
||||
const latest = useRef<{url: string; data: Record<string, unknown>} | null>(null);
|
||||
const handledLocally = page.component === 'investment/catalogue';
|
||||
const [state, setState] = useState<'idle' | 'loading' | 'error'>('idle');
|
||||
const latest = useRef<{
|
||||
url: string;
|
||||
data: Record<string, unknown>;
|
||||
} | null>(null);
|
||||
const active = useRef(false);
|
||||
const searchable = page.props.filters !== undefined || ['portal/legacy','desk/catalogue','investment/zones','partnerships/catalogue','education/index','management/territory-geometry-import'].includes(page.component);
|
||||
const total = searchable ? count(page.props) : null;
|
||||
const searchable =
|
||||
page.props.filters !== undefined ||
|
||||
[
|
||||
'portal/legacy',
|
||||
'desk/catalogue',
|
||||
'investment/zones',
|
||||
'partnerships/catalogue',
|
||||
'education/index',
|
||||
'management/territory-geometry-import',
|
||||
].includes(page.component);
|
||||
// A page may contain several independent catalogues. Never infer a public
|
||||
// result universe by whichever collection happens to occur first.
|
||||
const descriptors: Record<string, [string, string]> = {
|
||||
'portfolio/index': ['records', 'Fichas da carteira'],
|
||||
'investment/zones': ['records', 'Zonas e parques'],
|
||||
'investment/business-marketplace': ['results', 'Anúncios de negócios'],
|
||||
'portal/legacy': ['articles', 'Artigos'],
|
||||
};
|
||||
const descriptor =
|
||||
page.component === 'portal/services'
|
||||
? page.props.section === 'documents' ||
|
||||
page.props.section === 'legislation'
|
||||
? ['resources', 'Documentos']
|
||||
: page.props.section === 'sectors'
|
||||
? ['sectors', 'Sectores']
|
||||
: null
|
||||
: descriptors[page.component];
|
||||
const total = descriptor
|
||||
? page.component === 'portal/legacy' && page.props.match === false
|
||||
? 0
|
||||
: count({ results: page.props[descriptor[0]] })
|
||||
: searchable
|
||||
? count(page.props)
|
||||
: null;
|
||||
const label = descriptor?.[1];
|
||||
useEffect(() => {
|
||||
const off = [router.on('before', event => {
|
||||
const visit = event.detail.visit;
|
||||
if (visit.method !== 'get') { active.current = false; return; }
|
||||
const data = visit.data as Record<string, unknown>;
|
||||
const url = visit.url;
|
||||
const isSearch = total !== null || ['q','query','search','filter','status','sector'].some(key => key in data || url.searchParams.has(key));
|
||||
active.current = isSearch;
|
||||
if (isSearch) { latest.current = {url: url.toString(), data}; setState('loading'); }
|
||||
}), router.on('success', () => { if (active.current) { active.current = false; setState('idle'); } }),
|
||||
router.on('error', () => { if (active.current) setState('error'); }),
|
||||
router.on('httpException', event => { if (active.current) { event.preventDefault(); setState('error'); } }),
|
||||
router.on('networkError', event => { if (active.current) { event.preventDefault(); setState('error'); } })];
|
||||
return () => off.forEach(remove => remove());
|
||||
}, [total]);
|
||||
if (handledLocally) return;
|
||||
const off = [
|
||||
router.on('before', (event) => {
|
||||
const visit = event.detail.visit;
|
||||
if (visit.method !== 'get') {
|
||||
active.current = false;
|
||||
return;
|
||||
}
|
||||
const data = visit.data as Record<string, unknown>;
|
||||
const url = visit.url;
|
||||
const isSearch =
|
||||
total !== null ||
|
||||
['q', 'query', 'search', 'filter', 'status', 'sector'].some(
|
||||
(key) => key in data || url.searchParams.has(key),
|
||||
);
|
||||
active.current = isSearch;
|
||||
if (isSearch) {
|
||||
latest.current = { url: url.toString(), data };
|
||||
setState('loading');
|
||||
}
|
||||
}),
|
||||
router.on('success', () => {
|
||||
if (active.current) {
|
||||
active.current = false;
|
||||
setState('idle');
|
||||
}
|
||||
}),
|
||||
router.on('error', () => {
|
||||
if (active.current) setState('error');
|
||||
}),
|
||||
router.on('httpException', (event) => {
|
||||
if (active.current) {
|
||||
event.preventDefault();
|
||||
setState('error');
|
||||
}
|
||||
}),
|
||||
router.on('networkError', (event) => {
|
||||
if (active.current) {
|
||||
event.preventDefault();
|
||||
setState('error');
|
||||
}
|
||||
}),
|
||||
];
|
||||
return () => off.forEach((remove) => remove());
|
||||
}, [total, handledLocally]);
|
||||
if (handledLocally) return null;
|
||||
if (total === null && state === 'idle') return null;
|
||||
return <aside data-search-feedback role={state === 'error' ? 'alert' : 'status'} aria-live="polite" aria-atomic="true" className="sticky top-0 z-50 border-b bg-white p-3 text-center text-slate-950 shadow">
|
||||
{state === 'loading' ? 'A pesquisar…' : state === 'error' ? <><span>Não foi possível concluir a pesquisa. Os resultados anteriores mantêm-se abaixo. </span><button type="button" className="underline" onClick={() => { const request = latest.current; if (request) router.get(request.url, request.data as never, {preserveState:true, preserveScroll:true}); }}>Tentar novamente</button></> : total === 0 ? 'Não foram encontrados resultados para esta pesquisa' : `${total} resultados encontrados`}
|
||||
</aside>;
|
||||
return (
|
||||
<>
|
||||
<aside
|
||||
data-search-feedback
|
||||
role={state === 'error' ? 'alert' : 'status'}
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
className="border-b bg-white p-3 text-center text-slate-950"
|
||||
>
|
||||
{label && <strong>{label}: </strong>}
|
||||
{state === 'loading' ? (
|
||||
'A pesquisar…'
|
||||
) : state === 'error' ? (
|
||||
<>
|
||||
<span>
|
||||
Não foi possível concluir a pesquisa. Os resultados
|
||||
anteriores mantêm-se abaixo.{' '}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="underline"
|
||||
onClick={() => {
|
||||
const request = latest.current;
|
||||
if (request)
|
||||
router.get(
|
||||
request.url,
|
||||
request.data as never,
|
||||
{
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
},
|
||||
);
|
||||
}}
|
||||
>
|
||||
Tentar novamente
|
||||
</button>
|
||||
</>
|
||||
) : total === 0 ? (
|
||||
'Não foram encontrados resultados para esta pesquisa'
|
||||
) : (
|
||||
`${total} ${total === 1 ? 'resultado encontrado' : 'resultados encontrados'}`
|
||||
)}
|
||||
</aside>
|
||||
{descriptor && <CatalogueFilterContext />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,48 @@
|
||||
export default function SearchState({loading, error, total, retry}: {loading:boolean; error?:string; total:number; retry:()=>void}) {
|
||||
return <div role={error ? 'alert' : 'status'} aria-live="polite" aria-atomic="true" className="my-3 rounded border p-3">
|
||||
{loading ? 'A pesquisar…' : error ? <>Não foi possível concluir a pesquisa. <button type="button" className="underline" onClick={retry}>Tentar novamente</button></> : total === 0 ? 'Não foram encontrados resultados para esta pesquisa' : `${total} resultados encontrados`}
|
||||
</div>;
|
||||
export default function SearchState({
|
||||
loading,
|
||||
error,
|
||||
total,
|
||||
retry,
|
||||
scope,
|
||||
countLabel,
|
||||
}: {
|
||||
loading: boolean;
|
||||
error?: string;
|
||||
total: number;
|
||||
retry: () => void;
|
||||
scope?: string;
|
||||
countLabel?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
role={error ? 'alert' : 'status'}
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
className="my-3 rounded border p-3"
|
||||
>
|
||||
{scope && <span className="font-medium">{scope}: </span>}
|
||||
{loading ? (
|
||||
'A pesquisar…'
|
||||
) : error ? (
|
||||
<>
|
||||
Não foi possível concluir a pesquisa.{' '}
|
||||
<button type="button" className="underline" onClick={retry}>
|
||||
Tentar novamente
|
||||
</button>
|
||||
</>
|
||||
) : total === 0 ? (
|
||||
scope ? (
|
||||
'Nenhuma opção encontrada'
|
||||
) : (
|
||||
'Não foram encontrados resultados para esta pesquisa'
|
||||
)
|
||||
) : countLabel ? (
|
||||
`${total} ${countLabel}`
|
||||
) : scope ? (
|
||||
`${total} ${total === 1 ? 'opção disponível' : 'opções disponíveis'}`
|
||||
) : (
|
||||
`${total} ${total === 1 ? 'resultado encontrado' : 'resultados encontrados'}`
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
import EducationalArticleCard, {
|
||||
type EducationalArticle,
|
||||
} from './educational-article-card';
|
||||
export type SectorEntryData = {
|
||||
articles: {
|
||||
title: string;
|
||||
href: string;
|
||||
image: { href: string; alt: string } | null;
|
||||
scope: string;
|
||||
}[];
|
||||
articles: EducationalArticle[];
|
||||
relations: {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -66,9 +64,7 @@ export default function SectorEntry({ data }: { data: SectorEntryData }) {
|
||||
perfil e a modalidade já autorizados; se ainda não tem perfil,
|
||||
pode preparar a ideia e pedir o perfil de promotor.
|
||||
</p>
|
||||
<h2 className="text-xl font-bold">
|
||||
Conteúdo e possibilidades de negócio
|
||||
</h2>
|
||||
<h2 className="text-xl font-bold">Descubra onde investir</h2>
|
||||
{data.content_notice && <p>{data.content_notice}</p>}
|
||||
{!data.articles.length && (
|
||||
<p>
|
||||
@@ -78,22 +74,7 @@ export default function SectorEntry({ data }: { data: SectorEntryData }) {
|
||||
)}
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{data.articles.map((a) => (
|
||||
<article
|
||||
className="min-w-0 rounded border p-4"
|
||||
key={a.href}
|
||||
>
|
||||
{a.image && (
|
||||
<img
|
||||
className="mb-3 h-36 w-full object-cover"
|
||||
src={a.image.href}
|
||||
alt={a.image.alt}
|
||||
/>
|
||||
)}
|
||||
<Link className="font-semibold underline" href={a.href}>
|
||||
{a.title}
|
||||
</Link>
|
||||
<p>{a.scope}</p>
|
||||
</article>
|
||||
<EducationalArticleCard key={a.href} article={a} />
|
||||
))}
|
||||
</div>
|
||||
{data.specialization_href && (
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { SimulationPublishedIva, type PublishedIvaInput } from './simulation-published-iva';
|
||||
import { SimulationMonthlyIva } from './simulation-monthly-iva';
|
||||
import { inheritSimulatorFields } from '@/lib/context-inheritance';
|
||||
import {
|
||||
PublicDocumentContent,
|
||||
type PublicDocument,
|
||||
} from '@/components/public-document-content';
|
||||
import { DossierPrefill } from '@/components/dossier-prefill';
|
||||
import { createOperationKey } from '@/lib/operation-key';
|
||||
import FiscalQaPreview from './fiscal-qa-preview';
|
||||
import { displayValue } from '@/lib/display-value';
|
||||
import {
|
||||
useCallback,
|
||||
@@ -54,6 +58,7 @@ import { SimulationFinancialTools } from '@/components/simulation-financial-tool
|
||||
import { SimulationCashflowPlanner } from '@/components/simulation-cashflow-planner';
|
||||
import { SimulationBusinessPlan } from '@/components/simulation-business-plan';
|
||||
import { SimulationFiscalStatus } from '@/components/simulation-fiscal-status';
|
||||
import { ivaIssueText, type IvaIssue } from '@/lib/iva-diagnostics';
|
||||
import {
|
||||
ProjectMilestonePlanner,
|
||||
ProjectTeamPlanner,
|
||||
@@ -774,7 +779,7 @@ function SearchChoice({
|
||||
|
||||
export function SimulationForm() {
|
||||
const { auth } = usePage<{
|
||||
auth: { user: { email_verified_at?: string | null } | null };
|
||||
auth: { user: { id: number; email_verified_at?: string | null } | null };
|
||||
}>().props;
|
||||
const pageUrl = usePage().url;
|
||||
const draftContext = useMemo(() => {
|
||||
@@ -786,11 +791,12 @@ export function SimulationForm() {
|
||||
};
|
||||
}, [pageUrl]);
|
||||
const contextId = draftContext.context;
|
||||
const storageScope = contextId
|
||||
const projectScope = contextId
|
||||
? `:case:${contextId}`
|
||||
: draftContext.requestedDraft
|
||||
? `:draft:${draftContext.requestedDraft}`
|
||||
: '';
|
||||
const storageScope = `${auth?.user ? `:account:${auth.user.id}` : ':anonymous'}${projectScope}`;
|
||||
const draftKey = legacyDraftKey + storageScope;
|
||||
const draftCreateKey = legacyDraftCreateKey + storageScope;
|
||||
const draftIdKey = legacyDraftIdKey + storageScope;
|
||||
@@ -802,12 +808,14 @@ export function SimulationForm() {
|
||||
const canAssociateDraft = Boolean(auth?.user?.email_verified_at);
|
||||
const [draft, setDraft] = useState<SimulationDraft>({});
|
||||
const [ready, setReady] = useState(false);
|
||||
useEffect(() => { if (ready) setDraft(current => inheritSimulatorFields(current)); }, [draft, ready]);
|
||||
const [contextReview, setContextReview] = useState({
|
||||
revision: 0,
|
||||
reviewed: false,
|
||||
});
|
||||
const [step, setStep] = useState(0);
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const [ivaIssues, setIvaIssues] = useState<IvaIssue[]>([]);
|
||||
const [serverFieldErrors, setServerFieldErrors] = useState<
|
||||
Record<string, string>
|
||||
>({});
|
||||
@@ -834,8 +842,10 @@ export function SimulationForm() {
|
||||
}, [openedTool, step]);
|
||||
function openDraftTool(tool: 'calculation' | 'fiscal') {
|
||||
setToolError('');
|
||||
const index = journeySections.findIndex(
|
||||
(section) => section.id === 'tax',
|
||||
const index = journeySections.findIndex((section) =>
|
||||
tool === 'calculation'
|
||||
? ['financial', 'tax'].includes(section.id)
|
||||
: section.id === 'tax',
|
||||
);
|
||||
if (index < 0) {
|
||||
setToolError(
|
||||
@@ -864,6 +874,10 @@ export function SimulationForm() {
|
||||
const [isCalculating, setIsCalculating] = useState(false);
|
||||
const [hasSaveConflict, setHasSaveConflict] = useState(false);
|
||||
const [result, setResult] = useState<ApiSimulationExecutionV1 | null>(null);
|
||||
const [calculationStale, setCalculationStale] = useState(false);
|
||||
useEffect(() => {
|
||||
if (result) setCalculationStale(false);
|
||||
}, [result]);
|
||||
const arithmeticResult = useMemo(
|
||||
() => readArithmeticSnapshot(result?.result_snapshot),
|
||||
[result],
|
||||
@@ -880,6 +894,7 @@ export function SimulationForm() {
|
||||
() => readVersionedArithmeticReportReference(result),
|
||||
[result],
|
||||
);
|
||||
const isFiscalQaPreview = ['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(arithmeticSnapshot?.scenario_kind));
|
||||
const versionedReportMissing =
|
||||
arithmeticResult.integrity === 'versioned' &&
|
||||
arithmeticReportReference === null;
|
||||
@@ -923,6 +938,7 @@ export function SimulationForm() {
|
||||
const [businessLinkError, setBusinessLinkError] = useState('');
|
||||
const [businessLinkBusy, setBusinessLinkBusy] = useState(false);
|
||||
const [continueLaterSaved, setContinueLaterSaved] = useState(false);
|
||||
const [saveAttempt, setSaveAttempt] = useState(0);
|
||||
const calculationInFlightRef = useRef(false);
|
||||
const draftIdRef = useRef<string | null>(null);
|
||||
const draftEtagRef = useRef<string | null>(null);
|
||||
@@ -1039,6 +1055,13 @@ export function SimulationForm() {
|
||||
: (simulationFieldGroups[current.id] ?? []);
|
||||
const fields = fieldsForCurrent.filter(
|
||||
(field) =>
|
||||
!(
|
||||
current.id === 'tax' &&
|
||||
draft.fiscal_scenario_mode === 'custom_planning' &&
|
||||
!['fiscal_scenario_mode', 'base', 'rate', 'reduction'].includes(
|
||||
field.key,
|
||||
)
|
||||
) &&
|
||||
(!field.showWhen || field.showWhen(draft)) &&
|
||||
!(
|
||||
draft.purpose === 'simulate_only' &&
|
||||
@@ -1311,6 +1334,7 @@ export function SimulationForm() {
|
||||
setSaveStatus('A guardar o rascunho no servidor…');
|
||||
saveQueueRef.current = saveQueueRef.current
|
||||
.then(async () => {
|
||||
if (fingerprint === lastSavedFingerprintRef.current) return;
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
@@ -1465,6 +1489,7 @@ export function SimulationForm() {
|
||||
draftCreateKey,
|
||||
draftIdKey,
|
||||
draftKey,
|
||||
saveAttempt,
|
||||
]);
|
||||
|
||||
async function returnToInvestment() {
|
||||
@@ -1689,6 +1714,7 @@ export function SimulationForm() {
|
||||
async function saveAndContinueLater() {
|
||||
if (!draft.purpose) return;
|
||||
setContinueLaterSaved(false);
|
||||
setSaveAttempt((attempt) => attempt + 1);
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 550));
|
||||
await saveQueueRef.current;
|
||||
const target = canSavePersonalData ? draft : getAutosaveSnapshot(draft);
|
||||
@@ -1916,6 +1942,7 @@ export function SimulationForm() {
|
||||
) {
|
||||
if (!ready) return;
|
||||
setContinueLaterSaved(false);
|
||||
setIvaIssues((previous) => previous.filter((issue) => issue.field !== key));
|
||||
setServerFieldErrors((previous) => {
|
||||
const next = { ...previous };
|
||||
delete next[key];
|
||||
@@ -1943,7 +1970,8 @@ export function SimulationForm() {
|
||||
return next;
|
||||
});
|
||||
if (key === 'purpose') setStep(0);
|
||||
if (key !== 'purpose') setResult(null);
|
||||
if (result) setCalculationStale(true);
|
||||
setResult(null);
|
||||
}
|
||||
|
||||
async function clearDraft() {
|
||||
@@ -2139,15 +2167,15 @@ export function SimulationForm() {
|
||||
);
|
||||
}
|
||||
|
||||
async function runSimulation() {
|
||||
async function runSimulation(publishedInput?: PublishedIvaInput) {
|
||||
if (calculationInFlightRef.current) return;
|
||||
const validation = validateSimulationDraft(draft, 'tax');
|
||||
if (draft.fiscal_scenario_mode === 'custom_planning') {
|
||||
const validation = publishedInput ? {} : validateSimulationDraft(draft, 'tax');
|
||||
if (!publishedInput && draft.fiscal_scenario_mode === 'custom_planning') {
|
||||
Object.assign(validation, validateCustomFiscalPlanning(draft));
|
||||
}
|
||||
setErrors(validation);
|
||||
setServerError('');
|
||||
if (draft.fiscal_scenario_mode !== 'custom_planning') return;
|
||||
if (!publishedInput && draft.fiscal_scenario_mode !== 'custom_planning') return;
|
||||
if (Object.keys(validation).length) {
|
||||
document
|
||||
.getElementById(`field-${Object.keys(validation)[0]}`)
|
||||
@@ -2158,7 +2186,7 @@ export function SimulationForm() {
|
||||
setIsCalculating(true);
|
||||
try {
|
||||
const xsrf = csrfToken();
|
||||
const body = {
|
||||
const body = publishedInput ?? {
|
||||
base: draft.base,
|
||||
rate: draft.rate,
|
||||
reduction: draft.reduction,
|
||||
@@ -2266,7 +2294,7 @@ export function SimulationForm() {
|
||||
}
|
||||
}
|
||||
if (
|
||||
JSON.stringify({
|
||||
publishedInput || JSON.stringify({
|
||||
base: draft.base,
|
||||
rate: draft.rate,
|
||||
reduction: draft.reduction,
|
||||
@@ -2274,13 +2302,27 @@ export function SimulationForm() {
|
||||
)
|
||||
setResult(simulation ?? null);
|
||||
if (canSavePersonalData) void loadSimulationHistory();
|
||||
sessionStorage.removeItem(idempotencyStorageKey);
|
||||
if (simulation?.status === 'completed') {
|
||||
setSaveStatus(
|
||||
'Cálculo concluído. Reveja o resultado e os pressupostos.',
|
||||
);
|
||||
} else if (
|
||||
simulation?.status === 'failed' ||
|
||||
simulation?.status === 'cancelled'
|
||||
) {
|
||||
setSaveStatus(
|
||||
'O cálculo não foi concluído. Os dados introduzidos foram preservados.',
|
||||
);
|
||||
}
|
||||
if (!publishedInput) sessionStorage.removeItem(idempotencyStorageKey);
|
||||
return simulation ?? null;
|
||||
} catch (cause) {
|
||||
setServerError(
|
||||
cause instanceof Error
|
||||
? cause.message
|
||||
: 'Erro de ligação. Tente novamente.',
|
||||
);
|
||||
return null;
|
||||
} finally {
|
||||
calculationInFlightRef.current = false;
|
||||
setIsCalculating(false);
|
||||
@@ -2396,6 +2438,16 @@ export function SimulationForm() {
|
||||
aria-labelledby="simulator-step-title"
|
||||
aria-busy={!ready}
|
||||
>
|
||||
{calculationStale && (
|
||||
<p
|
||||
role="status"
|
||||
className="rounded border border-amber-700 bg-amber-50 p-3 text-amber-950"
|
||||
>
|
||||
As entradas foram alteradas. Calcule novamente antes de
|
||||
guardar ou exportar um novo resultado. Os resultados
|
||||
anteriores mantêm-se no histórico.
|
||||
</p>
|
||||
)}
|
||||
{!ready && (
|
||||
<p role="status">
|
||||
A recuperar o rascunho. Aguarde para editar.
|
||||
@@ -3242,6 +3294,7 @@ export function SimulationForm() {
|
||||
: Boolean(field.required);
|
||||
const fieldError =
|
||||
serverFieldErrors[field.key] ?? errors[field.key];
|
||||
const ivaNotes = ivaIssues.filter((issue) => issue.field === field.key).map((issue, index) => <p key={index} role="note" className="mt-1 text-sm text-amber-950">{ivaIssueText(issue)}</p>);
|
||||
if (field.options) {
|
||||
const parentKey =
|
||||
field.key === 'district_id'
|
||||
@@ -3288,6 +3341,7 @@ export function SimulationForm() {
|
||||
}
|
||||
/>
|
||||
<FieldGuidance fieldKey={field.key} />
|
||||
{ivaNotes}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3362,6 +3416,7 @@ export function SimulationForm() {
|
||||
{fieldError}
|
||||
</p>
|
||||
)}
|
||||
{ivaNotes}
|
||||
{field.key === 'reduction' && (
|
||||
<p className="mt-1 text-sm text-slate-600">
|
||||
Hipótese sua, não representa um
|
||||
@@ -3414,11 +3469,16 @@ export function SimulationForm() {
|
||||
{(current.id === 'financial' ||
|
||||
(draft.purpose === 'simulate_only' &&
|
||||
current.id === 'tax')) && (
|
||||
<SimulationCashflowPlanner
|
||||
<details className="rounded border p-3"><summary>Cenário personalizado de tesouraria — imposto manual, separado do IVA calculado</summary><SimulationCashflowPlanner
|
||||
snapshot={String(draft.cashflow_snapshot ?? '')}
|
||||
onSnapshotChange={(value) =>
|
||||
change('cashflow_snapshot', value)
|
||||
}
|
||||
currency={String(draft.currency ?? '')}
|
||||
onCurrencyChange={(value) => change('currency', value)}
|
||||
/>
|
||||
/></details>
|
||||
)}
|
||||
{(current.id === 'financial' || current.id === 'tax') && <SimulationMonthlyIva draft={draft} busy={isCalculating} onCalculate={runSimulation} onChange={change} />}
|
||||
{current.id === 'financial' &&
|
||||
(draft.detail_level === 'advanced' ||
|
||||
draft.journey_type === 'small_business') &&
|
||||
@@ -3554,6 +3614,7 @@ export function SimulationForm() {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{current.id === 'tax' && <SimulationPublishedIva draft={draft} onChange={change} busy={isCalculating} onCalculate={runSimulation} />}
|
||||
{current.id === 'tax' && (
|
||||
<div
|
||||
id="draft-tool-fiscal"
|
||||
@@ -3561,6 +3622,12 @@ export function SimulationForm() {
|
||||
aria-label="Consulta de parâmetros fiscais"
|
||||
>
|
||||
<SimulationFiscalStatus
|
||||
onDiagnostics={setIvaIssues}
|
||||
onCorrect={(key) => {
|
||||
const target = journeySections.findIndex((section) => (simulationFieldGroups[section.id] ?? []).some((field) => field.key === key));
|
||||
if (target >= 0) setStep(target);
|
||||
window.requestAnimationFrame(() => document.getElementById(`field-${key}`)?.focus());
|
||||
}}
|
||||
salaryView={String(draft.irps_salary_view ?? '')}
|
||||
scenarioMode={
|
||||
openedTool === 'fiscal'
|
||||
@@ -3570,7 +3637,7 @@ export function SimulationForm() {
|
||||
component={String(draft.fiscal_component ?? '')}
|
||||
periodStart={getFiscalPeriodRange(draft)?.start}
|
||||
periodEnd={getFiscalPeriodRange(draft)?.end}
|
||||
context={Object.fromEntries(
|
||||
context={{ ...Object.fromEntries(
|
||||
[
|
||||
'fiscal_periodicity',
|
||||
'fiscal_operation',
|
||||
@@ -3587,7 +3654,7 @@ export function SimulationForm() {
|
||||
? [[key, draft[key] as string]]
|
||||
: [],
|
||||
),
|
||||
)}
|
||||
), ...(typeof draft.fiscal_currency === 'string' && draft.fiscal_currency !== '' ? {currency: draft.fiscal_currency} : {}) }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -3651,43 +3718,56 @@ export function SimulationForm() {
|
||||
O período e as respostas servem para encaminhar uma
|
||||
exploração informativa. A residência é auto-indicada ou
|
||||
fica por determinar; não é inferida por nacionalidade ou
|
||||
dias de presença. Regras de 2026 continuam pendentes de
|
||||
validação e este formulário não determina cumprimento
|
||||
dias de presença. Cada cálculo identifica a regra, versão, cobertura e
|
||||
limitações; este formulário não determina cumprimento
|
||||
fiscal. Um crédito estimado de IVA não significa pedido
|
||||
aprovado nem dinheiro disponível. Não carregue
|
||||
documentos nesta simulação exploratória.
|
||||
</p>
|
||||
)}
|
||||
{current.id === 'tax' && (
|
||||
{(current.id === 'tax' || result) && (
|
||||
<div className="space-y-3 border-t pt-4">
|
||||
<h4 className="font-semibold">
|
||||
Cenário personalizado de planeamento
|
||||
{isFiscalQaPreview ? (arithmeticSnapshot?.scenario_kind === 'fiscal_rule_published' ? 'IVA pela versão publicada seleccionada' : 'Pré-visualização fiscal QA seleccionada') : current.id === 'tax'
|
||||
? 'Cenário personalizado de planeamento'
|
||||
: 'Resultado histórico seleccionado'}
|
||||
</h4>
|
||||
{draft.fiscal_scenario_mode === 'custom_planning' && (
|
||||
<p
|
||||
role="note"
|
||||
className="rounded bg-slate-50 p-3 text-sm"
|
||||
>
|
||||
Os valores abaixo são pressupostos que
|
||||
introduziu para planeamento. O resultado é uma
|
||||
operação percentual genérica, não uma taxa
|
||||
legal, imposto devido nem confirmação de
|
||||
elegibilidade.
|
||||
{current.id !== 'tax' && result && (
|
||||
<p>
|
||||
Este resultado conserva os pressupostos e a
|
||||
versão da execução seleccionada. Não substitui
|
||||
as entradas deste dossier.
|
||||
</p>
|
||||
)}
|
||||
{draft.fiscal_scenario_mode === 'custom_planning' && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={isCalculating}
|
||||
aria-busy={isCalculating}
|
||||
className="rounded-lg bg-emerald-800 px-4 py-3 font-semibold text-white focus-visible:outline-2 disabled:cursor-wait disabled:opacity-60"
|
||||
onClick={() => void runSimulation()}
|
||||
>
|
||||
{isCalculating
|
||||
? 'A calcular…'
|
||||
: 'Validar e calcular no servidor'}
|
||||
</button>
|
||||
)}
|
||||
{current.id === 'tax' &&
|
||||
draft.fiscal_scenario_mode ===
|
||||
'custom_planning' && (
|
||||
<p
|
||||
role="note"
|
||||
className="rounded bg-slate-50 p-3 text-sm"
|
||||
>
|
||||
Os valores abaixo são pressupostos que
|
||||
introduziu para planeamento. O resultado é
|
||||
uma operação percentual genérica, não uma
|
||||
taxa legal, imposto devido nem confirmação
|
||||
de elegibilidade.
|
||||
</p>
|
||||
)}
|
||||
{current.id === 'tax' &&
|
||||
draft.fiscal_scenario_mode ===
|
||||
'custom_planning' && (
|
||||
<button
|
||||
type="button"
|
||||
disabled={isCalculating}
|
||||
aria-busy={isCalculating}
|
||||
className="rounded-lg bg-emerald-800 px-4 py-3 font-semibold text-white focus-visible:outline-2 disabled:cursor-wait disabled:opacity-60"
|
||||
onClick={() => void runSimulation()}
|
||||
>
|
||||
{isCalculating
|
||||
? 'A calcular…'
|
||||
: 'Validar e calcular no servidor'}
|
||||
</button>
|
||||
)}
|
||||
{serverError && (
|
||||
<p role="alert" className="text-red-800">
|
||||
{serverError}
|
||||
@@ -3712,7 +3792,7 @@ export function SimulationForm() {
|
||||
submetido.
|
||||
</p>
|
||||
)}
|
||||
{result.status === 'completed' && (
|
||||
{result.status === 'completed' && !isFiscalQaPreview && (
|
||||
<p>
|
||||
Resultado aritmético indicativo:{' '}
|
||||
<strong className="break-all">
|
||||
@@ -3749,9 +3829,10 @@ export function SimulationForm() {
|
||||
taxas legais, elegibilidade ou obrigação
|
||||
fiscal.
|
||||
</p>
|
||||
{isFiscalQaPreview && <FiscalQaPreview snapshot={result.result_snapshot} />}
|
||||
{result.status === 'completed' && (
|
||||
<div className="mt-3 space-y-2 border-t pt-3">
|
||||
{arithmeticResult.integrity !==
|
||||
{!isFiscalQaPreview && arithmeticResult.integrity !==
|
||||
'invalid' && (
|
||||
<details className="rounded border p-3">
|
||||
<summary className="cursor-pointer font-semibold focus-visible:outline-2">
|
||||
@@ -4122,8 +4203,7 @@ export function SimulationForm() {
|
||||
'não identificada',
|
||||
'não identificada',
|
||||
)}{' '}
|
||||
· resultado
|
||||
aritmético{' '}
|
||||
· {['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(source.result_snapshot.scenario_kind)) ? 'IVA calculado exacto' : 'resultado aritmético'}{' '}
|
||||
{displayScalar(
|
||||
sourceResult.value ??
|
||||
(sourceResult.integrity ===
|
||||
@@ -4362,7 +4442,9 @@ export function SimulationForm() {
|
||||
e, se já escolhido, o cálculo percentual
|
||||
personalizado. Consultar parâmetros fiscais
|
||||
apresenta fontes e lacunas; não determina imposto
|
||||
devido.
|
||||
devido. Na etapa Operação fiscal encontra também
|
||||
o cálculo de IVA geral sobre base líquida, quando
|
||||
activado, com âmbito e versão identificados.
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { modeloAFields, modeloAPurchaseReferences, modeloAReference } from '@/lib/iva-modelo-a';
|
||||
|
||||
export function SimulationModeloA({draft}: {draft:Record<string,unknown>}) {
|
||||
let purchases: ReturnType<typeof modeloAPurchaseReferences> = [];
|
||||
let error = '';
|
||||
try { purchases = modeloAPurchaseReferences(draft); }
|
||||
catch (cause) { error = cause instanceof Error ? cause.message : 'Não foi possível consultar as aquisições projectadas.'; }
|
||||
return <section aria-label="Correspondência com o Modelo A" className="space-y-3 rounded border p-3">
|
||||
<h4 className="font-semibold">Modelo A — correspondência da simulação</h4>
|
||||
<p>{modeloAReference.status}</p>
|
||||
<p>Os campos 14–18 do apuramento permanecem indeterminados. O IVA das vendas não é imposto final a pagar. Os dados deste percurso são projecções, separados de facturas e declarações efectivas.</p>
|
||||
<details><summary>Aquisições já projectadas — sem dedução presumida</summary>
|
||||
<p>Reutilizam-se os valores das compras/encomendas para existências do plano. O mês da encomenda não prova a data da factura, recepção ou pagamento. Valores declarados ainda sem indicação de base líquida/IVA incluído não são tratados como base tributável. IVA suportado e IVA dedutível continuam por determinar; zero de compras não prova inexistência de outras aquisições.</p>
|
||||
{error && <p role="alert">{error}</p>}
|
||||
{!purchases.length && !error && <p>Sem agenda de compras recuperável do plano empresarial.</p>}
|
||||
{!!purchases.length && <div className="overflow-x-auto"><table className="min-w-full text-sm"><caption>Pressupostos actuais de compras; sem resultado fiscal guardado</caption><thead><tr>{['Mês do plano','Compras declaradas','Sem IVA','IVA suportado','Com IVA','IVA dedutível'].map((name)=><th className="p-2 text-left" key={name}>{name}</th>)}</tr></thead><tbody>{purchases.map((row)=><tr key={row.month}><th className="p-2" scope="row">{row.month}</th><td>{row.declared ?? 'Sem informação'}</td><td>Por classificar</td><td>Indeterminado</td><td>Por classificar</td><td>Indeterminado</td></tr>)}</tbody></table></div>}
|
||||
</details>
|
||||
<details><summary>Ver campos, cálculos, fundamentos e cobertura do Modelo A</summary>
|
||||
<p>{modeloAReference.file}. {modeloAReference.edition} SHA256: {modeloAReference.sha256}.</p>
|
||||
<p>O exemplar contém divergências de numeração/remissões, registadas no confronto documental. Não foram corrigidas no original. A existência de um campo não activa uma regra fiscal.</p>
|
||||
<div className="overflow-x-auto"><table className="min-w-full text-sm"><caption>Campo do Modelo A → finalidade → origem → cálculo → fundamento → cobertura</caption><thead><tr>{['Campo','Finalidade','Origem dos dados','Cálculo','Fundamento','Estado'].map((name)=><th className="p-2 text-left" key={name}>{name}</th>)}</tr></thead><tbody>{modeloAFields.map((field)=><tr key={field.field}>{[field.field,field.purpose,field.origin,field.calculation,field.basis,field.status].map((value,index)=><td className="border-t p-2 align-top" key={index}>{value}</td>)}</tr>)}</tbody></table></div>
|
||||
</details>
|
||||
</section>;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import { InvestmentContextAssessment } from './investment-context-assessment';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { monthlyIvaInput, monthlyIvaKey, monthlySalesFromDraft, readMonthlyIvaRecord, readMonthlyIvaSettings, monthlyIvaCashTreatment, type MonthlyIvaInput, type MonthlyIvaSettings } from '@/lib/monthly-iva';
|
||||
import { fiscalQaRows } from '@/lib/fiscal-qa-preview';
|
||||
import { isApiSimulationReadResponseV1, type ApiSimulationExecutionV1 } from '@/lib/simulation-contract';
|
||||
import { SimulationModeloA } from './simulation-modelo-a';
|
||||
|
||||
export function SimulationMonthlyIva({draft, busy, onCalculate, onChange}: {
|
||||
draft: Record<string, unknown>; busy: boolean;
|
||||
onCalculate: (input: MonthlyIvaInput) => Promise<ApiSimulationExecutionV1 | null | undefined>;
|
||||
onChange: (field: string, value: string) => void;
|
||||
}) {
|
||||
const settings = readMonthlyIvaSettings(String(draft.monthly_iva_settings ?? ''));
|
||||
const record = readMonthlyIvaRecord(String(draft.monthly_iva_snapshot ?? ''));
|
||||
const [retry, setRetry] = useState(0);
|
||||
const [error, setError] = useState('');
|
||||
const [available, setAvailable] = useState<boolean | null>(null);
|
||||
const attempt = useRef('');
|
||||
const callbacks = useRef({onCalculate, onChange}); callbacks.current = {onCalculate, onChange};
|
||||
let input: MonthlyIvaInput | null = null; let inputError = ''; let source = '';
|
||||
try { source = monthlySalesFromDraft(draft).source; input = monthlyIvaInput(draft, settings); }
|
||||
catch (cause) { inputError = cause instanceof Error ? cause.message : 'Reveja a projecção de vendas.'; }
|
||||
const key = input ? monthlyIvaKey(input) : '';
|
||||
const current = !!(input && record && monthlyIvaKey(record.input) === key);
|
||||
const completed = current && record?.execution.status === 'completed';
|
||||
const saveSettings = (patch: Partial<MonthlyIvaSettings>) => onChange('monthly_iva_settings', JSON.stringify({...settings, ...patch}));
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
fetch('/api/v1/fiscal-consumer', {credentials: 'same-origin', headers: {Accept:'application/json'}}).then(async (response) => {
|
||||
if (!response.ok) throw new Error(); const payload = await response.json(); if (active) setAvailable(payload?.data?.enabled === true);
|
||||
}).catch(() => {if (active) setError('Não foi possível consultar a disponibilidade da regra. As vendas foram preservadas.');});
|
||||
return () => { active = false; };
|
||||
}, [retry]);
|
||||
useEffect(() => {
|
||||
if (!settings.auto || !input || input.currency !== 'MZN' || busy || !available || completed || attempt.current === key) return;
|
||||
const request = input;
|
||||
const pending = current && record && ['queued','running'].includes(record.execution.status) ? record.execution.id : null;
|
||||
const timer = window.setTimeout(() => {
|
||||
attempt.current = key; setError('');
|
||||
void (async () => {
|
||||
try {
|
||||
let execution;
|
||||
if (pending) {
|
||||
const response = await fetch(`/api/v1/simulations/${pending}`, {credentials:'same-origin', headers:{Accept:'application/json'}});
|
||||
const value: unknown = await response.json();
|
||||
if (!response.ok || !isApiSimulationReadResponseV1(value)) throw new Error('Não foi possível recuperar a execução mensal guardada. Não foi criada uma execução alternativa.');
|
||||
execution = value.data;
|
||||
} else execution = await callbacks.current.onCalculate(request);
|
||||
if (!execution) throw new Error('O cálculo mensal não foi concluído. As vendas e a versão anterior foram preservadas; tente novamente.');
|
||||
callbacks.current.onChange('monthly_iva_snapshot', JSON.stringify({input:request, execution}));
|
||||
} catch (cause) {setError(cause instanceof Error ? cause.message : 'Não foi possível calcular o IVA mensal.');}
|
||||
})();
|
||||
}, 700);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [key, settings.auto, busy, available, completed, retry]);
|
||||
|
||||
const snapshot = record?.execution.result_snapshot;
|
||||
const projection = snapshot?.monthly_projection as {rows?: Array<{date:string; sales_declared:string|null; net_sales:string|null; rate:string|null; vat:string|null; gross_sales:string|null; status:string}>} | undefined;
|
||||
return <section className="space-y-3 rounded border border-emerald-700 p-4" aria-label="IVA mensal das vendas projectadas">
|
||||
<InvestmentContextAssessment raw={draft.context_applicability_snapshot} />
|
||||
<h3 className="font-semibold">IVA mensal das vendas projectadas</h3>
|
||||
<p>{source}</p><p>Declare se os preços projectados são sem IVA ou com IVA incluído. Nenhuma taxa é fixada neste formulário; o servidor selecciona a versão publicada.</p>
|
||||
<label className="block">Primeiro mês da projecção<input type="month" value={settings.start} min="2026-01" max="2026-12" onChange={(event) => saveSettings({start:event.target.value})} className="block rounded border p-2" /></label>
|
||||
<label className="block">Tratamento do IVA nos preços projectados<select value={settings.basis} onChange={(event) => saveSettings({basis:event.target.value as MonthlyIvaSettings['basis']})} className="block rounded border p-2"><option value="unknown">Por indicar</option><option value="net">Vendas sem IVA (base líquida)</option><option value="included">Vendas com IVA incluído</option></select></label>
|
||||
<label className="block"><input type="checkbox" checked={settings.declared} onChange={(event) => saveSettings({declared:event.target.checked})} /> Declaro que as vendas projectadas são tributáveis à taxa geral interna e não incluem as operações excluídas deste lote.</label>
|
||||
<p className="text-sm">Sem importações, taxas reduzidas, isenções, regimes especiais, deduções, reembolsos ou apuramento final. Valores com IVA incluído permanecem indeterminados neste lote; não se aplica a taxa ao preço bruto.</p>
|
||||
<p className="text-sm">{monthlyIvaCashTreatment}</p>
|
||||
{!!draft.currency && draft.currency !== "MZN" && <p role="alert">Moeda do projecto: {String(draft.currency)}. A regra disponível aceita apenas MZN. As vendas e a moeda são preservadas, sem conversão; o IVA mensal não pode ser determinado por esta versão.</p>}
|
||||
{inputError && <p role="alert">{inputError}</p>}
|
||||
{available === null && !error && <p role="status">A consultar a disponibilidade da versão publicada…</p>}
|
||||
{available === false && <p role="status">A versão publicada de IVA está indisponível neste ambiente para novas execuções. Os pressupostos e históricos mantêm-se.</p>}
|
||||
{record && !current && <p role="alert">As vendas ou os pressupostos mudaram. Os resultados abaixo são históricos e estão desactualizados; não são usados como resultados da projecção actual.</p>}
|
||||
{settings.auto && !completed && current && <p role="status">Execução mensal guardada: {record?.execution.status === 'queued' ? 'em fila' : record?.execution.status === 'running' ? 'em processamento' : 'não concluída'}. Pode voltar a consultar sem duplicar.</p>}
|
||||
{busy && <p role="status">A actualizar os resultados dependentes das vendas…</p>}
|
||||
{error && <p role="alert">{error}</p>}
|
||||
<button type="button" disabled={busy || !available || !input || input.currency !== 'MZN'} onClick={() => {attempt.current=''; setRetry((value) => value+1); saveSettings({auto:true});}} className="rounded bg-emerald-800 p-2 text-white disabled:opacity-50">Calcular / actualizar IVA mensal e guardar versão</button>
|
||||
{settings.auto && <button type="button" className="ml-3 underline" onClick={() => saveSettings({auto:false})}>Pausar actualização automática</button>}
|
||||
{input && <p className="text-sm">{input.monthly_sales.length} meses; moeda {input.currency || 'por indicar'}. Campo vazio significa informação em falta; zero declarado permanece zero. Depois da primeira execução, alterações das vendas actualizam o cálculo enquanto a actualização automática estiver ligada.</p>}
|
||||
{projection?.rows && <div className="overflow-x-auto" tabIndex={0}><table className="min-w-full text-sm"><caption>IVA mensal — {current ? 'versão correspondente às entradas' : 'versão histórica'}</caption><thead><tr>{['Mês','Vendas declaradas','Vendas tributáveis sem IVA','Taxa aplicada (%)','IVA calculado','Total com IVA','Estado'].map((label) => <th key={label} className="p-2 text-left">{label}</th>)}</tr></thead><tbody>{projection.rows.map((row) => <tr key={row.date}><th scope="row" className="p-2">{row.date.slice(0,7)}</th><td>{row.sales_declared ?? 'Sem informação'}</td><td>{row.net_sales ?? 'Indeterminado'}</td><td>{row.rate ?? 'Indeterminada'}</td><td>{row.vat ?? 'Indeterminado'}</td><td>{row.gross_sales ?? 'Indeterminado'}</td><td>{{calculated:'Calculado',missing_sales:'Sem informação de vendas',unsupported_basis:'Base líquida não determinada',indeterminate:'Enquadramento ou regra indeterminados'}[row.status] ?? 'Indeterminado'}</td></tr>)}</tbody></table></div>}
|
||||
{snapshot && <details><summary>Ver fundamento e relatório da versão guardada</summary><dl>{fiscalQaRows(snapshot).map(([label,value], index) => <div key={index}><dt className="font-semibold">{label}</dt><dd>{value}</dd></div>)}</dl></details>}
|
||||
<SimulationModeloA draft={draft} />
|
||||
</section>;
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { MonthlyIvaInput } from '@/lib/monthly-iva';
|
||||
|
||||
export type PublishedIvaInput = MonthlyIvaInput | {
|
||||
scenario_kind: 'fiscal_rule_published';
|
||||
base: string;
|
||||
currency: 'MZN';
|
||||
reference_date: string;
|
||||
operation: 'internal_general_net';
|
||||
scope_declared: boolean;
|
||||
};
|
||||
|
||||
export function SimulationPublishedIva({ busy, onCalculate, draft, onChange }: {
|
||||
draft: Record<string, unknown>;
|
||||
onChange: (field: string, value: string) => void;
|
||||
busy: boolean;
|
||||
onCalculate: (input: PublishedIvaInput) => Promise<unknown>;
|
||||
}) {
|
||||
const [available, setAvailable] = useState(false);
|
||||
const [message, setMessage] = useState('A consultar disponibilidade do cálculo de IVA…');
|
||||
const base = String(draft.published_iva_base ?? '');
|
||||
const date = String(draft.published_iva_date ?? '');
|
||||
const currency = String(draft.published_iva_currency ?? '');
|
||||
const declared = draft.published_iva_scope === 'yes';
|
||||
const setBase = (value: string) => onChange('published_iva_base', value);
|
||||
const setDate = (value: string) => onChange('published_iva_date', value);
|
||||
const setCurrency = (value: string) => onChange('published_iva_currency', value);
|
||||
const setDeclared = (value: boolean) => onChange('published_iva_scope', value ? 'yes' : 'no');
|
||||
const [error, setError] = useState('');
|
||||
useEffect(() => {
|
||||
let active = true;
|
||||
fetch('/api/v1/fiscal-consumer', { credentials: 'same-origin', headers: { Accept: 'application/json' } })
|
||||
.then(async (response) => {
|
||||
if (!response.ok) throw new Error();
|
||||
const payload = await response.json();
|
||||
if (!active) return;
|
||||
setAvailable(payload?.data?.enabled === true);
|
||||
setMessage(typeof payload?.data?.message === 'string' ? payload.data.message : 'Cálculo de IVA indisponível.');
|
||||
}).catch(() => { if (active) setMessage('Não foi possível consultar a disponibilidade do IVA. As entradas e históricos foram preservados.'); });
|
||||
return () => { active = false; };
|
||||
}, []);
|
||||
|
||||
async function calculate() {
|
||||
if (!/^\d+(?:\.\d+)?$/.test(base) || base.length > 40 || !date || currency !== 'MZN') {
|
||||
setError('Indique uma base líquida decimal válida, a moeda MZN e a data de exigibilidade. As entradas foram preservadas.');
|
||||
return;
|
||||
}
|
||||
setError('');
|
||||
await onCalculate({ scenario_kind: 'fiscal_rule_published', base, currency: 'MZN', reference_date: date,
|
||||
operation: 'internal_general_net', scope_declared: declared });
|
||||
// Refresh after admission/refusal so a suspension is reflected without discarding inputs.
|
||||
try {
|
||||
const response = await fetch('/api/v1/fiscal-consumer', { credentials: 'same-origin', headers: { Accept: 'application/json' } });
|
||||
if (!response.ok) return;
|
||||
const payload = await response.json();
|
||||
setAvailable(payload?.data?.enabled === true);
|
||||
if (typeof payload?.data?.message === 'string') setMessage(payload.data.message);
|
||||
} catch { /* The execution flow already preserves and reports connection errors. */ }
|
||||
}
|
||||
|
||||
return <section id="published-iva" tabIndex={-1} aria-label="IVA — taxa geral" className="space-y-3 rounded border border-emerald-700 p-4">
|
||||
<h4 className="font-semibold">IVA — taxa geral</h4>
|
||||
<p role="status">{message}</p>
|
||||
<p className="text-sm">Cobertura: exigibilidade em 2026, operação interna à taxa geral. A base tributável é indicada por si; não é copiada do orçamento. Sem isenções, taxas reduzidas, bases especiais, importações, deduções, declaração ou pagamento. O cálculo não valida o enquadramento jurídico.</p>
|
||||
<fieldset disabled={busy} className="space-y-3">
|
||||
<label className="block">Base tributável líquida (decimal, com ponto)<input aria-label="Base tributável líquida" inputMode="decimal" value={base} onChange={(e) => setBase(e.target.value)} className="block rounded border p-2" /></label>
|
||||
<label className="block">Moeda da base<select aria-label="Moeda da base IVA" value={currency} onChange={(e) => setCurrency(e.target.value)} className="block rounded border p-2"><option value="">Seleccione</option><option value="MZN">MZN</option>{currency && currency !== "MZN" && <option value={currency}>{currency} — moeda declarada</option>}</select></label>
|
||||
<label className="block">Data de exigibilidade declarada<input aria-label="Data de exigibilidade IVA" type="date" min="2026-01-01" max="2026-12-31" value={date} onChange={(e) => setDate(e.target.value)} className="block rounded border p-2" /></label>
|
||||
<label className="block"><input type="checkbox" checked={declared} onChange={(e) => setDeclared(e.target.checked)} /> Declaro operação interna à taxa geral, base líquida já apurada e ausência das excepções acima.</label>
|
||||
<p className="text-sm">Sem esta declaração, o resultado fica indeterminado. O valor calculado conserva a precisão decimal; não é saldo a pagar.</p>
|
||||
<button type="button" disabled={!available || busy} onClick={() => void calculate()} className="rounded bg-emerald-800 px-4 py-2 text-white disabled:opacity-50">{busy ? 'A calcular…' : 'Calcular IVA e gravar'}</button>
|
||||
</fieldset>
|
||||
{currency && currency !== "MZN" && <p role="alert">Moeda: {currency}. A versão disponível aceita apenas MZN. O valor declarado foi preservado; não foi feita conversão. Este cálculo permanece indisponível para esta moeda.</p>}
|
||||
{error && <p role="alert">{error}</p>}
|
||||
</section>;
|
||||
}
|
||||
@@ -10,14 +10,16 @@ const navigation = [
|
||||
|
||||
export function TempoFooter() {
|
||||
const { t, language } = useTempoUi();
|
||||
const { portalInstitutionalContactEmail } = usePage<{ portalInstitutionalContactEmail?: string | null }>().props;
|
||||
const { portalInstitutionalContactEmail } = usePage<{
|
||||
portalInstitutionalContactEmail?: string | null;
|
||||
}>().props;
|
||||
return (
|
||||
<footer
|
||||
className="tempo-footer border-t border-white/5"
|
||||
lang={language}
|
||||
>
|
||||
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-12 px-6 py-10 md:grid-cols-2 lg:grid-cols-5 lg:gap-16 lg:px-12">
|
||||
<div className="space-y-8">
|
||||
<div className="mx-auto grid max-w-7xl grid-cols-1 gap-6 px-6 py-6 md:grid-cols-2 lg:grid-cols-5 lg:gap-8 lg:px-12">
|
||||
<div className="space-y-4">
|
||||
<Link
|
||||
href="/"
|
||||
aria-label="InvesteMoz — início"
|
||||
@@ -37,9 +39,9 @@ export function TempoFooter() {
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<h2 className="tempo-footer-heading">{t('Contactos')}</h2>
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
['+258868010880', '+258 86 801 0880'],
|
||||
['+258833219644', '+258 83 321 9644'],
|
||||
@@ -58,21 +60,26 @@ export function TempoFooter() {
|
||||
</a>
|
||||
))}
|
||||
<a
|
||||
href={portalInstitutionalContactEmail ? `mailto:${portalInstitutionalContactEmail}` : '/apoio'}
|
||||
href={
|
||||
portalInstitutionalContactEmail
|
||||
? `mailto:${portalInstitutionalContactEmail}`
|
||||
: '/apoio'
|
||||
}
|
||||
className="tempo-contact-link"
|
||||
>
|
||||
<span className="tempo-contact-icon">
|
||||
<Mail size={16} aria-hidden="true" />
|
||||
</span>
|
||||
<strong className="min-w-0 text-sm font-medium [overflow-wrap:anywhere]">
|
||||
{portalInstitutionalContactEmail ?? t('Apoio ao investidor')}
|
||||
<strong className="min-w-0 text-[13px] leading-5 font-medium [overflow-wrap:anywhere]">
|
||||
{portalInstitutionalContactEmail ??
|
||||
t('Apoio ao investidor')}
|
||||
</strong>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<nav aria-label="Navegação do rodapé" className="space-y-8">
|
||||
<nav aria-label="Navegação do rodapé" className="space-y-4">
|
||||
<h2 className="tempo-footer-heading">{t('Navegação')}</h2>
|
||||
<ul className="space-y-4">
|
||||
<ul className="space-y-2">
|
||||
{navigation.map(([href, label]) => (
|
||||
<li key={href}>
|
||||
<Link href={href} className="tempo-footer-link">
|
||||
@@ -86,9 +93,9 @@ export function TempoFooter() {
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<nav aria-label="Informação legal" className="space-y-8">
|
||||
<nav aria-label="Informação legal" className="space-y-4">
|
||||
<h2 className="tempo-footer-heading">Legal</h2>
|
||||
<ul className="space-y-4">
|
||||
<ul className="space-y-2">
|
||||
{[
|
||||
[
|
||||
'politica-de-privacidade',
|
||||
@@ -111,7 +118,7 @@ export function TempoFooter() {
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<h2 className="tempo-footer-heading">Social</h2>
|
||||
<p className="text-xs leading-relaxed font-medium text-slate-300">
|
||||
{t(
|
||||
@@ -121,8 +128,8 @@ export function TempoFooter() {
|
||||
<TempoSocialLinks footer />
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t border-white/10 py-2">
|
||||
<div className="mx-auto flex max-w-7xl flex-col items-center justify-between gap-6 px-6 text-center text-[10px] tracking-[0.2em] text-slate-400 md:flex-row md:text-left md:text-xs">
|
||||
<div className="border-t border-white/10 py-1.5">
|
||||
<div className="mx-auto flex max-w-7xl flex-col items-center justify-between gap-1 px-6 text-center text-[10px] tracking-[0.1em] text-slate-400 md:flex-row md:text-left md:text-[11px]">
|
||||
<p>
|
||||
© {new Date().getFullYear()}{' '}
|
||||
<span className="text-[#ed1c24]">Tempo</span>.{' '}
|
||||
|
||||
@@ -56,12 +56,14 @@ export default function TwoFactorRecoveryCodes({
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex gap-3">
|
||||
<LockKeyhole className="size-4" aria-hidden="true" />
|
||||
2FA recovery codes
|
||||
<LockKeyhole className="size-4" aria-hidden="true" />{' '}
|
||||
Códigos de recuperação{' '}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Recovery codes let you regain access if you lose your 2FA
|
||||
device. Store them in a secure password manager.
|
||||
{' '}
|
||||
Os códigos de recuperação permitem recuperar o acesso se
|
||||
perder o dispositivo autenticador. Guarde-os num gestor de
|
||||
palavras-passe seguro.{' '}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -76,7 +78,8 @@ export default function TwoFactorRecoveryCodes({
|
||||
className="size-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{codesAreVisible ? 'Hide' : 'View'} recovery codes
|
||||
{codesAreVisible ? 'Ocultar' : 'Ver'} códigos de
|
||||
recuperação{' '}
|
||||
</Button>
|
||||
|
||||
{canRegenerateCodes && (
|
||||
@@ -92,7 +95,7 @@ export default function TwoFactorRecoveryCodes({
|
||||
disabled={processing}
|
||||
aria-describedby="regenerate-warning"
|
||||
>
|
||||
<RefreshCw /> Regenerate codes
|
||||
<RefreshCw /> Gerar novos códigos{' '}
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
@@ -112,7 +115,7 @@ export default function TwoFactorRecoveryCodes({
|
||||
ref={codesSectionRef}
|
||||
className="bg-muted grid gap-1 rounded-lg p-4 font-mono text-sm"
|
||||
role="list"
|
||||
aria-label="Recovery codes"
|
||||
aria-label="Códigos de recuperação"
|
||||
>
|
||||
{recoveryCodesList.length ? (
|
||||
recoveryCodesList.map((code, index) => (
|
||||
@@ -127,7 +130,7 @@ export default function TwoFactorRecoveryCodes({
|
||||
) : (
|
||||
<div
|
||||
className="space-y-2"
|
||||
aria-label="Loading recovery codes"
|
||||
aria-label="A carregar os códigos de recuperação"
|
||||
>
|
||||
{Array.from(
|
||||
{ length: 8 },
|
||||
@@ -145,13 +148,15 @@ export default function TwoFactorRecoveryCodes({
|
||||
|
||||
<div className="text-muted-foreground text-xs select-none">
|
||||
<p id="regenerate-warning">
|
||||
Each recovery code can be used once to
|
||||
access your account and will be removed
|
||||
after use. If you need more, click{' '}
|
||||
{' '}
|
||||
Cada código permite uma única entrada e
|
||||
deixa de ser válido após a utilização.
|
||||
Para obter novos códigos, seleccione{' '}
|
||||
<span className="font-bold">
|
||||
Regenerate codes
|
||||
{' '}
|
||||
Gerar novos códigos{' '}
|
||||
</span>{' '}
|
||||
above.
|
||||
acima.{' '}
|
||||
</p>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -104,7 +104,8 @@ function TwoFactorSetupStep({
|
||||
<div className="relative flex w-full items-center justify-center">
|
||||
<div className="bg-border absolute inset-0 top-1/2 h-px w-full" />
|
||||
<span className="bg-card relative px-2 py-1">
|
||||
or, enter the code manually
|
||||
{' '}
|
||||
ou introduza a chave manualmente{' '}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -210,7 +211,8 @@ function TwoFactorVerificationStep({
|
||||
onClick={onBack}
|
||||
disabled={processing}
|
||||
>
|
||||
Back
|
||||
{' '}
|
||||
Voltar{' '}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
@@ -219,7 +221,8 @@ function TwoFactorVerificationStep({
|
||||
processing || code.length < OTP_MAX_LENGTH
|
||||
}
|
||||
>
|
||||
Confirm
|
||||
{' '}
|
||||
Confirmar{' '}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -262,27 +265,27 @@ export default function TwoFactorSetupModal({
|
||||
}>(() => {
|
||||
if (twoFactorEnabled) {
|
||||
return {
|
||||
title: 'Two-factor authentication enabled',
|
||||
title: 'Autenticação de dois factores activada',
|
||||
description:
|
||||
'Two-factor authentication is now enabled. Scan the QR code or enter the setup key in your authenticator app.',
|
||||
buttonText: 'Close',
|
||||
'A autenticação de dois factores está activada. Leia o código QR ou introduza a chave na aplicação autenticadora.',
|
||||
buttonText: 'Fechar',
|
||||
};
|
||||
}
|
||||
|
||||
if (showVerificationStep) {
|
||||
return {
|
||||
title: 'Verify authentication code',
|
||||
title: 'Confirmar código de autenticação',
|
||||
description:
|
||||
'Enter the 6-digit code from your authenticator app',
|
||||
buttonText: 'Continue',
|
||||
'Introduza o código de seis dígitos da aplicação autenticadora',
|
||||
buttonText: 'Continuar',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
title: 'Enable two-factor authentication',
|
||||
title: 'Activar autenticação de dois factores',
|
||||
description:
|
||||
'To finish enabling two-factor authentication, scan the QR code or enter the setup key in your authenticator app',
|
||||
buttonText: 'Continue',
|
||||
'Para concluir a configuração, leia o código QR ou introduza a chave na aplicação autenticadora.',
|
||||
buttonText: 'Continuar',
|
||||
};
|
||||
}, [twoFactorEnabled, showVerificationStep]);
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ function DialogContent({
|
||||
{children}
|
||||
<DialogPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only"> Fechar </span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
|
||||
@@ -72,7 +72,7 @@ function SheetContent({
|
||||
{children}
|
||||
<SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
|
||||
<XIcon className="size-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
<span className="sr-only"> Fechar </span>
|
||||
</SheetPrimitive.Close>
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
|
||||
@@ -6,7 +6,7 @@ function Spinner({ className, ...props }: React.ComponentProps<"svg">) {
|
||||
return (
|
||||
<Loader2Icon
|
||||
role="status"
|
||||
aria-label="Loading"
|
||||
aria-label="A carregar"
|
||||
className={cn("size-4 animate-spin", className)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -40,8 +40,7 @@ export function UserMenuContent({ user }: Props) {
|
||||
prefetch
|
||||
onClick={cleanup}
|
||||
>
|
||||
<Settings className="mr-2" />
|
||||
Settings
|
||||
<Settings className="mr-2" /> Definições{' '}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
@@ -55,7 +55,10 @@ export const useTwoFactorAuth = (): UseTwoFactorAuthReturn => {
|
||||
|
||||
setQrCodeSvg(svg);
|
||||
} catch {
|
||||
setErrors((prev) => [...prev, 'Failed to fetch QR code']);
|
||||
setErrors((prev) => [
|
||||
...prev,
|
||||
'Não foi possível carregar o código QR.',
|
||||
]);
|
||||
setQrCodeSvg(null);
|
||||
}
|
||||
}, [submit]);
|
||||
@@ -68,7 +71,10 @@ export const useTwoFactorAuth = (): UseTwoFactorAuthReturn => {
|
||||
|
||||
setManualSetupKey(key);
|
||||
} catch {
|
||||
setErrors((prev) => [...prev, 'Failed to fetch a setup key']);
|
||||
setErrors((prev) => [
|
||||
...prev,
|
||||
'Não foi possível carregar a chave de configuração.',
|
||||
]);
|
||||
setManualSetupKey(null);
|
||||
}
|
||||
}, [submit]);
|
||||
@@ -79,7 +85,10 @@ export const useTwoFactorAuth = (): UseTwoFactorAuthReturn => {
|
||||
const codes = (await submit(recoveryCodes())) as string[];
|
||||
setRecoveryCodesList(codes);
|
||||
} catch {
|
||||
setErrors((prev) => [...prev, 'Failed to fetch recovery codes']);
|
||||
setErrors((prev) => [
|
||||
...prev,
|
||||
'Não foi possível carregar os códigos de recuperação.',
|
||||
]);
|
||||
setRecoveryCodesList([]);
|
||||
}
|
||||
}, [submit]);
|
||||
|
||||
@@ -12,17 +12,17 @@ import type { NavItem } from '@/types';
|
||||
|
||||
const sidebarNavItems: NavItem[] = [
|
||||
{
|
||||
title: 'Profile',
|
||||
title: 'Perfil',
|
||||
href: edit(),
|
||||
icon: null,
|
||||
},
|
||||
{
|
||||
title: 'Security',
|
||||
title: 'Segurança',
|
||||
href: editSecurity(),
|
||||
icon: null,
|
||||
},
|
||||
{
|
||||
title: 'Appearance',
|
||||
title: 'Aparência',
|
||||
href: editAppearance(),
|
||||
icon: null,
|
||||
},
|
||||
@@ -34,15 +34,15 @@ export default function SettingsLayout({ children }: PropsWithChildren) {
|
||||
return (
|
||||
<div className="px-4 py-6">
|
||||
<Heading
|
||||
title="Settings"
|
||||
description="Manage your profile and account settings"
|
||||
title="Definições"
|
||||
description="Gerir o perfil e as definições da conta"
|
||||
/>
|
||||
|
||||
<div className="flex flex-col lg:flex-row lg:space-x-12">
|
||||
<aside className="w-full max-w-xl lg:w-48">
|
||||
<nav
|
||||
className="flex flex-col space-y-1 space-x-0"
|
||||
aria-label="Settings"
|
||||
aria-label="Definições"
|
||||
>
|
||||
{sidebarNavItems.map((item, index) => (
|
||||
<Button
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user