Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f4eb5cfaa | ||
|
|
042ffbde44 | ||
|
|
064296e1fb | ||
|
|
a1fbda06ad | ||
|
|
52fbb1468a | ||
|
|
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 |
@@ -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
|
||||
|
||||
@@ -39,3 +39,10 @@ Referenciar estas orientações nos requisitos correspondentes da matriz existen
|
||||
- Preservar o ficheiro original, a proveniência, o identificador exacto, a edição/data, o hash e o histórico. Não marcar autenticidade, vigência, direitos ou scan como confirmados sem evidência.
|
||||
- Para fontes fiscais, inventariar também as fontes não analisadas e os diplomas/artigos sem correspondência documental. Parâmetros ausentes ou ambiguidades permanecem indeterminados; não usar zero nem a taxa mais recente como substituto.
|
||||
- Antes de aplicar mudanças em dados activos, confirmar backup/restauro e a acção autenticada que continua necessária. Testes e migrações devem usar exclusivamente uma base QA isolada com guarda contra a base activa.
|
||||
|
||||
|
||||
## Execução das fontes e decisões — orientação permanente da Tempo, 22/10/2026
|
||||
|
||||
A legislação, os formulários e os procedimentos oficiais disponibilizados são fontes válidas para implementar os requisitos. As decisões e procedimentos da Tempo orientam a sua utilização no portal. Traduzir essas fontes em campos, regras, documentos, cálculos e percursos executáveis com rastreabilidade, sem exigir nova validação jurídica genérica ou criar aprovadores, restrições ou condições por interpretação do Codex.
|
||||
|
||||
Uma divergência concreta deve identificar os trechos, períodos e comportamento afectados; prosseguir com os restantes requisitos. Uma decisão de implementação em falta deve ser apresentada especificamente à Tempo, sem a substituir por regra inventada. Rever e remover bloqueios anteriores sem fundamento numa fonte aplicável ou decisão expressa da Tempo, preservando dados e histórico. A rastreabilidade explica a execução e não constitui uma autoridade adicional. Mantêm-se os limites autorizados de QA e das alterações no activo.
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\InvestmentLegalSource;
|
||||
use App\Models\User;
|
||||
use App\Services\SimulationTaxParameterResolver;
|
||||
use App\Services\SimulationTaxRuleExpression;
|
||||
use App\Services\SimulationTaxRuleGovernance;
|
||||
use App\Support\IsolatedQaDatabase;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
/** Explicit registration: never run during migration, deployment or document import. */
|
||||
class RegisterFiscalQaDraft extends Command
|
||||
{
|
||||
protected $signature = 'fiscal:qa-register-iva {actor : Existing QA editor ID} {source : Existing accepted legal source UUID}';
|
||||
|
||||
protected $description = 'Cadastrar apenas o rascunho IVA do primeiro lote numa base QA isolada, sem publicação';
|
||||
|
||||
public function handle(SimulationTaxRuleGovernance $governance): int
|
||||
{
|
||||
abort_unless(IsolatedQaDatabase::allowsSchemaChanges(), 403, 'O cadastro deste lote só é permitido na base QA isolada.');
|
||||
$actor = User::findOrFail($this->argument('actor'));
|
||||
$source = InvestmentLegalSource::findOrFail($this->argument('source'));
|
||||
abort_unless($source->validation_status === 'validated' && $source->diploma === 'Lei 22/2022'
|
||||
&& $source->article === 'Artigo 17(1)', 422, 'Seleccione a referência aceite da taxa geral, Lei 22/2022, artigo 17(1).');
|
||||
$payload = json_decode(file_get_contents(database_path('qa/iva-general-net-v1.json')), true, flags: JSON_THROW_ON_ERROR);
|
||||
$actualHash = hash_file('sha256', base_path('docs/investmoz/simulations/source-archive/Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf'));
|
||||
abort_unless(hash_equals($payload['source_sha256'], $actualHash), 422, 'O exemplar não corresponde ao hash da ficha.');
|
||||
$payload['legal_source_id'] = (string) $source->id;
|
||||
app(SimulationTaxRuleExpression::class)->validate($payload['rule_definition']);
|
||||
app(SimulationTaxParameterResolver::class)->validateParameters($payload['parameters']);
|
||||
$existing = $governance->listing($actor, ['rule_code' => $payload['rule_code']])->getCollection()
|
||||
->firstWhere('rule_version', 1);
|
||||
if ($existing) {
|
||||
foreach ($payload as $field => $value) {
|
||||
$stored = $existing->attributesToArray()[$field] ?? null;
|
||||
abort_unless($stored == $value, 409, 'O cadastro existente difere desta ficha; não será sobrescrito.');
|
||||
}
|
||||
abort_unless($existing->status === 'draft', 409, 'A versão existente já não é um rascunho.');
|
||||
$this->line(json_encode(['id' => $existing->id, 'status' => 'draft', 'reused' => true], JSON_THROW_ON_ERROR));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
$rule = $governance->create($actor, $payload);
|
||||
$this->line(json_encode(['id' => $rule->id, 'status' => $rule->status, 'reused' => false], JSON_THROW_ON_ERROR));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -37,11 +37,17 @@ class SimulationController extends Controller
|
||||
$token = $actorId === null ? $request->header('X-Simulation-Token') : null;
|
||||
abort_if($actorId === null && (! is_string($token) || strlen($token) < 32 || strlen($token) > 256), 422, 'X-Simulation-Token opaco gerado pelo cliente é obrigatório para execução anónima.');
|
||||
$validated = $request->validated();
|
||||
$data = ['base' => (string) $validated['base'], 'rate' => (string) $validated['rate'],
|
||||
'reduction' => (string) ($validated['reduction'] ?? '0'), 'organization_id' => $validated['organization_id'] ?? null];
|
||||
$organizationId = $data['organization_id'] ?? null;
|
||||
$organizationId = $validated['organization_id'] ?? null;
|
||||
$execution->authorizeOrganization($actorId, $organizationId);
|
||||
$result = $execution->create($data, $actorId, $organizationId, $token, $key);
|
||||
if (($validated['scenario_kind'] ?? null) === 'fiscal_rule_published') {
|
||||
$input = array_intersect_key($validated, array_flip(['base', 'currency', 'reference_date', 'operation', 'scope_declared', 'monthly_sales', 'sales_vat_basis']));
|
||||
$input['scope_declared'] = (bool) $input['scope_declared'];
|
||||
$result = $execution->createPublishedFiscal($input, $actorId, $organizationId, $token, $key);
|
||||
} else {
|
||||
$data = ['base' => (string) $validated['base'], 'rate' => (string) $validated['rate'],
|
||||
'reduction' => (string) ($validated['reduction'] ?? '0')];
|
||||
$result = $execution->create($data, $actorId, $organizationId, $token, $key);
|
||||
}
|
||||
$simulation = $result['simulation'];
|
||||
$body = ['data' => $simulation, 'replayed' => $result['replayed']];
|
||||
if ($actorId === null) {
|
||||
|
||||
@@ -19,13 +19,15 @@ class SimulationTaxParameterController extends Controller
|
||||
$input['period_start'],
|
||||
$input['period_end'],
|
||||
$input['context'] ?? [],
|
||||
$input['component'] === 'IVA',
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'component' => $input['component'],
|
||||
'period' => ['start' => $input['period_start'], 'end' => $input['period_end']],
|
||||
'assumptions' => $input['context'] ?? [],
|
||||
'parameters' => $data,
|
||||
'parameters' => $input['component'] === 'IVA' ? $data['parameters'] : $data,
|
||||
'diagnostics' => $input['component'] === 'IVA' ? $data['diagnostics'] : [],
|
||||
])->header('Cache-Control', 'no-store');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]);
|
||||
@@ -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)]);
|
||||
}
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Services;
|
||||
use App\Models\ExistingBusiness;
|
||||
use App\Models\Opportunity;
|
||||
use App\Models\Sector;
|
||||
use App\Models\SimulationFormDraft;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Support\Arr;
|
||||
@@ -12,6 +13,7 @@ use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
/** Context belongs to the existing encrypted dossier. Sources remain read-only. */
|
||||
@@ -21,7 +23,7 @@ final class InvestorContext
|
||||
|
||||
public const SOURCE = 'investor-context';
|
||||
|
||||
public const FIELDS = ['project_title', 'project_summary', 'sector_id', 'sector', 'province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location'];
|
||||
public const FIELDS = ['project_title', 'project_summary', 'sector_id', 'sector', 'province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location', 'currency', 'expected_start_date', 'fiscal_period', 'fiscal_periodicity', 'fiscal_month', 'investor_origin', 'person_type', 'entity_stage', 'tax_residency_country', 'fiscal_residency_country', 'activity_id', 'project_type', 'maturity_stage', 'special_zone_id', 'investment_regime', ...PrivateInvestmentPreparation::FIELDS];
|
||||
|
||||
public function __construct(private readonly VirtualDesk $desk) {}
|
||||
|
||||
@@ -34,8 +36,26 @@ final class InvestorContext
|
||||
{
|
||||
$payload = $this->desk->decode($case->payload);
|
||||
|
||||
return $payload['investor_path']['context'] ?? ['revision' => 0, 'selection' => [],
|
||||
$context = $payload['investor_path']['context'] ?? ['revision' => 0, 'selection' => [],
|
||||
'fields' => ['project_title' => $case->title], 'provenance' => ['project_title' => ['kind' => 'dossier', 'id' => $case->id, 'version' => $case->version]], 'review_steps' => []];
|
||||
// Explicit dossier anchor only: never the account's most recent simulation or another owner's draft.
|
||||
if (($case->simulation_id ?? null) && request()->user()?->id === (int) $case->user_id) {
|
||||
$draft = SimulationFormDraft::whereKey($case->simulation_id)->where('owner_user_id', $case->user_id)->whereNull('deleted_at')->first();
|
||||
if ($draft) {
|
||||
$values = [...SimulationFormDraft::decryptSnapshot($draft->personal_snapshot), ...SimulationFormDraft::decryptSnapshot($draft->economic_snapshot)];
|
||||
foreach (Arr::only($values, self::FIELDS) as $key => $value) {
|
||||
if (is_scalar($value) && (string) $value !== '' && (! array_key_exists($key, $context['fields']) || ($context['provenance'][$key]['kind'] ?? null) === 'linked_simulation')) {
|
||||
$context['fields'][$key] = (string) $value;
|
||||
$context['provenance'][$key] = ['kind' => 'linked_simulation', 'id' => $draft->id, 'version' => $draft->version_no, 'sha256' => $draft->snapshot_sha256];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$zone = $context['selection']['park'] ?? null;
|
||||
$context['fields']['investment_regime'] = $zone['fields']['investment_regime'] ?? 'general';
|
||||
$context['fields']['special_zone_id'] = $zone['id'] ?? '';
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
public function dependencyHash(array $selection): string
|
||||
@@ -53,17 +73,20 @@ final class InvestorContext
|
||||
$documents = [];
|
||||
$visibility = 'public';
|
||||
$state = null;
|
||||
$zoneAnalysis = null;
|
||||
if ($kind === 'opportunity') {
|
||||
abort_unless(ctype_digit($id), 422, 'Identificador de oportunidade inválido.');
|
||||
$o = Opportunity::with('sector')->findOrFail((int) $id);
|
||||
$published = Opportunity::published()->whereKey($o->id)->exists();
|
||||
if (!$published) {
|
||||
if (! $published) {
|
||||
abort_unless(request()->user() && $o->status !== 'archived' && app(PortalProfileAccess::class)->canView(request()->user(), $o), 404, 'Ficha privada indisponível neste âmbito.');
|
||||
$visibility = 'private';
|
||||
$state = app(PortalProfileAccess::class)->state($o);
|
||||
}
|
||||
if (request()->user()) $documents = app(OpportunityLibrarySupport::class)->snapshot(request()->user(), $o);
|
||||
$draft = !$published ? app(OpportunityManagement::class)->values($o) : [];
|
||||
if (request()->user()) {
|
||||
$documents = app(OpportunityLibrarySupport::class)->snapshot(request()->user(), $o);
|
||||
}
|
||||
$draft = ! $published ? app(OpportunityManagement::class)->values($o) : [];
|
||||
$title = $draft['canonical']['official_title'] ?? $o->title;
|
||||
$href = $published ? '/oportunidades/'.$o->slug : '/minhas-oportunidades/'.$o->id.'/view';
|
||||
$version = $o->management_version;
|
||||
@@ -122,19 +145,28 @@ final class InvestorContext
|
||||
$fields = $this->territoryFields($id);
|
||||
} elseif ($kind === 'park') {
|
||||
abort_unless(Str::isUuid($id), 422);
|
||||
$p = app(InvestmentZones::class)->visible()->where('category', 'industrial_park')->findOrFail($id);
|
||||
$p = app(InvestmentZones::class)->visible()->whereIn('category', ['industrial_park', 'zee', 'zfi'])->findOrFail($id);
|
||||
$title = $p->name;
|
||||
$fields = ['special_zone_id' => (string) $p->id, 'investment_regime' => (string) $p->category];
|
||||
$version = $p->version_no;
|
||||
$zoneAnalysis = ['conditions' => app(InvestmentZoneOperationalDetails::class)->publicData($p)['conditions'] ?? [],
|
||||
'source' => $p->legal_source_id ? DB::table('investment_legal_sources')->where('id', $p->legal_source_id)->first(['title', 'url', 'diploma', 'article', 'valid_from', 'valid_until']) : null,
|
||||
'recognition' => 'undetermined', 'executable_benefit' => false];
|
||||
$href = '/zonas/'.$id;
|
||||
if ($p->primary_territory_id) {
|
||||
$fields = $this->territoryFields($p->primary_territory_id);
|
||||
$fields = [...$fields, ...$this->territoryFields($p->primary_territory_id)];
|
||||
}
|
||||
} else {
|
||||
abort(422, 'Tipo de referência inválido.');
|
||||
}
|
||||
$fields = array_filter($fields, fn ($v) => is_scalar($v) && trim((string) $v) !== '');
|
||||
$record = ['key' => $key, 'kind' => $kind, 'id' => $id, 'title' => $title, 'version' => $version, 'href' => $href, 'fields' => $fields];
|
||||
if ($documents || $visibility === 'private') $record += ['documents' => $documents, 'visibility' => $visibility, 'state' => $state];
|
||||
if ($zoneAnalysis !== null) {
|
||||
$record['analysis'] = $zoneAnalysis;
|
||||
}
|
||||
if ($documents || $visibility === 'private') {
|
||||
$record += ['documents' => $documents, 'visibility' => $visibility, 'state' => $state];
|
||||
}
|
||||
|
||||
return [...$record, 'sha256' => hash('sha256', json_encode($record, JSON_THROW_ON_ERROR))];
|
||||
}
|
||||
@@ -170,7 +202,11 @@ final class InvestorContext
|
||||
}
|
||||
if (request()->user()) {
|
||||
$private = app(PortalProfileAccess::class)->scope(Opportunity::query(), request()->user())->where('status', '!=', 'published')->where('status', '!=', 'archived')->when($query, fn ($q) => $q->where('title', 'ilike', '%'.$query.'%'))->orderBy('title')->limit(51)->get();
|
||||
foreach ($private as $o) if (app(PortalProfileAccess::class)->canView(request()->user(), $o)) $items[] = ['key' => 'opportunity:'.$o->id, 'title' => $o->title.' — preparação privada', 'kind' => 'opportunity'];
|
||||
foreach ($private as $o) {
|
||||
if (app(PortalProfileAccess::class)->canView(request()->user(), $o)) {
|
||||
$items[] = ['key' => 'opportunity:'.$o->id, 'title' => $o->title.' — preparação privada', 'kind' => 'opportunity'];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (config('existing-business.enabled')) {
|
||||
foreach (ExistingBusiness::published()->when($query, fn ($q) => $q->where('public_title', 'ilike', '%'.$query.'%'))->orderBy('public_title')->limit(51)->get(['id', 'public_title']) as $o) {
|
||||
@@ -179,7 +215,7 @@ final class InvestorContext
|
||||
}
|
||||
|
||||
return ['opportunities' => $items, 'search_limited' => count($items) > 50, 'sectors' => Sector::choices()->orderBy('name')->get(['id', 'name', 'slug']),
|
||||
'parks' => app(InvestmentZones::class)->visible()->where('category', 'industrial_park')->orderBy('name')->get(['id', 'name', 'version_no'])];
|
||||
'parks' => app(InvestmentZones::class)->visible()->whereIn('category', ['industrial_park', 'zee', 'zfi'])->orderBy('name')->get(['id', 'name', 'category', 'version_no'])];
|
||||
}
|
||||
|
||||
public function current(User $actor): array
|
||||
@@ -232,10 +268,21 @@ final class InvestorContext
|
||||
|
||||
private function input(array $input): array
|
||||
{
|
||||
abort_if(array_diff(array_keys($input), ['version', 'selection', 'declarations', 'replace_fields', 'preview_hash', 'operation_key', 'confirm', '_token']), 422, 'Campo de controlo desconhecido.');
|
||||
abort_if(array_diff(array_keys($input), ['version', 'selection', 'declarations', 'replace_fields', 'preview_hash', 'operation_key', 'confirm', 'document_links', '_token']), 422, 'Campo de controlo desconhecido.');
|
||||
|
||||
return Validator::make($input, ['version' => 'required|integer|min:1', 'selection' => 'present|array:opportunity,sector,territory,park',
|
||||
'selection.*' => 'nullable|string|max:100', 'declarations' => 'nullable|array:project_title,project_summary', 'declarations.project_title' => 'nullable|string|max:200', 'declarations.project_summary' => 'nullable|string|max:5000',
|
||||
abort_if(! app(PrivateInvestmentPreparation::class)->available() && (! empty($input['document_links']) || array_intersect(array_keys(is_array($input['declarations'] ?? null) ? $input['declarations'] : []), PrivateInvestmentPreparation::FIELDS)), 409, 'Esta preparação ainda não está disponível neste ambiente. As respostas foram preservadas.');
|
||||
|
||||
$inputRules = [
|
||||
'document_links' => 'sometimes|array:'.implode(',', PrivateInvestmentPreparation::DOCUMENTS),
|
||||
'document_links.*' => 'nullable|uuid',
|
||||
];
|
||||
foreach (app(PrivateInvestmentPreparation::class)->questions() as $question) {
|
||||
$inputRules['declarations.'.$question['key']] = isset($question['options']) ? ['nullable', Rule::in(array_keys($question['options']))] : ['nullable', 'regex:/^\d+(?:\.\d+)?$/D', 'numeric', 'min:0', 'max:999999999999999999'];
|
||||
}
|
||||
|
||||
return Validator::make($input, [...$inputRules, 'version' => 'required|integer|min:1', 'selection' => 'present|array:opportunity,sector,territory,park',
|
||||
'selection.*' => 'nullable|string|max:100', 'declarations' => 'nullable|array:project_title,project_summary,currency,expected_start_date,investor_origin,person_type,entity_stage,tax_residency_country,fiscal_residency_country,project_type,maturity_stage,'.implode(',', PrivateInvestmentPreparation::FIELDS), 'declarations.project_title' => 'nullable|string|max:200', 'declarations.project_summary' => 'nullable|string|max:5000',
|
||||
'declarations.currency' => 'nullable|regex:/^[A-Z]{3}$/', 'declarations.expected_start_date' => 'nullable|date_format:Y-m-d', 'declarations.investor_origin' => 'nullable|in:national,foreign,mixed', 'declarations.person_type' => 'nullable|in:singular,collective,sole_trader,consortium', 'declarations.entity_stage' => 'nullable|in:existing,being_formed', 'declarations.tax_residency_country' => ['nullable', 'string', app(CountryCatalogue::class)->rule()], 'declarations.fiscal_residency_country' => ['nullable', 'string', app(CountryCatalogue::class)->rule()], 'declarations.project_type' => 'nullable|string|max:100', 'declarations.maturity_stage' => 'nullable|string|max:100',
|
||||
'replace_fields' => 'nullable|array', 'replace_fields.*' => 'string|in:'.implode(',', self::FIELDS)])->validate();
|
||||
}
|
||||
|
||||
@@ -247,6 +294,15 @@ final class InvestorContext
|
||||
abort_unless((int) $case->version === $data['version'], 409, 'O dossier mudou; recupere a versão atual.');
|
||||
app(InvestorPath::class)->view($actor, $id);
|
||||
$old = $this->state($case);
|
||||
$links = $old['preparation_documents'] ?? [];
|
||||
foreach ($data['document_links'] ?? [] as $key => $document) {
|
||||
if ($document) {
|
||||
abort_unless(DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $id)->where('d.id', $document)->whereNull('d.deleted_at')->exists(), 422, 'Seleccione um documento já associado a este dossier; os dados anteriores foram preservados.');
|
||||
$links[$key] = $document;
|
||||
} else {
|
||||
unset($links[$key]);
|
||||
}
|
||||
}
|
||||
$selection = $old['selection'];
|
||||
foreach ($data['selection'] as $slot => $key) {
|
||||
if ($key === null || $key === '') {
|
||||
@@ -299,7 +355,7 @@ final class InvestorContext
|
||||
}
|
||||
// Remove inherited subordinate geography only when its source was actually replaced.
|
||||
foreach ($fields as $key => $value) {
|
||||
if (! array_key_exists($key, $proposed) && in_array($key, ['province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location', 'sector_id', 'sector'], true) && isset($provenance[$key]['key'])) {
|
||||
if (! array_key_exists($key, $proposed) && in_array($key, ['province_id', 'district_id', 'admin_post_id', 'locality_id', 'province', 'district', 'location', 'sector_id', 'sector', 'special_zone_id', 'investment_regime'], true) && isset($provenance[$key]['key'])) {
|
||||
unset($fields[$key],$provenance[$key]);
|
||||
}
|
||||
}
|
||||
@@ -314,14 +370,32 @@ final class InvestorContext
|
||||
$changed[] = $slot;
|
||||
}
|
||||
}
|
||||
$dependencyChanged = $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection);
|
||||
$changedFields = array_column($replaced, 'field');
|
||||
$conditionsChanged = (bool) array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary']));
|
||||
$dependencyChanged = $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || $conditionsChanged;
|
||||
$privateConditionsChanged = (bool) array_intersect($changedFields, PrivateInvestmentPreparation::FIELDS);
|
||||
$calculationInputsChanged = $this->dependencyHash($old['selection']) !== $this->dependencyHash($selection) || (bool) array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary', ...array_diff(PrivateInvestmentPreparation::FIELDS, ['total_investment', 'area_required', 'area_unit'])]));
|
||||
$payload = $this->desk->decode($case->payload);
|
||||
$results = $payload['investor_path']['results'] ?? [];
|
||||
$requests = DB::table('desk_requests')->where('case_id', $id)->orderBy('id')->get(['id', 'status', 'protocol', 'version'])->all();
|
||||
$steps = $dependencyChanged ? array_values(array_unique([...(array_intersect($changed, ['opportunity', 'sector']) ? ['S02', 'S03', 'S04', 'S08', 'S12', 'S13', 'S14', 'S15', 'S17'] : []), ...(array_intersect($changed, ['territory', 'park']) ? ['S08', 'S09', 'S10', 'S11', 'S17'] : [])])) : [];
|
||||
if (array_intersect($changedFields, array_diff(self::FIELDS, ['project_title', 'project_summary', ...PrivateInvestmentPreparation::FIELDS]))) {
|
||||
$steps = array_values(array_unique([...$steps, 'S02', 'S03', 'S08', 'S12', 'S13', 'S17']));
|
||||
}
|
||||
if ($privateConditionsChanged) {
|
||||
$steps = array_values(array_unique([...$steps, 'S02', 'S05', 'S06', 'S07']));
|
||||
}
|
||||
$privatePreparation = null;
|
||||
if (app(PrivateInvestmentPreparation::class)->available()) {
|
||||
$docs = $this->preparationDocuments($id);
|
||||
$beforePreparation = app(PrivateInvestmentPreparation::class)->assess($old, $docs);
|
||||
$afterPreparation = app(PrivateInvestmentPreparation::class)->assess(['fields' => $fields, 'selection' => $selection, 'preparation_documents' => $links], $docs);
|
||||
$privatePreparation = ['before' => $beforePreparation, 'after' => $afterPreparation];
|
||||
}
|
||||
$plan = ['case_id' => $id, 'version' => (int) $case->version, 'revision' => (int) $old['revision'], 'selection' => $selection, 'fields' => $fields, 'provenance' => $provenance,
|
||||
'updated' => $replaced, 'maintained' => $kept, 'changed_dimensions' => $changed, 'dependent_results' => (int) $case->user_id === $actor->id && $dependencyChanged ? array_map(fn ($r) => ['id' => $r['id'], 'engine' => $r['engine']], $results) : [],
|
||||
'requests' => $requests, 'review_steps' => $steps, 'dependency_changed' => $dependencyChanged, 'documents_preserved' => DB::table('desk_documents')->where('case_id', $id)->count(),
|
||||
'preparation_documents' => $links, 'private_investment' => $privatePreparation,
|
||||
'updated' => $replaced, 'maintained' => $kept, 'changed_dimensions' => $changed, 'dependent_results' => (int) $case->user_id === $actor->id && $calculationInputsChanged ? array_map(fn ($r) => ['id' => $r['id'], 'engine' => $r['engine']], $results) : [],
|
||||
'requests' => $requests, 'review_steps' => $steps, 'dependency_changed' => $dependencyChanged, 'calculation_inputs_changed' => $calculationInputsChanged, 'documents_preserved' => DB::table('desk_documents')->where('case_id', $id)->count(),
|
||||
'notice' => 'Não confirma incentivos, licenças, disponibilidade de terra, reserva ou autorização. Submissões e decisões anteriores mantêm o conteúdo enviado.'];
|
||||
$hash = hash_hmac('sha256', json_encode(Arr::sortRecursive([$actor->id, $data, $plan]), JSON_THROW_ON_ERROR), config('app.key'));
|
||||
|
||||
@@ -349,7 +423,7 @@ final class InvestorContext
|
||||
$before = $this->state($case);
|
||||
$plan = $this->preview($actor, $id, $input);
|
||||
abort_unless(hash_equals($plan['preview_hash'], $control['preview_hash']), 409, 'O dossier ou uma fonte mudou desde a prévia; compare novamente.');
|
||||
if (! $plan['changed_dimensions'] && ! $plan['updated'] && empty($input['declarations']) && empty($input['replace_fields'])) {
|
||||
if (! $plan['changed_dimensions'] && ! $plan['updated'] && empty($input['declarations']) && empty($input['replace_fields']) && empty($input['document_links'])) {
|
||||
$result = ['case_id' => $id, 'version' => (int) $case->version, 'revision' => $before['revision'], 'changed' => false, 'replayed' => false];
|
||||
DB::table('audit_events')->insert(['source_system' => self::SOURCE, 'entity_type' => 'desk_cases', 'entity_id' => $id, 'actor_user_id' => $actor->id, 'correlation_id' => $control['operation_key'], 'action' => 'context_unchanged', 'data_classification' => 'restricted', 'after_data' => json_encode(['case_id' => $id, 'request_hash' => $requestHash, 'result' => $result], JSON_THROW_ON_ERROR)]);
|
||||
|
||||
@@ -361,9 +435,9 @@ final class InvestorContext
|
||||
foreach ($plan['review_steps'] as $step) {
|
||||
$review[$step] = ['revision' => $revision, 'reason' => 'O contexto mudou; reveja os pressupostos e a evidência aplicável.'];
|
||||
}
|
||||
$context = ['revision' => $revision, 'selection' => $plan['selection'], 'fields' => $plan['fields'], 'provenance' => $plan['provenance'], 'review_steps' => $review,
|
||||
'dependency_hash' => $this->dependencyHash($plan['selection']), 'updated_at' => now()->toIso8601String(), 'updated_by' => $actor->id];
|
||||
if ($plan['dependency_changed']) {
|
||||
$context = ['preparation_documents' => $plan['preparation_documents'], 'preparation_snapshot' => $plan['private_investment']['after'] ?? $before['preparation_snapshot'] ?? null, 'revision' => $revision, 'selection' => $plan['selection'], 'fields' => $plan['fields'], 'provenance' => $plan['provenance'], 'review_steps' => $review,
|
||||
'dependency_hash' => $this->dependencyHash(['selection' => $plan['selection'], 'fields' => Arr::except($plan['fields'], ['project_title', 'project_summary'])]), 'updated_at' => now()->toIso8601String(), 'updated_by' => $actor->id];
|
||||
if ($plan['calculation_inputs_changed']) {
|
||||
foreach ($payload['investor_path']['results'] ?? [] as $i => $r) {
|
||||
$payload['investor_path']['results'][$i] = [...$r, 'context_state' => 'review_required', 'context_invalidated_revision' => $revision, 'context_invalidated_at' => now()->toIso8601String()];
|
||||
}
|
||||
@@ -384,6 +458,8 @@ final class InvestorContext
|
||||
abort_unless($this->desk->owns($actor, $case, false) || $actor->can('manage-portal'), 403);
|
||||
$ctx = $this->state($case);
|
||||
$path = app(InvestorPath::class)->view($actor, $id);
|
||||
$ctx['path_applicability'] = ['definition_id' => $path['definition_id'], 'definition_version' => $path['definition_version'], 'source_sha256' => $path['source_sha256'],
|
||||
'steps' => array_values(array_map(fn ($step) => Arr::only($step, ['code', 'title', 'applicability', 'reason', 'state', 'context_review_required']), array_filter($path['steps'], fn ($step) => ! empty($step['branch']))))];
|
||||
|
||||
$simulatorRevision = null;
|
||||
if ((int) $case->user_id === $actor->id && $case->simulation_id) {
|
||||
@@ -396,18 +472,36 @@ final class InvestorContext
|
||||
}
|
||||
}
|
||||
|
||||
return ['simulator_context_revision' => $simulatorRevision, 'case' => $path['case'], 'context' => $ctx, 'steps' => array_map(fn ($s) => Arr::only($s, ['code', 'title', 'state', 'context_review_required']), $path['steps']),
|
||||
$preparation = app(PrivateInvestmentPreparation::class)->available() ? app(PrivateInvestmentPreparation::class)->assess($ctx, $this->preparationDocuments($id)) : null;
|
||||
$ctx['preparation_assessment'] = $preparation;
|
||||
|
||||
return ['private_investment' => $preparation, 'preparation_documents' => $this->preparationDocuments($id), 'simulator_context_revision' => $simulatorRevision, 'case' => $path['case'], 'context' => $ctx, 'steps' => array_map(fn ($s) => Arr::only($s, ['code', 'title', 'state', 'context_review_required']), $path['steps']),
|
||||
'can_write' => $path['canWrite'], 'next' => $path['next'], 'results' => $path['results'], 'requests' => $path['requests'],
|
||||
'prefill' => $this->prefill($ctx), 'notice' => 'A seleção apoia a preparação e não confirma benefícios, licenças, direitos ou disponibilidade.'];
|
||||
}
|
||||
|
||||
public function preparationDocuments(string $id): array
|
||||
{
|
||||
return DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')->where('l.case_id', $id)->whereNull('d.deleted_at')->get(['l.id', 'l.document_id', 'd.file_name', 'd.status', 'd.malware_scan_status', 'd.version_no', 'd.sha256'])->map(fn ($row) => (array) $row)->all();
|
||||
}
|
||||
|
||||
public function prefill(array $ctx): array
|
||||
{
|
||||
$f = $ctx['fields'];
|
||||
$sim = Arr::only($f, ['project_title', 'project_summary', 'sector_id', 'province_id', 'district_id', 'admin_post_id', 'locality_id']);
|
||||
$sim = Arr::except(Arr::only($f, self::FIELDS), ['sector', 'province', 'district', 'location', ...array_diff(PrivateInvestmentPreparation::FIELDS, ['total_investment', 'area_required', 'area_unit'])]);
|
||||
$zone = $ctx['selection']['park'] ?? null;
|
||||
$sim['context_applicability_snapshot'] = json_encode([
|
||||
'version' => 1, 'context_revision' => $ctx['revision'],
|
||||
'zone' => $zone ? Arr::only($zone, ['id', 'title', 'href', 'version', 'sha256', 'fields', 'analysis']) : null,
|
||||
'provenance' => $ctx['provenance'],
|
||||
'path' => $ctx['path_applicability'] ?? null,
|
||||
'preparation' => isset($ctx['preparation_assessment']) ? Arr::except($ctx['preparation_assessment'], ['questions']) : null,
|
||||
'fields' => Arr::only($f, ['investor_origin', 'tax_residency_country', 'entity_stage', 'currency', 'expected_start_date', 'investment_regime']),
|
||||
'notice' => 'Referências e declarações para preparação; sem reconhecimento automático de elegibilidade ou benefício.',
|
||||
], JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
return ['simulator' => $sim, 'land' => array_filter(['project_reference' => $f['project_title'] ?? null, 'activity' => $f['sector'] ?? null, 'purpose' => $f['project_summary'] ?? null, 'parcels' => [Arr::only($f, array_keys(InvestmentGeography::FIELDS))]], fn ($v) => $v !== null),
|
||||
'desk' => array_filter(['title' => $f['project_title'] ?? null, 'territory_id' => $f['locality_id'] ?? $f['admin_post_id'] ?? $f['district_id'] ?? $f['province_id'] ?? null, 'location_source' => isset($f['province_id']) ? 'Referência consultada no contexto v'.($ctx['revision'] ?? 0).': '.($f['location'] ?? 'território identificado').'. Não confirma disponibilidade.' : null, 'description' => $f['project_summary'] ?? null, 'sector' => $f['sector'] ?? null, ...Arr::only($f, array_keys(InvestmentGeography::FIELDS))], fn ($v) => $v !== null),
|
||||
'submission' => array_filter(['sector' => $f['sector'] ?? null, 'objective' => $f['project_summary'] ?? null, 'location' => $f['location'] ?? null], fn ($v) => $v !== null)];
|
||||
'submission' => array_filter(['currency' => $f['currency'] ?? null, 'sector' => $f['sector'] ?? null, 'objective' => $f['project_summary'] ?? null, 'location' => $f['location'] ?? null], fn ($v) => $v !== null)];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ final class InvestorPath
|
||||
$documents = DB::table('desk_documents as l')->join('documents as d', 'd.id', '=', 'l.document_id')
|
||||
->where('l.case_id', $id)->whereNull('d.deleted_at')
|
||||
->when(! $owner && ! $manager, fn ($q) => $q->where(fn ($p) => $p->whereNull('l.request_id')->orWhereIn('l.request_id', $requests->pluck('id'))))
|
||||
->get(['l.id', 'l.document_id', 'd.file_name', 'd.status', 'd.malware_scan_status']);
|
||||
->get(['l.id', 'l.document_id', 'd.file_name', 'd.status', 'd.malware_scan_status', 'd.version_no', 'd.sha256']);
|
||||
// Only the private draft owner can receive the financial snapshot/result. Dossier access alone is insufficient.
|
||||
$results = (int) $case->user_id === $actor->id ? ($path['results'] ?? []) : [];
|
||||
$next = collect($steps)->first(fn ($step) => $step['preparation_allowed'] && ! in_array($step['state'], ['completed', 'approved', 'waiting_external', 'submitted', 'reviewing'], true));
|
||||
@@ -145,12 +145,17 @@ final class InvestorPath
|
||||
? LandAssistanceCase::where('user_id', $actor->id)->latest('updated_at')->get()
|
||||
->first(fn ($land) => ($land->payload['investor_path_case_id'] ?? null) === $id) : null;
|
||||
|
||||
return ['case' => ['id' => $id, 'title' => $case->title, 'version' => $case->version, 'closed' => $case->closed_at !== null],
|
||||
$preparation = ($owner || $manager) && app(PrivateInvestmentPreparation::class)->available() ? app(PrivateInvestmentPreparation::class)->assess($context, $documents->map(fn ($d) => (array) $d)->all()) : null;
|
||||
if ($preparation && $preparation['enabled']) {
|
||||
$next = ['code' => 'S06', 'title' => $preparation['next'], 'action' => 'Preparar', 'href' => '/investimentos/'.$id.'/contexto/editar#preparation-questions'];
|
||||
}
|
||||
|
||||
return ['private_investment' => $preparation, 'case' => ['id' => $id, 'title' => $case->title, 'version' => $case->version, 'closed' => $case->closed_at !== null],
|
||||
'journey_id' => $journey->id, 'definition_id' => $version->id, 'definition_version' => $version->version_no, 'source_sha256' => $definition['source_sha256'],
|
||||
'phases' => $definition['phases'], 'steps' => $steps, 'facts' => $facts, 'fact_reasons' => $path['fact_reasons'] ?? [],
|
||||
'fintech' => ($owner || $manager) && (isset($path['fintech']) || collect($path['sector_contexts'] ?? [])->contains(fn ($s) => in_array($s['slug'], ['fintech', 'tecnologias-de-informacao', 'mercado-financeiro'], true)) || collect($path['content_sources'] ?? [])->contains('return_href', '/tech-fintech')) ? app(FintechInvestment::class)->view($payload, $steps) : null,
|
||||
'canWrite' => $owner && ! $case->closed_at, 'canReview' => $manager && (int) $case->user_id !== $actor->id && ! $case->closed_at,
|
||||
'next' => $next ? ['code' => $next['code'], 'title' => $next['title'], 'action' => 'Preparar', 'href' => '#'.$next['code']] : null,
|
||||
'next' => $next ? ['code' => $next['code'], 'title' => $next['title'], 'action' => 'Preparar', 'href' => $next['href'] ?? '#'.$next['code']] : null,
|
||||
'investment_context' => ($owner || $manager) ? $context : null,
|
||||
'results' => $results, 'requests' => $requests, 'documents' => $documents,
|
||||
'zone_contexts' => ($owner || $manager) ? ($path['zone_contexts'] ?? []) : [],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Brick\Math\BigDecimal;
|
||||
|
||||
/** Adapter of the dossier's declared context to preparation of a private-investment request. No tax or entitlement decision. */
|
||||
final class PrivateInvestmentPreparation
|
||||
{
|
||||
public const VERSION = 'private-investment-preparation-2026-v1';
|
||||
|
||||
public const SOURCE = 'https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf';
|
||||
|
||||
public const HASH = 'ba1e9eaabfec5d0c4cb2d9f2444a6860f7a90f222034764197829be402d408ed';
|
||||
|
||||
public const FIELDS = ['investment_form', 'investment_preparation', 'investment_activity_scope', 'implementation_mode', 'capital_origin', 'site_tenure', 'project_concession', 'significant_implications', 'land_extension', 'forest_concession', 'forest_concession_hectares', 'total_investment', 'area_required', 'area_unit'];
|
||||
|
||||
public const DOCUMENTS = ['identity', 'registration', 'name_reservation', 'site_plan', 'lease', 'duat', 'representation_license', 'feasibility', 'financial_capacity', 'foreign_capital'];
|
||||
|
||||
public function available(): bool
|
||||
{
|
||||
return (bool) config('investor-path.private_preparation', false);
|
||||
}
|
||||
|
||||
public function questions(): array
|
||||
{
|
||||
$tri = ['unknown' => 'Por determinar', 'yes' => 'Sim', 'no' => 'Não'];
|
||||
|
||||
return [
|
||||
['key' => 'investment_preparation', 'label' => 'Pretende preparar o pedido de garantias/incentivos ao investimento privado?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 2'],
|
||||
['key' => 'investment_activity_scope', 'label' => 'Natureza efectiva da actividade do projecto', 'options' => ['unknown' => 'Por determinar', 'general' => 'Outra actividade económica', 'extractive' => 'Prospecção, pesquisa ou produção extractiva', 'processing' => 'Processamento industrial mineiro/petrolífero', 'public_nonprofit' => 'Investimento público ou exclusivamente social/sem fins lucrativos'], 'source' => 'Lei 8/2023, artigos 2 e 22'],
|
||||
['key' => 'investment_form', 'label' => 'Tipo deste investimento', 'options' => ['unknown' => 'Por determinar', 'direct' => 'Directo', 'indirect' => 'Indirecto'], 'source' => 'Lei 8/2023, artigo 14(b); Regulamento, artigo 13', 'help' => 'O limiar monetário do artigo 13 aplica-se ao investimento directo. A forma indirecta não é convertida silenciosamente em directa.'],
|
||||
['key' => 'implementation_mode', 'label' => 'Modalidade da empresa implementadora', 'options' => ['unknown' => 'Por determinar', 'domestic_entity' => 'Empresa constituída/em constituição em Moçambique', 'foreign_representation' => 'Representação comercial estrangeira'], 'source' => 'Regulamento 8/2024, artigo 9(2)'],
|
||||
['key' => 'capital_origin', 'label' => 'Origem dos capitais/recursos deste investimento', 'options' => ['unknown' => 'Por determinar', 'national' => 'Nacionais', 'external' => 'Provenientes do exterior', 'mixed' => 'Mistos'], 'source' => 'Lei 8/2023, glossário; Regulamento, artigos 4–6'],
|
||||
['key' => 'site_tenure', 'label' => 'Fundamento declarado da utilização das instalações', 'options' => ['unknown' => 'Por determinar', 'lease' => 'Arrendamento', 'duat' => 'DUAT'], 'source' => 'Regulamento, artigos 9(1)(d) e 14(1)(d)'],
|
||||
['key' => 'project_concession', 'label' => 'O empreendimento é PPP ou concessão empresarial?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(ii)'],
|
||||
['key' => 'significant_implications', 'label' => 'Existem previsíveis implicações económicas, ambientais, de segurança ou saúde pública que exijam autorização?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(i)', 'help' => 'Declare o enquadramento conhecido; o nome do sector não responde a esta pergunta. Sem fundamento suficiente, mantenha por determinar.'],
|
||||
['key' => 'land_extension', 'label' => 'O projecto requer extensão de terra?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(iii)'],
|
||||
['key' => 'forest_concession', 'label' => 'O projecto requer concessão florestal?', 'options' => $tri, 'source' => 'Lei 8/2023, artigo 22(1)(b)(iv)'],
|
||||
['key' => 'forest_concession_hectares', 'label' => 'Área da concessão florestal (hectares)', 'type' => 'number', 'when' => ['field' => 'forest_concession', 'equals' => 'yes'], 'source' => 'Lei 8/2023, artigo 22(1)(b)(iv)'],
|
||||
['key' => 'total_investment', 'label' => 'Valor total do investimento na moeda do projecto', 'type' => 'number', 'source' => 'Regulamento, artigo 13(2)'],
|
||||
['key' => 'area_required', 'label' => 'Área de terra requerida', 'type' => 'number', 'when' => ['field' => 'land_extension', 'equals' => 'yes'], 'source' => 'Lei 8/2023, artigo 22(1)(b)(iii)'],
|
||||
['key' => 'area_unit', 'label' => 'Unidade da área requerida', 'options' => ['ha' => 'Hectares', 'm2' => 'Metros quadrados'], 'when' => ['field' => 'land_extension', 'equals' => 'yes'], 'source' => 'Conversão de unidade exacta; não é benefício ou condição adicional'],
|
||||
];
|
||||
}
|
||||
|
||||
private function number(array $f, string $key): ?BigDecimal
|
||||
{
|
||||
$value = $f[$key] ?? '';
|
||||
|
||||
return is_scalar($value) && preg_match('/^\d+(?:\.\d+)?$/D', (string) $value) ? BigDecimal::of((string) $value) : null;
|
||||
}
|
||||
|
||||
public function assess(array $context, array $documents = []): array
|
||||
{
|
||||
$f = $context['fields'] ?? [];
|
||||
$missing = [];
|
||||
$answer = function (string $key) use ($f, &$missing): ?bool {
|
||||
if (! in_array($f[$key] ?? '', ['yes', 'no'], true)) {
|
||||
$missing[] = $key;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return $f[$key] === 'yes';
|
||||
};
|
||||
$enabled = ($f['investment_preparation'] ?? '') === 'yes';
|
||||
$date = $f['expected_start_date'] ?? '';
|
||||
$period = preg_match('/^2026-\d{2}-\d{2}$/D', $date) === 1;
|
||||
$scope = $f['investment_activity_scope'] ?? 'unknown';
|
||||
$covered = in_array($scope, ['general', 'processing'], true);
|
||||
$mode = 'indeterminate';
|
||||
$reasons = [];
|
||||
if ($enabled) {
|
||||
if (! $date) {
|
||||
$missing[] = 'expected_start_date';
|
||||
}
|
||||
if ($scope === 'unknown' || $scope === '') {
|
||||
$missing[] = 'investment_activity_scope';
|
||||
}
|
||||
if (empty($f['currency'])) {
|
||||
$missing[] = 'currency';
|
||||
}
|
||||
if ($covered && $period) {
|
||||
$triggers = [];
|
||||
if ($scope === 'processing') {
|
||||
$triggers['processing'] = true;
|
||||
$reasons[] = 'Processamento industrial mineiro/petrolífero: Lei 8/2023, artigo 22(1)(b)(v).';
|
||||
}
|
||||
$amount = $this->number($f, 'total_investment');
|
||||
$triggers['amount'] = $amount && ($f['currency'] ?? '') === 'MZN' && ($f['investment_form'] ?? '') === 'direct' ? $amount->isGreaterThan('32000000000') : null;
|
||||
if (! in_array($f['investment_form'] ?? '', ['direct', 'indirect'], true)) {
|
||||
$missing[] = 'investment_form';
|
||||
}
|
||||
if (($f['investment_form'] ?? '') === 'indirect') {
|
||||
$reasons[] = 'Investimento indirecto: a classificação pela dimensão e o encaminhamento monetário não são determinados por este lote. As restantes condições documentadas continuam avaliadas.';
|
||||
}
|
||||
if (! $amount) {
|
||||
$missing[] = 'total_investment';
|
||||
}
|
||||
if (($f['currency'] ?? '') !== 'MZN') {
|
||||
$reasons[] = 'A condição monetária não é determinada noutra moeda: não se fez conversão.';
|
||||
}
|
||||
foreach (['project_concession', 'significant_implications'] as $key) {
|
||||
$triggers[$key] = $answer($key);
|
||||
}
|
||||
$land = $answer('land_extension');
|
||||
if ($land === true) {
|
||||
$area = $this->number($f, 'area_required');
|
||||
$unit = $f['area_unit'] ?? '';
|
||||
if (! $area) {
|
||||
$missing[] = 'area_required';
|
||||
}if (! in_array($unit, ['ha', 'm2'], true)) {
|
||||
$missing[] = 'area_unit';
|
||||
}
|
||||
$triggers['land_extension'] = $area && in_array($unit, ['ha', 'm2'], true) ? $area->isGreaterThanOrEqualTo($unit === 'ha' ? '10000' : '100000000') : null;
|
||||
} else {
|
||||
$triggers['land_extension'] = $land;
|
||||
}
|
||||
$forest = $answer('forest_concession');
|
||||
if ($forest === true) {
|
||||
$area = $this->number($f, 'forest_concession_hectares');
|
||||
if (! $area) {
|
||||
$missing[] = 'forest_concession_hectares';
|
||||
}$triggers['forest_concession'] = $area ? $area->isGreaterThan('100000') : null;
|
||||
} else {
|
||||
$triggers['forest_concession'] = $forest;
|
||||
}
|
||||
foreach (['amount' => 'Valor superior a 32 mil milhões MZN — R13(2), R21(2)(a)', 'project_concession' => 'PPP/concessão empresarial — Lei 8/2023, art. 22(1)(b)(ii), R21(2)(b)', 'significant_implications' => 'Implicações significativas declaradas — Lei 8/2023, art. 22(1)(b)(i), R21(1)', 'land_extension' => 'Extensão de terra igual/superior a 10 mil hectares — Lei 8/2023, art. 22(1)(b)(iii), R21(2)(c)', 'forest_concession' => 'Concessão florestal superior a 100 mil hectares — Lei 8/2023, art. 22(1)(b)(iv), R21(2)(d)'] as $condition => $description) {
|
||||
if (($triggers[$condition] ?? null) === true) {
|
||||
$reasons[] = $description.'.';
|
||||
}
|
||||
}
|
||||
$mode = in_array(true, $triggers, true) ? 'authorization' : (in_array(null, $triggers, true) ? 'indeterminate' : 'registration');
|
||||
$reasons[] = $mode === 'authorization' ? 'Foi identificada uma condição de autorização; as restantes condições desconhecidas não foram presumidas negativas.' : ($mode === 'registration' ? 'As condições de autorização deste lote foram declaradas negativas e o montante não ultrapassa o limite.' : 'Falta resolver pelo menos uma condição de autorização.');
|
||||
}
|
||||
foreach (['implementation_mode', 'capital_origin', 'site_tenure', 'entity_stage'] as $key) {
|
||||
if (empty($f[$key]) || $f[$key] === 'unknown') {
|
||||
$missing[] = $key;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($mode === 'authorization' && in_array(true, array_intersect_key($triggers, array_flip(['amount', 'project_concession', 'land_extension', 'forest_concession'])), true)) {
|
||||
$missing = array_values(array_diff($missing, ['investment_form', 'project_concession', 'significant_implications', 'land_extension', 'forest_concession', 'forest_concession_hectares', 'area_required', 'area_unit', 'total_investment']));
|
||||
}
|
||||
$base = $enabled && $covered && $period ? true : null;
|
||||
if (! $enabled || in_array($scope, ['extractive', 'public_nonprofit'], true) || ($date && ! $period)) {
|
||||
$base = false;
|
||||
}
|
||||
$rules = [
|
||||
['identity', 'Identificação do investidor', $base, 'S06', 'R9(1)(a) / R14(1)(a)', 41, 'Identificação do proponente do pedido.'],
|
||||
['registration', 'Certidão de registo comercial', $base === true ? (($f['entity_stage'] ?? '') === 'existing' ? true : (($f['entity_stage'] ?? '') === 'being_formed' ? false : null)) : $base, 'S05', 'R9(1)(b) / R14(1)(b)', 41, 'Alternativa para empresa já registada.'],
|
||||
['name_reservation', 'Reserva da denominação social', $base === true ? (($f['entity_stage'] ?? '') === 'being_formed' ? true : (($f['entity_stage'] ?? '') === 'existing' ? false : null)) : $base, 'S05', 'R9(1)(b) / R14(1)(b)', 41, 'Alternativa para empresa em constituição.'],
|
||||
['site_plan', 'Planta ou esboço do local', $base, 'S06', 'R9(1)(c) / R14(1)(c)', 42, 'Local de implantação identificado; não certifica disponibilidade.'],
|
||||
];
|
||||
foreach (['lease' => 'Contrato de arrendamento', 'duat' => 'Título DUAT'] as $key => $label) {
|
||||
$rules[] = [$key, $label, $base === true ? (($f['site_tenure'] ?? 'unknown') === 'unknown' ? null : ($f['site_tenure'] ?? '') === $key) : $base, 'S06', 'R9(1)(d) / R14(1)(d)', 42, 'Alternativa correspondente à utilização declarada das instalações.'];
|
||||
}
|
||||
$rules[] = ['representation_license', 'Licença de representação comercial', $base === true ? (($f['implementation_mode'] ?? 'unknown') === 'unknown' ? null : ($f['implementation_mode'] ?? '') === 'foreign_representation') : $base, 'S06', 'R9(2)', 42, 'Só para a modalidade representação comercial estrangeira; não decorre da nacionalidade.'];
|
||||
foreach (['feasibility' => 'Estudo de viabilidade técnica e económico-financeira', 'financial_capacity' => 'Comprovação da capacidade financeira'] as $key => $label) {
|
||||
$rules[] = [$key, $label, $base === true ? ($mode === 'indeterminate' ? null : $mode === 'authorization') : $base, 'S02', 'R14(1)–(2)', 45, 'Integra a preparação do pedido de autorização.'];
|
||||
}
|
||||
$rules[] = ['foreign_capital', 'Prova de realização/registo do aporte externo', $base === true ? (($f['capital_origin'] ?? 'unknown') === 'unknown' ? null : in_array($f['capital_origin'] ?? '', ['external', 'mixed'], true)) : $base, 'S06', 'R5(1)–(5)', 39, 'Preparação específica do aporte externo; formalidades cambiais concretas não calculadas.'];
|
||||
$links = $context['preparation_documents'] ?? [];
|
||||
$requirements = [];
|
||||
foreach ($rules as [$key,$label,$applies,$stage,$article,$page,$reason]) {
|
||||
$doc = isset($links[$key]) ? collect($documents)->firstWhere('document_id', $links[$key]) : null;
|
||||
$requirements[] = ['key' => $key, 'label' => $label, 'state' => $applies === false ? 'not_applicable' : ($applies === null ? 'indeterminate' : ($doc ? 'associated' : 'missing')), 'required' => $applies === true, 'document_id' => $links[$key] ?? null, 'document' => $doc, 'stage' => $stage, 'reason' => $reason, 'source' => ['reference' => $article, 'url' => self::SOURCE.'#page='.$page, 'sha256' => self::HASH, 'period' => '2026; Regulamento desde 07/03/2024'], 'legal_verification' => 'not_inferred'];
|
||||
}
|
||||
$authoritySource = ['reference' => 'R13(1)', 'url' => self::SOURCE.'#page=45'];
|
||||
$zone = $context['selection']['park'] ?? null;
|
||||
$amount = $this->number($f, 'total_investment');
|
||||
$authority = 'Por determinar neste lote';
|
||||
if ($base === true && $mode === 'registration' && $amount && ($f['currency'] ?? '') === 'MZN') {
|
||||
$special = in_array($zone['fields']['investment_regime'] ?? '', ['zee', 'zfi'], true);
|
||||
$authority = $special ? ($amount->isLessThanOrEqualTo('6500000000') ? 'APIEX — condicionado ao enquadramento ZEE/ZFI efectivamente reconhecido' : 'Ministro que superintende Finanças') : ($amount->isLessThanOrEqualTo('3500000000') ? 'Governador da Província' : ($amount->isLessThanOrEqualTo('6500000000') ? 'APIEX' : 'Ministro que superintende Finanças'));
|
||||
}
|
||||
if ($base === true && $mode === 'authorization') {
|
||||
$council = array_intersect_key($triggers, array_flip(['amount', 'project_concession', 'land_extension', 'forest_concession']));
|
||||
$authority = in_array(true, $council, true) ? 'Conselho de Ministros' : (in_array(null, $council, true) ? 'Autorização identificada; completar as condições em falta para distinguir a entidade decisória' : 'Ministro que superintende Finanças; eventual submissão ao Conselho de Ministros nos termos do R21(3)');
|
||||
$authoritySource = ['reference' => 'R21(1)–(3)', 'url' => self::SOURCE.'#page=50'];
|
||||
}
|
||||
|
||||
$labels = array_column($this->questions(), 'label', 'key') + ['currency' => 'Moeda do projecto', 'expected_start_date' => 'Data prevista de início', 'entity_stage' => 'Situação da entidade'];
|
||||
$missingLabels = array_map(fn ($key) => $labels[$key] ?? $key, array_values(array_unique($missing)));
|
||||
|
||||
return ['version' => self::VERSION, 'enabled' => $enabled, 'period_covered' => $period, 'scope_covered' => $covered, 'mode' => $mode, 'reason' => $reasons, 'missing_fields' => array_values(array_unique($missing)), 'missing_labels' => $missingLabels, 'questions' => $this->questions(), 'requirements' => $requirements, 'authority' => $authority, 'authority_source' => $authoritySource, 'next' => $base === false ? 'Rever o âmbito/período; este lote não cobre o pedido' : ($mode === 'indeterminate' ? 'Completar condições do enquadramento' : ($mode === 'authorization' ? 'Preparar estudo e documentos do pedido de autorização' : 'Preparar documentos do pedido de mero registo')), 'calculation_notice' => 'O regime procedimental não altera a taxa de IVA. Reutilize a simulação geral apenas no seu âmbito e período; benefícios, apuramento final e conversão cambial não são presumidos.', 'source' => ['url' => self::SOURCE, 'sha256' => self::HASH], 'notice' => 'Preparação informativa baseada em declarações; documentos associados não constituem validação jurídica, certificado ou submissão à Administração.'];
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
return [
|
||||
// Explicit opt-in after authenticated installation; never infer activation from schema/ready.
|
||||
'private_preparation' => env('PRIVATE_INVESTMENT_PREPARATION_ENABLED', false),
|
||||
'enabled' => env('INVESTOR_PATH_ENABLED', false),
|
||||
'definition_id' => '61000919-0000-4000-8000-000000000001',
|
||||
];
|
||||
|
||||
@@ -0,0 +1,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"
|
||||
}
|
||||
@@ -1521,3 +1521,633 @@ Rever a branch e o manifesto nominal; reconciliar novos commits dos programadore
|
||||
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.
|
||||
|
||||
|
||||
### Revisão de a76369eb e primeiro percurso adaptativo fundamentado — v1, 22/10/2026
|
||||
|
||||
**Revisão encerrada no âmbito de a76369eb, sem integração.** O último regresso tem prova equivalente: destino do mesmo dossier observado pelo inventário do navegador e evidência `43317c22-50c8-40cf-98e4-e5d4d108c372` confirmada no serviço, ligada ao rascunho v8/contexto v1 e ao hash `a21fa5779bf8b8df366f16683c8a9bb9817a6d2921c7384f85df02135307fa13`. A observação visual final continuou limitada pela ferramenta. Não se repetiram A/B, cálculos, exportações ou nova sessão; reutilizam-se as provas aceites. Leitura dirigida do delta: isolamento por proprietário/dossier, edições/apagamentos, origem monetária, controlo de escrita e ausência de publicação fiscal. Não foi identificada nova preparação técnica que condicione a decisão de integração deste âmbito. A adaptação jurídica seguinte é independente.
|
||||
|
||||
**Percurso delimitado preparado:** preparação de um pedido de investimento privado para empreendimento económico em Moçambique, com actividade concreta declarada, instalação identificada e operações mensais enquadráveis no consumidor IVA geral existente. Trata a preparação do pedido de investimento; não se apresenta como licenciamento sectorial completo, certificado emitido, direito a incentivo ou submissão à Administração. Investidor nacional/estrangeiro, actividade, modalidade e localização são condições distintas. A saída é formulário orientado, etapas/documentos aplicáveis e simulação identificada pelo seu âmbito. Não se escolhe “Turismo” ou uma ZEE para presumir requisitos ou benefícios.
|
||||
|
||||
**Fonte primária aplicada:** compilação oficial APIEX “Legislação de Investimentos em Moçambique”, exemplar obtido de https://pt.apiex.gov.mz/wp-content/uploads/2025/02/LEGISLACAO-DE-INVESTIMENTOS-EM-MOCAMBIQUE.pdf — 6467655 bytes, SHA256 `ba1e9eaabfec5d0c4cb2d9f2444a6860f7a90f222034764197829be402d408ed`. Contém Lei8/2023 (pp.8–32), Decreto8/2024/Regulamento (pp.36–59), CBF (pp.64–92). Aceitação oficial de08/10 mantida; não há exigência de segunda autenticação ou aprovação jurídica da Tempo. Inventários existentes de604 registos/exemplares/anexos e9901 caminhos do acervo reutilizados antes da selecção; cópia privada, texto extraído e citações em `outputs/adaptive-path-20261022`. Não importar/publicar automaticamente. As pendências históricas genéricas de nova certificação nos levantamentos de28/09 e03/10 não são gates deste lote.
|
||||
|
||||
**Período:** Decreto8/2024, artigo5 do diploma preambular, indica aplicação desde a publicação de7/03/2024; Lei8/2023, artigo32, indica90dias após publicação. O lote proposto cobre datas de projecto em2026 e preserva estes inícios, exemplar e versão. Não foi encontrada alteração nas consultas oficiais dirigidas desta execução; isso não certifica inexistência de actos posteriores. A compilação de2025 não constitui consolidação universal de2026. Na ligação executável, uma fonte/versão sem cobertura temporal não autoriza fallback para a versão mais recente. Os artigos citados são do **Regulamento anexo** quando identificado “R”; “L” designa a Lei8/2023. Rastrear esta distinção evita confundir os dois artigos9 ou o artigo5 preambular com o do Regulamento.
|
||||
|
||||
| Condição | Fonte e período | Efeito no formulário | Efeito no percurso | Efeito no cálculo | Informação em falta / estado da ligação |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| Projecto económico em Moçambique que procura garantias/incentivos; actividade efectiva conhecida | L2(1)–(3), pp.8–9; período2026 deste lote | Reutilizar localização e actividade; perguntar a finalidade jurídica apenas se ausente | S02 identifica cobertura do procedimento; actividades excluídas recebem encaminhamento próprio | Não inferir benefício ou exclusão de IVA a partir deste âmbito | Falta ligação executável da cobertura ao perfil; legislação sectorial especial fica fora deste lote |
|
||||
| Capital/recursos do exterior ou nacionais; pessoa, residência e constituição separadas | L3/anexo, pp.28–29; R4–6, pp.38–40; período2026 | Reutilizar os dados declarados; pedir origem/forma do aporte quando faltar, sem transformar nacionalidade em classificação do capital | Acrescentar preparação da prova/registo do aporte externo quando aplicável | Nenhuma mudança automática de taxa IVA ou conversão; transferências cambiais não calculadas | Falta dado estruturado de aporte e ligação à documentação; requisitos cambiais concretos não abrangidos |
|
||||
| Implementadora em constituição ou já registada | R9(1)(b), p.41; R14(1)(b), p.45; desde7/03/2024, recorte2026 | Mostrar a alternativa documental correspondente, sem exigir novo preenchimento da entidade | S05/S06: reserva de denominação ou certidão comercial, com estado declarado/associado por conferir | Não altera IVA | Falta associar categorias documentais existentes à alternativa e ao estado do cadastro |
|
||||
| Implantação em instalações arrendadas ou com DUAT | R9(1)(c)–(d), pp.41–42; R14(1)(c)–(d), p.46; recorte2026 | Reutilizar local; perguntar apenas a modalidade/evidência ainda ausente | S06 identifica esboço/planta e alternativa contrato/DUAT; sem converter ausência de título em ausência de direito | Não atribui taxa, custo fundiário ou benefício | Falta modalidade estruturada e associação ao documento; sem decidir titularidade/disponibilidade |
|
||||
| Pedido por representação comercial estrangeira, distinto de qualquer investidor estrangeiro | R9(2)–(3), p.42; recorte2026 | Pergunta de representação apenas quando a implantação declarada o permita; depois referência da licença e documento | S06 acrescenta licença de representação e identifica formalidades documentais pertinentes | Sem alteração da taxa geral | Falta modalidade e origem documental estruturadas; não exigir licença a todo investidor estrangeiro |
|
||||
| Nenhuma condição de autorização; ou dimensão/actividade/terra que a exija | L22(1)(b)–(2), pp.20–21; R13(2), R14 e R21, pp.45–50; recorte2026 | Reutilizar montante/moeda/actividade/área; perguntar apenas PPP/concessão, implicações ou dados ainda desconhecidos | Seleccionar preparação de mero registo ou autorização; na segunda, S02/S06 acrescentam estudo de viabilidade e capacidade financeira | Comparação do montante exige MZN ou equivalência documentada; não converter nem inventar taxa; IVA permanece componente separada | Falta ligar os predicados e evidências ao perfil. “Não sei” não equivale a “não”. Limite de32mil milhões MZN só resolve a condição de montante, não todas as restantes |
|
||||
| Localização geral versus ZEE/ZFI com referência e âmbito documentados | R13(1)(a)–(b), p.45; R10(4), p.43; recorte2026 | Reutilizar zona/versionamento; pedir reconhecimento/condições pertinentes em falta | Diferenciar entidade de encaminhamento e articulação aplicável; S08/S17 conservam evidências e pendências | Sem regra executável do benefício, mostrar resultado especial indeterminado e cenário geral separado | a76369eb já apresenta referência/limite; faltam ligações jurídicas de encaminhamento e condições. Parque industrial não equivale automaticamente a ZEE/ZFI |
|
||||
| Operação mensal interna geral, base líquida, MZN e período coberto pela versão publicada | Regra IVA já fundamentada/autorizada; Lei22/2022 art.17(1), fontes/limites no recibo existente; recorte fiscal2026 | Reutilizar vendas/mês/moeda; perguntar apenas pressupostos fiscais ainda ausentes | Ligar cálculo e relatório à mesma versão de contexto/regra | Consumidor existente calcula por versão servida; zero difere de falta; preço com IVA incluído não vira base líquida | Ligação mensal já demonstrada. Dedução, reembolso, saldo final e benefício especial continuam fora |
|
||||
| Mudança de título/contacto ou outra condição irrelevante para a regra aplicada | Decisão Tempo22/10 e contratos existentes; escolha técnica de dependências identificada | Preservar a edição e preencher o que continuar compatível | Não reiniciar revisões/etapas sem dependência relevante | Não criar outra execução nem mudar taxa/base | Prova de título e testes de dependências já aceites; estender por dependência, não por “qualquer alteração” |
|
||||
|
||||
**Diferença útil que a implementação deste lote deve demonstrar, sem repetir A/B:** um caso C exclusivamente sintético, empresa em constituição, actividade económica não extractiva declarada, instalações arrendadas, capital emMZN, período2026. Primeiro completar as condições de enquadramento; depois a preparação de mero registo mostra a alternativa reserva de denominação/arrendamento. Alterar a condição de autorização para verdadeira troca o resultado procedimental, os campos necessários e os documentos para estudo/capacidade financeira. Com condição desconhecida, a selecção fica indeterminada e pede apenas essa informação. Alterar para representação comercial estrangeira acrescenta a licença pertinente; mudar apenas nacionalidade não produz esse efeito. Alterar de localização geral para ZEE/ZFI documentada altera o encaminhamento e as verificações, sem conceder benefício. O IVA geral continua matematicamente igual quando a mudança não afecta a sua regra; mudar vendas/base/moeda/período altera ou impede o cálculo conforme a cobertura existente. Não forçar um resultado numérico diferente quando a lei não o determina.
|
||||
|
||||
**Cadastro e código separados:** o conteúdo jurídico acima está preparado, com artigos, exemplar e início temporal. Falta cadastrar as correspondências versionadas campo/condição/etapa/documento e ligar a sua execução ao perfil existente. Usar o mesmo dossier, prévia/confirmar, definições/questionários versionados e suporte condicional `when` já utilizado nos formulários; reutilizar resolvedor temporal, avaliação e aritmética exacta nas interfaces em que sejam compatíveis, com adaptador explícito para os predicados procedimentais, sem criar outro motor nem armazenar procedimentos como imposto. O contexto já tem sector/actividade/localização/moeda/período/entidade; as respostas de modalidade e evidências ainda não ligadas devem ser acrescentadas ao mesmo contexto cifrado, com origem e alterações deliberadas. Reutilizar vínculos documentais/históricos e permissões existentes. Não alterar a definição publicada dos19passos nem activar outra regra fiscal para executar este cadastro.
|
||||
|
||||
**Estado desta entrega:** revisão de a76369eb concluída no âmbito aceite; **percurso seguinte fundamentado e delimitado, ainda não implementado/demonstrado de ponta a ponta**. A tabela não é prova de funcionalidade nem declara licenciamento sectorial completo. Não integrar este documento como se instalasse o percurso. Próxima execução técnica concreta: ligar as condições acima, persistir decisões/documentos e verificar apenas as transições de C, recuperação e relatório; conservar as provas aceites de contexto e IVA. Código, publicação de parâmetros/regras e activação continuam operações separadas. Não há nova autorização organizacional ou validação jurídica da Tempo presumida.
|
||||
|
||||
|
||||
### Percurso de investimento privado executável em QA — 22/10/2026
|
||||
|
||||
Implementação do delta de `52fbb146`, na branch `codex/private-investment-path-executable-QA`. A integração independente de `a76369eb` não foi repetida nem condicionada por este lote. Não houve escrita na homologação, integração em main, publicação ou activação de regras fiscais.
|
||||
|
||||
**Orientação permanente aplicada:** decisão expressa da Tempo de 22/10, registada também em AGENTS.md: legislação, modelos e procedimentos oficiais disponibilizados são a base aceite para implementar; decisões da Tempo orientam a implementação. Não existe nova aprovação jurídica genérica, aprovador adicional ou validação de credibilidade como condição para executar este percurso. Dados concretamente em falta são identificados por campo e não anulam os restantes requisitos. Associação documental não equivale a reconhecimento administrativo; esta distinção é informativa, sem bloquear a gravação ou a preparação.
|
||||
|
||||
**Ligação executável:** `PrivateInvestmentPreparation` é o adaptador delimitado das condições procedimentais já documentadas para o contexto cifrado do dossier. Correspondência `private-investment-preparation-2026-v1`, exemplar APIEX/hash anteriormente registados, recorte de início previsto em 2026. Reutiliza campos conhecidos do mesmo projecto/rascunho, permissões, vínculos documentais, prévia assinada, controlo de concorrência, chave idempotente, eventos antes/depois e os 19 passos existentes. Não é outro motor fiscal, catálogo paralelo de benefícios ou novo processo de aprovação. O resolvedor fiscal publicado e a aritmética decimal existentes continuam responsáveis pelos seus cálculos; não se armazenaram predicados procedimentais como impostos.
|
||||
|
||||
No dossier normal, «Preparar o pedido de investimento privado» abre o editor de contexto. Valores já declarados ficam recuperados e editáveis, sem nova confirmação por ecrã. Dados condicionais de área só aparecem para extensão de terra/concessão florestal. A prévia mostra ramo antes/depois, documentos novos/inactivos, passos afectados e resultados que precisam de revisão. Apenas a confirmação grava uma revisão; cancelamento conserva o estado anterior. Os requisitos correspondentes aparecem nas etapas S02/S05/S06 e o próximo passo acompanha mero registo/autorização. As dependências formais da definição publicada não foram substituídas por novos bloqueios.
|
||||
|
||||
**Fundamento executado:** Lei 8/2023 artigos 2 e 22; Regulamento aprovado pelo Decreto 8/2024 artigos 5, 9, 13, 14 e 21, conforme a tabela de 52fbb146 acima. Na interface, R designa este Regulamento. As exigências têm razão, artigo, período e ligação ao exemplar oficial. As alternativas certidão/reserva e DUAT/arrendamento não são cumulativas. Representação estrangeira determina a licença correspondente; nacionalidade ou residência, isoladamente, não a determinam. Aporte externo tem origem declarada separadamente da nacionalidade. Montante >32 mil milhões MZN, terra >=10 mil ha e concessão florestal >100 mil ha usam comparação decimal exacta, sem conversão cambial presumida. R21 distingue Finanças/Conselho de Ministros conforme as condições efectivamente conhecidas; uma condição positiva suficiente não espera pelas restantes para identificar o pedido de autorização.
|
||||
|
||||
| Comparação nova, exclusivamente QA | Campos e documentos | Próximo passo / efeito no cálculo | Prova |
|
||||
| --- | --- | --- | --- |
|
||||
| C: empresa em constituição, actividade geral, 1.000.000 MZN, 01/01/2026, modalidade doméstica, capital nacional, instalações arrendadas; condições de autorização negativas | Reserva de denominação, identificação, planta e arrendamento; não exige certidão, DUAT, licença de representação ou estudo/capacidade de autorização | Mero registo; encaminhamento Governador. Nenhuma taxa IVA alterada | UI → prévia → confirmar; contexto v1 |
|
||||
| C: associar suporte sintético já existente ao arrendamento | Mesmo documento associado por ID; não cria cópia nem o aprova | Conserva o pedido; contexto v2 | UI; um vínculo em desk_documents |
|
||||
| C: PPP/concessão empresarial passa a sim, modalidade passa a representação estrangeira e instalações a DUAT | Entram estudo/capacidade, licença de representação e DUAT; arrendamento deixa de ser exigido, conservando o documento anterior | Autorização, Conselho de Ministros; S02/S05/S06/S07 para revisão. Esta mudança meramente procedimental não invalida os inputs do IVA | Prévia visual antes de confirmar; contexto v3 recuperado; PNG C-authorization-preview |
|
||||
| C: recuperação e simulador | MZN, data, título, contexto v3 e preparação por autorização recuperados sem reintrodução; arrendamento inactivo visível no histórico | Mesmo âmbito fiscal geral; nenhuma execução fiscal nova pedida neste ensaio | Dossier normal, simulador e rascunho ligado; leitura persistida e relatório Word |
|
||||
| Regresso autorização → mero registo; entidade em constituição → existente | Estudo/capacidade/licença deixam de ser exigidos; arrendamento anterior volta associado; certidão substitui reserva, sem apagar histórico | Ramo e requisitos recalculados | Controlo HTTP transaccional dirigido, não repetição visual A/B |
|
||||
| ZEE/ZFI versus parque industrial | Encaminhamento APIEX condicionado ao enquadramento reconhecido; parque não é convertido em ZEE/ZFI | Nenhum benefício ou taxa especial concedidos | Predicados com referências sintéticas de QA; não prova de elegibilidade de uma zona real |
|
||||
|
||||
**Provas novas:** três testes feature, 96 asserções, exclusivamente na base `simulator_fiscal_delta_20261021_test`, loopback 55441, com transacções revertidas e guarda do alvo. Cobrem limiares exactos, desconhecidos, exclusões/período, não inferência por nacionalidade, alternativas e preservação de vínculo, recuperação, idempotência, prévia sem escrita, adulteração, documento de outro dossier recusado e consumidor desligado, incluindo edição independente do título sem perder snapshot ou vínculos. Um teste frontend confere somente o novo conteúdo congelado do relatório. TypeScript e build passaram. Não foram repetidos cálculos decimais, exportadores completos, paginação PDF, CSV ou ensaios A/B aceites.
|
||||
|
||||
**Recibo C da fase por autorização (histórico v3):** dossier `4133311b-5426-4cec-900d-1f1484484083`; contexto v3; rascunho `beba1dec-fc05-4820-b099-24952c5ab36c`, v4, SHA256 `4136e82a3b9420ab8f980ec0d7bda6dc2a67b568c18535b5ba0ecc80efd37e37`. Um documento sintético `e5ce142a-1178-46a2-b039-f187139c62cd`, sem pretensão de autenticidade oficial ou reconhecimento; mantém quarentena/scan pendente, sem contornar o carregamento/antimalware. A fixture apenas disponibiliza metadados privados para observar associação, não ensaia novamente uploads e não faz parte de dados/migrações de entrega. Três eventos de mudança do contexto; uma associação documental. C não contém projecção de vendas nem novo resultado fiscal; as provas anteriores dessa componente são reutilizadas.
|
||||
|
||||
**Relatório efectivamente transferido:** `relatorio-investmoz-sintetico-qa-c-pedido-de-investimento-privado.docx`, 16.674 bytes, SHA256 `28cfe4b75116c892d3e29fde389de7d067b456761756bab732af625178ee69ce`. Conferido o novo conteúdo: pedido de autorização, versão, fundamentos R14 e exigências não aplicáveis com histórico preservado. Não se repetiu a verificação de paginação/exportadores. Os anexos do dossier não são inseridos silenciosamente no pacote; mantém-se a selecção explícita existente. O relatório identifica a preparação/simulação e não alega submissão ou aceitação administrativa.
|
||||
|
||||
**Limitação da ferramenta:** a observação do evento de download excedeu o tempo e a extensão perdeu a ligação. O ficheiro terminou e foi encontrado em Transferências antes de qualquer repetição; conteúdo/hash conferidos localmente. Não se repetiu a exportação nem se classificou o incidente como falha do simulador. A prévia de impacto e recuperação já tinham prova visual; a recepção do ficheiro é prova distinta.
|
||||
|
||||
**Limites concretos:** este lote prepara o pedido de garantias/incentivos de investimento privado, não executa submissão administrativa, licenciamento completo por sector ou decisão de elegibilidade. Prospecção/pesquisa/produção extractiva, investimento público/exclusivamente social sem fins lucrativos e períodos fora de 2026 não são cobertos por este adaptador; processamento industrial tem o ramo de autorização documentado. A equivalência de montante noutra moeda não é determinada sem dados/fundamento cambial aplicáveis; condições positivas independentes continuam executáveis. O catálogo visual deste QA contém apenas parques sintéticos, não uma ZEE/ZFI real elegível: encaminhamento especial conferido no predicado, sem declarar prova visual de reconhecimento real. Benefícios territoriais, deduções, reembolsos e apuramento final do IVA continuam indisponíveis; o cálculo geral já autorizado conserva o seu âmbito.
|
||||
|
||||
**Utilização e integração:** abrir `http://127.0.0.1:18580/investimentos/4133311b-5426-4cec-900d-1f1484484083` na conta QA habitual. Usar «Comparar e alterar contexto», editar apenas a condição relevante, comparar e confirmar; para documentos, usar o Balcão do mesmo dossier e associar os existentes. QA habilita `investor-path.private_preparation=true` no router privado. A configuração de entrega `PRIVATE_INVESTMENT_PREPARATION_ENABLED` fica desligada por defeito; a homologação não foi alterada. Integração eventual pelo processo Git existente: rever branch/código e limites, incorporar sobre main actualizado preservando alterações concorrentes, build e conferir apenas o delta no destino; habilitação deste percurso é uma configuração separada, não publicação/activação fiscal. Suspender a configuração oculta o novo consumidor e recusa novas escritas específicas, sem eliminar contexto, documentos, eventos, rascunhos ou relatórios congelados. Sem migração ou seed de fixtures no destino. Nenhuma nova ronda de preparação técnica foi identificada para este recorte; revisão/decisão de integração continuam separadas.
|
||||
|
||||
|
||||
**Distinção final conferida, sem nova validação genérica:** Lei 8/2023 artigo 14(b) distingue investimento directo/indirecto; R13 refere expressamente investimento directo nos limiares monetários. Acrescentado o dado «Tipo deste investimento», separado da origem/nacionalidade. Sem esse dado, a via dependente da dimensão permanece indeterminada; para forma indirecta este lote não determina dimensão/encaminhamento monetário, mas continua a avaliar as condições independentes documentadas. PPP/concessão positiva continua a determinar autorização e Conselho de Ministros, sem esperar por esse campo irrelevante à decisão já suficiente. A interface foi conferida: retirar a condição PPP pede exactamente o tipo em falta; declarar directo resolve mero registo e retira estudo/capacidade financeira. Comparação antes de confirmar em `C-final-directness-preview.png` e percurso recuperado em `C-final-path.png`.
|
||||
|
||||
Estado final C: dossier v8, contexto **v4**, investimento directo declarado, PPP não, pedido de **mero registo**; quatro eventos de contexto e um documento/vínculo conservados. A modalidade estrangeira/DUAT conservou a edição deliberada. O rascunho v4 e o Word já transferido mantêm o contexto **v3 por autorização**, com hash acima: são prova histórica congelada e não o relatório da revisão v4. Não houve reescrita silenciosa nem reexportação. Ao abrir o simulador, o mecanismo comum de continuidade disponibiliza a revisão actual para o próximo rascunho/relatório; as provas anteriores deste mecanismo continuam reutilizadas.
|
||||
|
||||
|
||||
### Correspondência do novo relatório com o contexto v4 e dependências Git — 22/10/2026
|
||||
|
||||
Verificação dirigida solicitada pela Tempo após a avaliação do resumo de `a1fbda06`; nenhuma alteração funcional necessária. A prova anterior só demonstrava o Word congelado v3. Nesta continuação abriu-se o simulador pelo link normal do mesmo dossier C (incluindo o rascunho ligado), recuperou-se automaticamente o contexto v4 e gerou-se **uma** saída Word nova. Não se repetiram cálculos, CSV, paginação, anexos ou ensaios A/B aceites; não houve nova validação jurídica, integração ou alteração fiscal.
|
||||
|
||||
| Elemento | Word histórico v3 | Novo Word do contexto v4 |
|
||||
| --- | --- | --- |
|
||||
| Identificação dentro do documento | «Revisão 3» e «Contexto congelado v3» | «Revisão 4» e «Contexto congelado v4» |
|
||||
| Pedido | Autorização | Mero registo |
|
||||
| Estudo de viabilidade | Documento por associar | Não aplicável; histórico preservado |
|
||||
| Capacidade financeira | Documento por associar | Não aplicável; histórico preservado |
|
||||
| Bytes / SHA256 | 16.674 / `28cfe4b75116c892d3e29fde389de7d067b456761756bab732af625178ee69ce` | 16.678 / `a2849427f4a91fa6ab78e433be8b919baca80a0f4f452a4546a5b169b921d753` |
|
||||
| Cópia identificada para revisão | `outputs/private-path-executable-20261022/C-report-HISTORICO-contexto-v3.docx` | `outputs/private-path-executable-20261022/C-report-contexto-v4.docx` |
|
||||
|
||||
O original v3 conserva exactamente os bytes/hash anteriores; a cópia está explicitamente designada **HISTÓRICO**. A revisão congelada no próprio Word identifica o contexto a que pertence, sem o transformar no estado actual. O novo ficheiro foi encontrado em Transferências após o clique de exportação e conferido por leitura das células efectivamente geradas; recibo em `C-report-v3-v4-comparison.json`. O rascunho passou a v5, SHA256 `ba824fe0362ca87b301da55298122796f44fdfb11507830b3fe8f54cf88d132d`, com contexto v4 e preparação por mero registo; dossier v8/contexto v4 e um documento mantidos. Prova visual de recuperação em `C-new-report-context-v4.png`. Não se altera a validade histórica da prova v3.
|
||||
|
||||
**Dependências para integração após a avaliação da Tempo:** ancestralidade confirmada `57fc9148 → a76369eb → 52fbb146 → a1fbda06`. `a76369eb` fornece a continuidade comum do contexto/edições e é dependência funcional de `a1fbda06`; pode ser integrado independentemente. `52fbb146` é a preparação documental intermédia, sem código executável. A branch `codex/private-investment-path-executable-QA` inclui essa ancestralidade e a presente prova documental; não aplicar apenas o diff de `a1fbda06` num destino sem a continuidade ou alteração equivalente. Se a76369eb já tiver sido integrado, o merge normal reconhece a ancestralidade e não o reaplica como um segundo desenvolvimento.
|
||||
|
||||
Procedimento: após a decisão, recuperar o main actual, rever a comparação e fazer integração Git que preserve as alterações concorrentes; resolver apenas conflitos efectivos, sem reset/force-push de main. A leitura desta continuação encontrou main em `57fc914821d0cefa02caa80a74b3d42e663f2742`; é fotografia do estado, não autorização para ignorar commits posteriores. Conferir a revisão servida e só o delta no destino. A habilitação `PRIVATE_INVESTMENT_PREPARATION_ENABLED` é configuração distinta; não publica nem activa regras fiscais. Nesta execução a branch permanece separada, sem integração/habilitação na homologação. Limitações sectoriais/fiscais explícitas mantêm âmbito próprio e não bloqueiam este recorte concluído.
|
||||
|
||||
|
||||
### Integração autorizada do percurso adaptativo — 22/10/2026
|
||||
|
||||
A Tempo autorizou a implementação após o fecho do recorte. Main remoto foi actualizado por avanço directo de `57fc9148` para `064296e1`, preservando a sequência `a76369eb → 52fbb146 → a1fbda06 → 064296e1`; não havia alteração concorrente ou conflito no momento do envio. A cadeia automática recusou a primeira tentativa no teste frontend «server draft snapshots split private identity from economic values»: 188/189 passaram. A versão servida anterior `57fc9148` foi conservada, conforme o mecanismo de recuperação; não se contornou a verificação.
|
||||
|
||||
Causa concreta: o adaptador de continuidade acrescentava `compatible_fields_snapshot` vazio na recuperação de um rascunho legado sem nenhuma origem compatível. Correcção delimitada na branch `codex/adaptive-delivery-compatibility-QA`: devolver o rascunho original quando não há origem compatível nem metadados de herança anteriores. A herança com origem/metadados continua executada, preservando edições e limites por dossier. Nove controlos dirigidos da herança e o teste exacto da partição/recuperação passaram; não se reabriram os cálculos, relatórios ou cenários aceites. A correcção deve seguir o mesmo processo automático; não altera regras fiscais, esquema ou dados activos. Recibos da tentativa e da disponibilização final em `outputs/investmoz-adaptive-path-delivery-20261022`.
|
||||
|
||||
|
||||
### Percurso adaptativo disponibilizado na homologação — 22/10/2026, 10:17 Maputo
|
||||
|
||||
Autorização expressa da Tempo executada. Main passou de `57fc9148` por avanço directo para `064296e1`, conservando a ancestralidade `a76369eb → 52fbb146 → a1fbda06 → 064296e1`. O remoto não tinha alterações concorrentes adicionais no momento da integração; não houve conflito, reset ou envio forçado. A primeira verificação automática recusou o novo metadado vazio na recuperação legada e conservou a release anterior; a causa e a correcção dirigida constam da secção anterior. A correcção `042ffbde44cac27704d6f8b7ec25b525a42e3575` foi ensaiada em branch e integrada por avanço directo, sem suprimir o teste que detectou a incompatibilidade.
|
||||
|
||||
**Revisão efectivamente servida:** `042ffbde44cac27704d6f8b7ec25b525a42e3575`, branch main, build de 22/10/2026 às 10:16:14 Maputo; instalação automática concluída às 10:16:16. Manifesto SHA256 `655eef6e6a5e71c5138c68465a5b2ffc36d2cadeff96e6b35682dbfd19d42e79`. Conferida em `https://teste-investidor.tempo.co.mz/build/revision.json` e no recibo protegido da cadeia. A cadeia correu as verificações próprias; manualmente só foram repetidos os dez controlos afectados pela incompatibilidade. Zero migrations e zero importações no destino, sem copiar fixtures QA.
|
||||
|
||||
**Funcionalidade utilizável:** `https://teste-investidor.tempo.co.mz/investimentos` (login normal); abrir ou criar o dossier legítimo do investimento e usar «Preparar o pedido de investimento privado» / «Comparar e alterar contexto». Estão disponíveis as perguntas condicionais, fundamentos, comparação antes de confirmação, requisitos/documentos e próximos passos do lote `private-investment-preparation-2026-v1`, com os limites de 2026 já documentados. A conferência do destino limitou-se à revisão, configuração efectiva e entradas: investimentos redirecciona ao login (302), simulador responde 200. Não se criaram dossiers/contas reais ou sintéticas no destino nem se repetiram os cenários funcionais QA aceites.
|
||||
|
||||
**Habilitação concreta, distinta de fiscalidade:** a configuração do percurso `PRIVATE_INVESTMENT_PREPARATION_ENABLED` estava ausente/false; passou a true mediante a sessão SSH habitual autenticada pelo utilizador. Antes da escrita foram conferidos revisão servida e hash da configuração; cópia protegida dos bytes anteriores verificada, alteração limitada a essa chave, proprietário e modo0640 preservados, cache de configuração reconstruída e workers notificados de reinício gracioso. Hash .env anterior `7cdaba591bb5787485a349c24db2fd7c8279436da0eb31502e44db2fffa21d03`; posterior `a6b4e9064cf6dbb2d945b53418a6c9252f9c736ff3fde4016b95b74885a9ee0f`. A leitura posterior confirmou `investor-path.enabled=true` e `private_preparation=true`. A cópia de recuperação permanece privada em `/var/lib/investmoz-deploy/backups/private-preparation-20261022/`; não contém uma publicação de parâmetros e não segue para Git.
|
||||
|
||||
**Estado fiscal preservado:** nenhuma publicação/activação de nova regra e nenhuma operação na base activa. O consumidor IVA geral previamente autorizado manteve a mesma disponibilidade, cobertura2026 e mensagem antes/depois. As exclusões sectoriais, forma indirecta/dimensão monetária não determinada, benefícios territoriais e apuramento fiscal completo continuam explícitos e não bloqueiam o lote procedimental concluído.
|
||||
|
||||
**Suspensão/recuperação:** desligar somente a chave do novo percurso e reconstruir cache/reiniciar graciosamente os workers mantém dossiers, documentos, respostas, versões, relatórios e históricos. Para reverter configuração, usar o valor anterior apenas depois de conferir o hash actual e preservar alterações posteriores; não substituir todo o .env sem conferir concorrência. A reversão de código/build segue o procedimento de releases já existente, sem restaurar dados antigos. A release anterior foi efectivamente conservada na primeira tentativa recusada. Recibo completo local em `outputs/investmoz-adaptive-path-delivery-20261022/delivery-receipt.json`; a presente actualização documental fica na branch de registo, sem novo push main ou nova disponibilização desnecessária.
|
||||
|
||||
@@ -7381,3 +7381,12 @@ P1,AUD-SEARCH-T08,QA — revisão Tempo pendente,Nenhuma alteração de relaçõ
|
||||
P1,AUD-SEARCH-T09,QA — revisão Tempo pendente,Gestão de procura/capacidade permanece separada e protegida; sem alteração de permissões.,Reutiliza integralmente a pesquisa corrigida com âmbito educação mantido.,,HTTP/Inertia: results.total=0 com texto inexistente; filtros e componente comuns.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,investment/catalogue,Gestão de procura/capacidade permanece separada e protegida; sem alteração de permissões.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia: results.total=0 com texto inexistente; filtros e componente comuns.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
|
||||
P1,AUD-SEARCH-T10,QA — revisão Tempo pendente,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,"Resumo único nomeado Documentos, chips removíveis, controlos vazios ocultos sem apagar valores seleccionados, rodapé comum.",,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portal/services; portal/document,Não houve importação/publicação; a cópia QA usada não contém recursos públicos reais para nova prova positiva.,,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia e navegador: vazio e termos preservados; retorno documental existente conservado.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
|
||||
P1,AUD-SEARCH-T11,QA — revisão Tempo pendente,"Situação jurídica, consulta e cálculo continuam independentes.",Reutiliza a correcção da biblioteca e preserva filtros jurídicos/documentais pertinentes.,,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,,,,,,navegação/pesquisa,"Conversa Tempo 20/10/2026; manual, secção Pesquisa transversal QA",Consulta coerente e compreensível; nenhuma alteração de dados para coincidir contagens.,portal/services; portal/document,"Situação jurídica, consulta e cálculo continuam independentes.",,Rever a branch antes de integrar em main; sem disponibilização remota nesta execução.,,,Pesquisa transversal QA 20/10/2026,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,,,,Apenas delta identificado; não certifica funcionalidades não alteradas.,
|
||||
P1,AUD-GUIDE-CARD-20261020,QA review pending,New delta; not approved for main,Common educational card and existing source/context adapter,,Desktop/mobile/keyboard; public-state and source checks,,,,,,educational presentation,Tempo 20/10/2026; same manual,,,Tempo visual review; library integration verified on existing QA; previous dossier proof retained,,Integration approval for new delta,,,,1 test / 8 assertions; TypeScript/build; real public-source visual preview,,,,No import; no remote changes; no claim of new dossier proof,
|
||||
P1,AUD-SIM-INT01,Previous inline-date proof retained; unchanged delta not repeated,Invalid one-off fiscal date can throw instead of returning validation state,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Previous inline-date proof retained; unchanged delta not repeated,,,,No claim of full review completion,
|
||||
P1,AUD-SIM-INT02,QA zero rules verified with full visibility; prepared archive and analyses reused; current homologation read awaits SSH authentication,Review actual fiscal parameters sources periods and eligibility,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,QA zero rules verified with full visibility; prepared archive and analyses reused; current homologation read awaits SSH authentication,,,,No claim of full review completion,
|
||||
P1,AUD-SIM-INT03,Two dossiers and new login recovered in browser; cashflow persistence corrected and HTTP-tested; new visual cashflow delta pending,Review isolation recovery across projects and sessions,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Two dossiers and new login recovered in browser; cashflow persistence corrected and HTTP-tested; new visual cashflow delta pending,,,,No claim of full review completion,
|
||||
P1,AUD-SIM-INT04,Historical results visible after fix; 12 and 24 recovered; hashes unchanged; synthetic temporal resolver versions verified in memory only,Review stale and historical results and correct dossier,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Historical results visible after fix; 12 and 24 recovered; hashes unchanged; synthetic temporal resolver versions verified in memory only,,,,No claim of full review completion,
|
||||
P1,AUD-SIM-INT05,Independent business-plan and cashflow ledgers passed; complete credit recipe included in screen and exports,Independent financial controls unknown zero units and currencies,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Independent business-plan and cashflow ledgers passed; complete credit recipe included in screen and exports,,,,No claim of full review completion,
|
||||
P1,AUD-SIM-INT06,Normal UI CSV and DOCX match shared values; all six DOCX pages checked; native Edge PDF awaits user save; new cashflow visual output pending,Generated reports screen download and pagination,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Normal UI CSV and DOCX match shared values; all six DOCX pages checked; native Edge PDF awaits user save; new cashflow visual output pending,,,,No claim of full review completion,
|
||||
P1,AUD-SIM-INT07,Calculation navigation and failed-save retry fixed and browser-verified; normal downloads verified; interactive attachment blocked by extension permission,Explicit validate calculate save export errors and retry,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Calculation navigation and failed-save retry fixed and browser-verified; normal downloads verified; interactive attachment blocked by extension permission,,,,No claim of full review completion,
|
||||
P1,AUD-SIM-INT08,Generic HTTP and fiscal-expression validation messages translated; status/correlation verified; selectable translations and source titles preserved,Portuguese across portal and simulator outputs,,,,,,,,,simulator and shared language,Tempo request 20/10/2026; same manual,,,,,No new activation authorised,,,,Generic HTTP and fiscal-expression validation messages translated; status/correlation verified; selectable translations and source titles preserved,,,,No claim of full review completion,
|
||||
|
||||
|
Can't render this file because it is too large.
|
@@ -2990,3 +2990,19 @@ Provas: outputs/investmoz-git-delivery-20261020/RECIBO-DISPONIBILIZACAO-GIT-MAIN
|
||||
## 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 (
|
||||
|
||||
@@ -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,232 @@
|
||||
import {
|
||||
PreparationSummary,
|
||||
type Preparation,
|
||||
} from '@/components/private-investment-preparation';
|
||||
import { Link } from '@inertiajs/react';
|
||||
|
||||
type Snapshot = {
|
||||
preparation?: Preparation | null;
|
||||
context_revision: number;
|
||||
path?: {
|
||||
definition_version: number;
|
||||
steps: Array<{
|
||||
code: string;
|
||||
title: string;
|
||||
applicability: string;
|
||||
reason: string;
|
||||
state: string;
|
||||
context_review_required?: boolean;
|
||||
}>;
|
||||
};
|
||||
zone?: {
|
||||
title: string;
|
||||
href: string;
|
||||
version: number | string;
|
||||
fields?: Record<string, string>;
|
||||
analysis?: {
|
||||
conditions?: Array<{
|
||||
id: string;
|
||||
label: string;
|
||||
knowledge: string;
|
||||
source_reference?: string;
|
||||
valid_from?: string;
|
||||
valid_until?: string;
|
||||
}>;
|
||||
source?: {
|
||||
title: string;
|
||||
url: string;
|
||||
diploma?: string;
|
||||
article?: string;
|
||||
valid_from?: string;
|
||||
valid_until?: string;
|
||||
} | null;
|
||||
};
|
||||
} | null;
|
||||
fields?: Record<string, string>;
|
||||
};
|
||||
export function InvestmentContextAssessment({
|
||||
raw,
|
||||
contextId,
|
||||
}: {
|
||||
raw: unknown;
|
||||
contextId?: string | null;
|
||||
}) {
|
||||
let snapshot: Snapshot | null = null;
|
||||
try {
|
||||
const value = JSON.parse(String(raw ?? ''));
|
||||
if (value.version === 1) snapshot = value;
|
||||
} catch {
|
||||
/* Missing context remains explicit. */
|
||||
}
|
||||
if (!snapshot) return null;
|
||||
const fields = snapshot.fields ?? {};
|
||||
const zone = snapshot.zone;
|
||||
const regime = zone?.fields?.investment_regime ?? fields.investment_regime;
|
||||
const names: Record<string, string> = {
|
||||
zee: 'ZEE',
|
||||
zfi: 'ZFI',
|
||||
industrial_park: 'Parque industrial',
|
||||
};
|
||||
return (
|
||||
<section
|
||||
className="space-y-2 rounded border border-amber-400 p-3"
|
||||
aria-label="Aplicabilidade ao investimento"
|
||||
>
|
||||
<h3 className="font-semibold">
|
||||
Aplicabilidade ao investimento · contexto v
|
||||
{snapshot.context_revision}
|
||||
</h3>
|
||||
<p>
|
||||
Origem do investidor:{' '}
|
||||
{{
|
||||
national: 'nacional',
|
||||
foreign: 'estrangeiro',
|
||||
mixed: 'misto',
|
||||
}[fields.investor_origin] ?? 'informação em falta'}
|
||||
. Residência fiscal declarada:{' '}
|
||||
{fields.tax_residency_country || 'informação em falta'}. Estas
|
||||
condições são distintas; uma não determina a outra.
|
||||
</p>
|
||||
{zone ? (
|
||||
<>
|
||||
<p>
|
||||
<strong>
|
||||
Potencialmente aplicável — análise específica de{' '}
|
||||
{names[regime ?? ''] ?? 'zona seleccionada'}.
|
||||
</strong>{' '}
|
||||
<Link className="underline" href={zone.href}>
|
||||
{zone.title} · versão {zone.version} · consultar
|
||||
ficha e fundamentos
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
A selecção identifica a referência consultada; não
|
||||
confirma o reconhecimento da entidade, actividade
|
||||
autorizada, licença ou acesso a benefícios. Consulte as
|
||||
condições documentadas na ficha e reúna a autorização ou
|
||||
evidência correspondente no dossier.
|
||||
</p>
|
||||
{zone.analysis?.source && (
|
||||
<p>
|
||||
Fundamento cadastrado:{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={zone.analysis.source.url}
|
||||
>
|
||||
{zone.analysis.source.diploma ||
|
||||
zone.analysis.source.title}{' '}
|
||||
{zone.analysis.source.article}
|
||||
</a>
|
||||
. Período cadastrado:{' '}
|
||||
{zone.analysis.source.valid_from ||
|
||||
'início por indicar'}{' '}
|
||||
a{' '}
|
||||
{zone.analysis.source.valid_until ||
|
||||
'sem termo indicado'}
|
||||
. A cobertura do investimento exige correspondência
|
||||
com estas condições.
|
||||
</p>
|
||||
)}
|
||||
{zone.analysis?.conditions?.length ? (
|
||||
<ul className="list-disc pl-5">
|
||||
{zone.analysis.conditions.map((condition) => (
|
||||
<li key={condition.id}>
|
||||
<strong>{condition.label}</strong>:{' '}
|
||||
{condition.knowledge === 'documented'
|
||||
? 'condição documentada; cumprimento pelo investimento por confirmar'
|
||||
: condition.knowledge === 'disputed'
|
||||
? 'condição com informação divergente'
|
||||
: 'informação em falta'}
|
||||
.{' '}
|
||||
{condition.source_reference && (
|
||||
<>
|
||||
Fundamento:{' '}
|
||||
{condition.source_reference}.
|
||||
</>
|
||||
)}{' '}
|
||||
{condition.valid_from && (
|
||||
<>Desde {condition.valid_from}.</>
|
||||
)}{' '}
|
||||
{condition.valid_until && (
|
||||
<>Até {condition.valid_until}.</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p>
|
||||
Condições específicas: informação estruturada em
|
||||
falta nesta referência. A ficha e os documentos
|
||||
continuam consultáveis; não se presume cumprimento.
|
||||
</p>
|
||||
)}
|
||||
<p role="status">
|
||||
Benefício fiscal deste enquadramento: indeterminado. Não
|
||||
existe neste consumidor uma regra executável ligada ao
|
||||
reconhecimento e condições da zona. No simulador, o IVA
|
||||
disponível corresponde apenas ao cenário geral sem
|
||||
benefício, sobre vendas declaradas compatíveis; não
|
||||
simula o regime especial. O cenário com benefício e o
|
||||
apuramento final estão indisponíveis.
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<p>
|
||||
Enquadramento geral de preparação: sem ZEE, ZFI ou parque
|
||||
seleccionado. Isso não confirma a elegibilidade para uma
|
||||
operação fiscal. O cálculo mantém o âmbito e as exclusões da
|
||||
versão publicada.
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
Confirmado neste quadro significa apenas dado recuperado do
|
||||
dossier; declarações e referências não são decisões jurídicas.
|
||||
Requisitos específicos não documentados permanecem
|
||||
indeterminados.
|
||||
</p>
|
||||
{snapshot.path && (
|
||||
<details>
|
||||
<summary>
|
||||
Passos adaptados pelo percurso existente · versão{' '}
|
||||
{snapshot.path.definition_version}
|
||||
</summary>
|
||||
<ul className="list-disc pl-5">
|
||||
{snapshot.path.steps.map((step) => (
|
||||
<li key={step.code}>
|
||||
{step.code} — {step.title}:{' '}
|
||||
{step.context_review_required
|
||||
? 'informação alterada; rever fundamento'
|
||||
: step.applicability ===
|
||||
'not_needed_for_preparation'
|
||||
? 'não aplicável à preparação declarada'
|
||||
: step.applicability === 'undetermined'
|
||||
? 'informação em falta'
|
||||
: ['completed', 'approved'].includes(
|
||||
step.state,
|
||||
)
|
||||
? 'preparação confirmada no percurso; não é decisão fiscal'
|
||||
: 'potencialmente aplicável'}
|
||||
. {step.reason}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</details>
|
||||
)}
|
||||
{snapshot.preparation?.enabled && (
|
||||
<PreparationSummary
|
||||
data={snapshot.preparation}
|
||||
caseId={contextId ?? undefined}
|
||||
/>
|
||||
)}
|
||||
{contextId && (
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/investimentos/${contextId}/contexto/editar`}
|
||||
>
|
||||
Alterar contexto e comparar impacto antes de guardar
|
||||
</Link>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,457 @@
|
||||
import { Link } from '@inertiajs/react';
|
||||
import { card, input } from '@/pages/investment/shared';
|
||||
|
||||
export type PreparationDocument = {
|
||||
document_id: string;
|
||||
file_name: string;
|
||||
status: string;
|
||||
malware_scan_status: string;
|
||||
};
|
||||
export type Preparation = {
|
||||
version: string;
|
||||
enabled: boolean;
|
||||
mode: string;
|
||||
period_covered: boolean;
|
||||
scope_covered: boolean;
|
||||
reason: string[];
|
||||
missing_fields: string[];
|
||||
missing_labels?: string[];
|
||||
authority: string;
|
||||
next: string;
|
||||
calculation_notice: string;
|
||||
notice: string;
|
||||
source: { url: string; sha256: string };
|
||||
authority_source: { reference: string; url: string };
|
||||
questions: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
type?: string;
|
||||
options?: Record<string, string>;
|
||||
when?: { field: string; equals: string };
|
||||
source: string;
|
||||
help?: string;
|
||||
}>;
|
||||
requirements: Array<{
|
||||
key: string;
|
||||
label: string;
|
||||
state: string;
|
||||
required: boolean;
|
||||
document_id: string | null;
|
||||
document: PreparationDocument | null;
|
||||
stage: string;
|
||||
reason: string;
|
||||
source: { reference: string; url: string; period: string };
|
||||
}>;
|
||||
};
|
||||
const states: Record<string, string> = {
|
||||
not_applicable: 'Não aplicável neste contexto',
|
||||
indeterminate: 'Informação em falta para decidir',
|
||||
missing: 'Documento por associar',
|
||||
associated: 'Documento associado — conteúdo por conferir',
|
||||
};
|
||||
export const preparationMode = (mode: string) =>
|
||||
({
|
||||
authorization: 'Pedido de autorização',
|
||||
registration: 'Pedido de mero registo',
|
||||
indeterminate: 'Enquadramento por determinar',
|
||||
})[mode] ?? mode;
|
||||
|
||||
export function PreparationSummary({
|
||||
data,
|
||||
caseId,
|
||||
}: {
|
||||
data: Preparation | null | undefined;
|
||||
caseId?: string;
|
||||
}) {
|
||||
if (!data) return null;
|
||||
return (
|
||||
<section
|
||||
id="private-investment-preparation"
|
||||
className={card + ' space-y-3'}
|
||||
aria-label="Preparação adaptada do investimento privado"
|
||||
>
|
||||
<h2 className="text-xl font-bold">
|
||||
Preparar o pedido de investimento privado
|
||||
</h2>
|
||||
{!data.enabled ? (
|
||||
<p>
|
||||
Use os dados deste dossier para identificar o pedido e os
|
||||
documentos aplicáveis. A preparação é opcional e não atribui
|
||||
benefícios.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<p>
|
||||
<strong>{preparationMode(data.mode)}</strong> · recorte
|
||||
de 2026 ·{' '}
|
||||
{data.version.replace(
|
||||
'private-investment-preparation-',
|
||||
'',
|
||||
)}
|
||||
</p>
|
||||
<p>{data.next}</p>
|
||||
{data.missing_fields.length > 0 && (
|
||||
<p>
|
||||
Informação em falta:{' '}
|
||||
{(
|
||||
data.missing_labels ??
|
||||
data.missing_fields.map(
|
||||
(key) =>
|
||||
data.questions?.find(
|
||||
(q) => q.key === key,
|
||||
)?.label ??
|
||||
(
|
||||
{
|
||||
currency: 'Moeda do projecto',
|
||||
expected_start_date:
|
||||
'Data prevista de início',
|
||||
entity_stage:
|
||||
'Situação da entidade',
|
||||
} as Record<string, string>
|
||||
)[key] ??
|
||||
key,
|
||||
)
|
||||
).join('; ')}
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
{!data.period_covered && (
|
||||
<p>
|
||||
Este incremento cobre pedidos com início previsto em
|
||||
2026. Uma data em falta ou fora desse recorte não
|
||||
permite determinar o pedido por este lote; os
|
||||
restantes trabalhos do dossier continuam
|
||||
disponíveis.
|
||||
</p>
|
||||
)}
|
||||
{!data.scope_covered && (
|
||||
<p>
|
||||
Declare a actividade efectiva. Prospecção,
|
||||
pesquisa/produção extractiva e investimento público
|
||||
ou exclusivamente social/sem fins lucrativos seguem
|
||||
enquadramentos próprios, fora deste lote.
|
||||
</p>
|
||||
)}
|
||||
{data.reason.map((reason) => (
|
||||
<p key={reason}>{reason}</p>
|
||||
))}
|
||||
<p>
|
||||
Encaminhamento indicativo: {data.authority}.{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={data.authority_source.url}
|
||||
>
|
||||
{data.authority_source.reference}
|
||||
</a>
|
||||
</p>
|
||||
<ul className="space-y-3">
|
||||
{data.requirements
|
||||
.filter((r) => r.state !== 'not_applicable')
|
||||
.map((r) => (
|
||||
<li key={r.key} className="rounded border p-3">
|
||||
<strong>{r.label}</strong> —{' '}
|
||||
{states[r.state]}.{' '}
|
||||
{r.document?.file_name && (
|
||||
<span>
|
||||
Ficheiro: {r.document.file_name}
|
||||
.{' '}
|
||||
</span>
|
||||
)}
|
||||
<p>
|
||||
{r.reason}{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={r.source.url}
|
||||
>
|
||||
Ver fundamento: {r.source.reference}
|
||||
</a>{' '}
|
||||
· {r.source.period}.
|
||||
</p>
|
||||
{caseId && (
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/investimentos/${caseId}/contexto/editar#preparation-documents`}
|
||||
>
|
||||
Rever dados ou associar documento do
|
||||
dossier
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<details>
|
||||
<summary>
|
||||
Exigências que não se aplicam — histórico preservado
|
||||
</summary>
|
||||
{data.requirements
|
||||
.filter((r) => r.state === 'not_applicable')
|
||||
.map((r) => (
|
||||
<p key={r.key}>
|
||||
{r.label}: {r.reason}{' '}
|
||||
{r.document_id && (
|
||||
<>
|
||||
Associação anterior mantida
|
||||
{r.document?.file_name
|
||||
? `: ${r.document.file_name}`
|
||||
: ''}
|
||||
.
|
||||
</>
|
||||
)}{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={r.source.url}
|
||||
>
|
||||
{r.source.reference}
|
||||
</a>
|
||||
</p>
|
||||
))}
|
||||
</details>
|
||||
<p>{data.calculation_notice}</p>
|
||||
<p>{data.notice}</p>
|
||||
</>
|
||||
)}
|
||||
<a className="underline" href={data.source.url}>
|
||||
Consultar o exemplar oficial e os fundamentos
|
||||
</a>
|
||||
{caseId && (
|
||||
<p>
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/investimentos/${caseId}/contexto/editar#preparation-questions`}
|
||||
>
|
||||
Completar apenas os dados em falta ou editar declarações
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreparationEditor({
|
||||
data,
|
||||
fields,
|
||||
changes,
|
||||
documents,
|
||||
links,
|
||||
onChange,
|
||||
onLink,
|
||||
disabled,
|
||||
}: {
|
||||
data: Preparation;
|
||||
fields: Record<string, string>;
|
||||
changes: Record<string, string>;
|
||||
documents: PreparationDocument[];
|
||||
links: Record<string, string | null>;
|
||||
onChange: (key: string, value: string) => void;
|
||||
onLink: (key: string, value: string | null) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
const values = { ...fields, ...changes };
|
||||
const active = values.investment_preparation === 'yes';
|
||||
const excluded = ['extractive', 'public_nonprofit'].includes(
|
||||
values.investment_activity_scope,
|
||||
);
|
||||
const applicable = (q: Preparation['questions'][number]) =>
|
||||
!q.when || values[q.when.field] === q.when.equals;
|
||||
// Keep a field in place while the user edits it; collapse only recovered answers.
|
||||
const known = (q: Preparation['questions'][number]) =>
|
||||
!!fields[q.key] && fields[q.key] !== 'unknown';
|
||||
const render = (q: Preparation['questions'][number]) => (
|
||||
<label className="grid gap-1" key={q.key} htmlFor={'prep-' + q.key}>
|
||||
{q.label}
|
||||
{q.options ? (
|
||||
<select
|
||||
id={'prep-' + q.key}
|
||||
className={input}
|
||||
disabled={disabled}
|
||||
value={values[q.key] ?? ''}
|
||||
onChange={(e) => onChange(q.key, e.target.value)}
|
||||
>
|
||||
<option value="">Por indicar</option>
|
||||
{Object.entries(q.options).map(([value, text]) => (
|
||||
<option key={value} value={value}>
|
||||
{text}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
) : (
|
||||
<input
|
||||
id={'prep-' + q.key}
|
||||
className={input}
|
||||
disabled={disabled}
|
||||
type="text"
|
||||
inputMode="decimal"
|
||||
value={values[q.key] ?? ''}
|
||||
onChange={(e) => onChange(q.key, e.target.value)}
|
||||
/>
|
||||
)}
|
||||
<span className="text-sm">
|
||||
{q.help} Fundamento: {q.source}.
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
const questions = data.questions.filter(
|
||||
(q) =>
|
||||
q.key !== 'investment_preparation' &&
|
||||
applicable(q) &&
|
||||
(!excluded || q.key === 'investment_activity_scope'),
|
||||
);
|
||||
const needed = questions.filter(
|
||||
(q) =>
|
||||
!known(q) &&
|
||||
(!data.enabled ||
|
||||
data.mode !== 'authorization' ||
|
||||
data.missing_fields.includes(q.key)),
|
||||
);
|
||||
return (
|
||||
<section
|
||||
id="preparation-questions"
|
||||
className={card + ' space-y-4'}
|
||||
aria-label="Dados do pedido de investimento privado"
|
||||
>
|
||||
<h2 className="text-xl font-bold">
|
||||
Dados em falta para preparar o pedido
|
||||
</h2>
|
||||
<p>
|
||||
Valores conhecidos são reutilizados. As alterações só são
|
||||
gravadas depois da comparação de impacto. Nacionalidade e
|
||||
residência não determinam a origem do capital nem a modalidade
|
||||
da empresa.
|
||||
</p>
|
||||
{render(data.questions[0])}
|
||||
{active && (
|
||||
<>
|
||||
{needed.map(render)}
|
||||
<details>
|
||||
<summary>
|
||||
Dados já declarados — consultar ou alterar
|
||||
</summary>
|
||||
<div className="space-y-3">
|
||||
{questions.filter(known).map(render)}
|
||||
</div>
|
||||
</details>
|
||||
<p>
|
||||
Data, moeda e situação da entidade usam as declarações
|
||||
acima.{' '}
|
||||
{data.missing_fields.filter((k) =>
|
||||
[
|
||||
'currency',
|
||||
'expected_start_date',
|
||||
'entity_stage',
|
||||
].includes(k),
|
||||
).length > 0 &&
|
||||
'Complete esses campos em falta antes de comparar.'}
|
||||
</p>
|
||||
<div id="preparation-documents" className="space-y-3">
|
||||
<h3 className="font-bold">
|
||||
Associar documentos já existentes no dossier
|
||||
</h3>
|
||||
<p>
|
||||
Carregue documentos pelo Balcão Virtual do mesmo
|
||||
dossier. A associação não aprova o conteúdo nem
|
||||
substitui a análise do pedido.
|
||||
</p>
|
||||
{data.requirements
|
||||
.filter((r) => r.required)
|
||||
.map((r) => (
|
||||
<label className="grid gap-1" key={r.key}>
|
||||
{r.label}
|
||||
<select
|
||||
className={input}
|
||||
disabled={disabled}
|
||||
value={
|
||||
Object.hasOwn(links, r.key)
|
||||
? (links[r.key] ?? '')
|
||||
: (r.document_id ?? '')
|
||||
}
|
||||
onChange={(e) =>
|
||||
onLink(
|
||||
r.key,
|
||||
e.target.value || null,
|
||||
)
|
||||
}
|
||||
>
|
||||
<option value="">Por associar</option>
|
||||
{documents.map((doc) => (
|
||||
<option
|
||||
key={doc.document_id}
|
||||
value={doc.document_id}
|
||||
>
|
||||
{doc.file_name} ·{' '}
|
||||
{{
|
||||
quarantined:
|
||||
'Em quarentena',
|
||||
active: 'Disponível',
|
||||
deleted: 'Removido',
|
||||
}[doc.status] ??
|
||||
'Estado por conferir'}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span>
|
||||
{r.reason}{' '}
|
||||
<a
|
||||
className="underline"
|
||||
href={r.source.url}
|
||||
>
|
||||
{r.source.reference}
|
||||
</a>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<p>
|
||||
Compare o impacto para actualizar o enquadramento, as perguntas
|
||||
e as exigências. Uma exigência que deixe de ser aplicável
|
||||
conserva o documento anterior no histórico.
|
||||
</p>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export function PreparationImpact({
|
||||
before,
|
||||
after,
|
||||
}: {
|
||||
before: Preparation;
|
||||
after: Preparation;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className="space-y-2 rounded border border-amber-400 p-3"
|
||||
aria-label="Impacto no pedido de investimento"
|
||||
>
|
||||
<h3 className="font-bold">Impacto no pedido e nos documentos</h3>
|
||||
<p>
|
||||
{preparationMode(before.mode)} → {preparationMode(after.mode)}.
|
||||
</p>
|
||||
<p>Próximo passo: {after.next}.</p>
|
||||
<ul className="list-disc pl-5">
|
||||
{after.requirements
|
||||
.filter(
|
||||
(r) =>
|
||||
before.requirements.find((b) => b.key === r.key)
|
||||
?.state !== r.state,
|
||||
)
|
||||
.map((r) => (
|
||||
<li key={r.key}>
|
||||
{r.label}:{' '}
|
||||
{
|
||||
states[
|
||||
before.requirements.find(
|
||||
(b) => b.key === r.key,
|
||||
)?.state ?? 'indeterminate'
|
||||
]
|
||||
}{' '}
|
||||
→ {states[r.state]}.{' '}
|
||||
{r.document_id && 'Vínculo documental preservado.'}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p>{after.calculation_notice}</p>
|
||||
<PreparationSummary data={after} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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,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
|
||||
|
||||
@@ -776,6 +776,37 @@ function multiplyQuantity(
|
||||
return (quantityMilli * unitPriceCents + 500n) / 1000n;
|
||||
}
|
||||
|
||||
/** Reuses the sales model, without requiring unrelated cost/cash assumptions. Blank is never zero. */
|
||||
export function monthlyProjectedSales(input: BusinessFinancialPlanInput): Array<string | null> {
|
||||
const periods = input.periods ?? 12;
|
||||
if (!Number.isInteger(periods) || periods < 1 || periods > 60) throw new Error('Horizonte de vendas inválido.');
|
||||
return Array.from({ length: periods }, (_, month) => {
|
||||
if (!input.products.length) return null;
|
||||
let total = 0n;
|
||||
for (const product of input.products) {
|
||||
const units = product.monthlyUnitsByPeriod === undefined ? product.monthlyUnits : product.monthlyUnitsByPeriod[month];
|
||||
const price = product.unitPriceByPeriod === undefined ? product.unitPrice : product.unitPriceByPeriod[month];
|
||||
if (typeof units !== 'string' || typeof price !== 'string' || units.trim() === '' || price.trim() === '') return null;
|
||||
total += multiplyQuantity(quantity(units), cents(price, `Preço de ${product.label}`));
|
||||
}
|
||||
return format(total);
|
||||
});
|
||||
}
|
||||
|
||||
/** Existing purchase orders are planning references, never a proxy for deductible VAT or invoice dates. */
|
||||
export function monthlyProjectedPurchaseOrders(input: BusinessFinancialPlanInput): Array<string | null> {
|
||||
const periods = input.periods ?? 12;
|
||||
if (!Number.isInteger(periods) || periods < 1 || periods > 60) throw new Error('Horizonte de compras inválido.');
|
||||
return Array.from({length: periods}, (_, month) => {
|
||||
const amounts = input.useProductInventorySchedule
|
||||
? input.products.map((product) => product.productInventoryPurchasesByPeriod?.[month]?.amount)
|
||||
: [input.useInventoryPurchaseSchedule
|
||||
? input.inventoryPurchaseSchedule?.[month]?.amount : input.monthlyInventoryPurchases];
|
||||
if (!amounts.length || amounts.some((amount) => typeof amount !== 'string' || amount.trim() === '')) return null;
|
||||
return format(amounts.reduce((total, amount) => total + cents(amount!, 'Compras projectadas'), 0n));
|
||||
});
|
||||
}
|
||||
|
||||
function format(centsValue: bigint): string {
|
||||
const absolute = centsValue < 0n ? -centsValue : centsValue;
|
||||
return `${centsValue < 0n ? '-' : ''}${absolute / 100n}.${String(absolute % 100n).padStart(2, '0')}`;
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { restoreBusinessPlanSnapshot } from './business-financial-plan.ts';
|
||||
const empty = (value: unknown) => value == null || value === '' || (Array.isArray(value) && value.length === 0);
|
||||
function canonical(value: unknown): unknown {
|
||||
if (Array.isArray(value)) return value.map(canonical);
|
||||
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).sort(([a],[b])=>a.localeCompare(b)).map(([key,item])=>[key,canonical(item)]));
|
||||
return value;
|
||||
}
|
||||
// Change tracking only, never an authorization, integrity or legal-acceptance token. No personal values in metadata.
|
||||
function signature(value: unknown): string {
|
||||
const text=JSON.stringify(canonical(value)) ?? 'undefined';let hash=14695981039346656037n;
|
||||
for(let index=0;index<text.length;index++) hash=BigInt.asUintN(64,(hash^BigInt(text.charCodeAt(index)))*1099511628211n);
|
||||
return `${text.length}:${hash.toString(16)}`;
|
||||
}
|
||||
export function inheritCompatibleFields(scope:string, values:Record<string,unknown>, source:Record<string,unknown>, raw:unknown, allowed?:string[]): {patch:Record<string,unknown>; metadata:string; inherited:string[]; conflicts:string[]} {
|
||||
let previous:Record<string,string>={};
|
||||
try { const saved=JSON.parse(String(raw ?? ''));if(saved.version===1 && saved.scope===scope && saved.inherited && typeof saved.inherited==='object') previous=saved.inherited; } catch { /* Legacy inputs are retained, not overwritten. */ }
|
||||
const next={...previous};const patch:Record<string,unknown>={};const inherited:string[]=[];const conflicts:string[]=[];
|
||||
const geo=['province_id','district_id','admin_post_id','locality_id'];
|
||||
const geoConflict=geo.some(key=>(!empty(values[key]) || key in previous) && signature(values[key])!==signature(source[key]) && previous[key]!==signature(values[key]));
|
||||
for(const [key,value] of Object.entries(source)) {
|
||||
if ((allowed && !allowed.includes(key)) || (empty(value) && !(key in previous))) continue;
|
||||
const current=values[key];
|
||||
const track = (item:unknown) => key==='parcels' ? Object.fromEntries(geo.map(field=>[field,(item as Array<Record<string,unknown>>)?.[0]?.[field] ?? ''])) : item;
|
||||
const owned=previous[key]===signature(track(current));
|
||||
const vacant=key==='parcels' ? geo.every(field=>empty((current as Array<Record<string,unknown>>)?.[0]?.[field])) : empty(current);
|
||||
if ((geo.includes(key) && geoConflict) || (!owned && (!vacant || key in previous))) {
|
||||
if(signature(current)!==signature(value)) conflicts.push(key);
|
||||
continue;
|
||||
}
|
||||
if(key==='parcels') {
|
||||
const parcels=current as Array<Record<string,unknown>>|undefined;
|
||||
if(!owned && parcels?.length && geo.some(field=>!empty(parcels[0]?.[field]))) {conflicts.push(key);continue;}
|
||||
}
|
||||
if(signature(track(current))!==signature(track(value))) {
|
||||
if(key==='parcels') {
|
||||
const parcels=(current as Array<Record<string,unknown>>) ?? [];
|
||||
patch[key]=[{...parcels[0],...Object.fromEntries(geo.map(field=>[field,(value as Array<Record<string,unknown>>)[0]?.[field] ?? '']))},...parcels.slice(1)];
|
||||
} else patch[key]=value;
|
||||
}
|
||||
next[key]=signature(track(value));inherited.push(key);
|
||||
}
|
||||
return {patch,metadata:JSON.stringify({version:1,scope,inherited:next}),inherited,conflicts};
|
||||
}
|
||||
|
||||
export function inheritSimulatorFields<T extends Record<string,unknown>>(draft:T): T {
|
||||
const source:Record<string,unknown>={};
|
||||
const plan=draft.business_plan_snapshot ? restoreBusinessPlanSnapshot(String(draft.business_plan_snapshot)) : null;
|
||||
if(plan?.currency) source.currency=plan.currency;
|
||||
const currency=draft.currency || source.currency;
|
||||
if(currency) {source.fiscal_currency=currency;source.opportunity_currency=currency;source.published_iva_currency=currency;}
|
||||
if(draft.tax_residency_country) source.fiscal_residency_country=draft.tax_residency_country;
|
||||
if(draft.fiscal_reference_date) source.published_iva_date=draft.fiscal_reference_date;
|
||||
let settings:Record<string,unknown>={start:'',basis:'unknown',declared:false,auto:false};
|
||||
let settingsValid=!draft.monthly_iva_settings;
|
||||
try {const saved=JSON.parse(String(draft.monthly_iva_settings ?? ''));if(saved && !Array.isArray(saved) && typeof saved.start==='string' && ['net','included','unknown'].includes(saved.basis) && typeof saved.declared==='boolean' && typeof saved.auto==='boolean') {settings=saved;settingsValid=true;}} catch { /* Invalid stored inputs remain intact. */ }
|
||||
const start=String(draft.expected_start_date ?? '');
|
||||
if(/^\d{4}-\d{2}-\d{2}$/.test(start)) source.monthly_iva_start=start.slice(0,7);
|
||||
else if(/^\d{4}$/.test(String(draft.fiscal_period)) && draft.fiscal_periodicity==='monthly' && /^(0?[1-9]|1[0-2])$/.test(String(draft.fiscal_month))) source.monthly_iva_start=`${draft.fiscal_period}-${String(draft.fiscal_month).padStart(2,'0')}`;
|
||||
if(!settingsValid) delete source.monthly_iva_start;
|
||||
// Legacy drafts without a compatible source need no inheritance marker or autosave churn.
|
||||
if (!Object.keys(source).length && !draft.compatible_fields_snapshot) return draft;
|
||||
const values={...draft,monthly_iva_start:settings.start ?? ''};
|
||||
const patch=inheritCompatibleFields('simulator-compatible-fields',values,source,draft.compatible_fields_snapshot);
|
||||
const next:Record<string,unknown>={...draft,...patch.patch,compatible_fields_snapshot:patch.metadata};
|
||||
if('monthly_iva_start' in patch.patch) next.monthly_iva_settings=JSON.stringify({...settings,start:patch.patch.monthly_iva_start});
|
||||
delete next.monthly_iva_start;
|
||||
return JSON.stringify(next)===JSON.stringify(draft) ? draft : next as T;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import {
|
||||
validateDossierAttachments,
|
||||
type DossierAttachment,
|
||||
} from './project-dossier-report.ts';
|
||||
|
||||
export type DossierExportOperation =
|
||||
| 'word'
|
||||
| 'package'
|
||||
| 'pdf'
|
||||
| 'csv'
|
||||
| 'attachments';
|
||||
const messages: Record<DossierExportOperation, string> = {
|
||||
word: 'Não foi possível gerar o relatório Word.',
|
||||
package: 'Não foi possível preparar o pacote de anexos.',
|
||||
pdf: 'Não foi possível preparar a versão para PDF.',
|
||||
csv: 'Não foi possível preparar o ficheiro CSV.',
|
||||
attachments: 'Não foi possível ler os anexos seleccionados.',
|
||||
};
|
||||
let failureSequence = 0;
|
||||
|
||||
export function dossierExportFailure(
|
||||
operation: DossierExportOperation,
|
||||
cause: unknown,
|
||||
log: (message: string, detail: unknown) => void = console.error,
|
||||
): string {
|
||||
const reference = `EXP-${Date.now().toString(36)}-${++failureSequence}`;
|
||||
// Local diagnostics only: never send dossier values or file contents.
|
||||
log('[InvesteMoz exportação]', { reference, operation, cause });
|
||||
return `${messages[operation]} Os dados, as secções e os anexos foram mantidos. Tente novamente pelo mesmo botão. Referência: ${reference}.`;
|
||||
}
|
||||
|
||||
/** Synchronous acquisition prevents a second click before React renders busy. */
|
||||
export class DossierExportGate {
|
||||
private active = false;
|
||||
async run(task: () => Promise<void>): Promise<boolean> {
|
||||
if (this.active) return false;
|
||||
this.active = true;
|
||||
try {
|
||||
await task();
|
||||
return true;
|
||||
} finally {
|
||||
this.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function prepareDossierAttachments(
|
||||
current: DossierAttachment[],
|
||||
incoming: File[],
|
||||
): Promise<{
|
||||
attachments: DossierAttachment[];
|
||||
duplicateCount: number;
|
||||
problem?: string;
|
||||
}> {
|
||||
for (const file of incoming) {
|
||||
const problem = validateDossierAttachments([file]);
|
||||
if (problem)
|
||||
return { attachments: current, duplicateCount: 0, problem };
|
||||
}
|
||||
const identity = async (file: File) => {
|
||||
const hash = await crypto.subtle.digest(
|
||||
'SHA-256',
|
||||
await file.arrayBuffer(),
|
||||
);
|
||||
return `${file.name}:${Array.from(new Uint8Array(hash), (b) => b.toString(16).padStart(2, '0')).join('')}`;
|
||||
};
|
||||
const known = new Set(
|
||||
await Promise.all(current.map((item) => identity(item.file))),
|
||||
);
|
||||
const added: DossierAttachment[] = [];
|
||||
let duplicateCount = 0;
|
||||
for (const file of incoming) {
|
||||
const key = await identity(file);
|
||||
if (known.has(key)) {
|
||||
duplicateCount++;
|
||||
continue;
|
||||
}
|
||||
known.add(key);
|
||||
added.push({ file, include: true, caption: '' });
|
||||
}
|
||||
const problem = validateDossierAttachments(
|
||||
[...current, ...added].map((item) => item.file),
|
||||
);
|
||||
return {
|
||||
attachments: problem ? current : [...current, ...added],
|
||||
duplicateCount,
|
||||
problem: problem ?? undefined,
|
||||
};
|
||||
}
|
||||
@@ -468,9 +468,16 @@ export function exportFinancialToolsCsv(
|
||||
['Pressupostos do empréstimo'],
|
||||
['Capital', input.loan.principal],
|
||||
['Taxa anual (%)', input.loan.annualRatePercent],
|
||||
['Tipo de taxa', input.loan.rateType],
|
||||
[
|
||||
'Tipo de taxa',
|
||||
input.loan.rateType === 'nominal' ? 'Nominal' : 'Efectiva',
|
||||
],
|
||||
['Prazo (meses)', input.loan.termMonths],
|
||||
['Periodicidade (meses)', input.loan.periodMonths],
|
||||
[
|
||||
'Arredondamento',
|
||||
'Montantes a duas casas decimais; juros e prestação arredondados à unidade monetária mínima, metade para cima; última prestação ajusta o saldo residual.',
|
||||
],
|
||||
['Carência (períodos)', input.loan.gracePeriods ?? 0],
|
||||
['Comissão inicial', input.loan.upfrontFee ?? '0'],
|
||||
['Comissão periódica', input.loan.recurringFee ?? '0'],
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { isApiSimulationResultSnapshotV1 } from "./simulation-contract.ts";
|
||||
import { modeloAComponentRows } from './iva-modelo-a.ts';
|
||||
|
||||
export function fiscalQaRows(value: unknown): Array<[string, string]> {
|
||||
if (!isApiSimulationResultSnapshotV1(value)) return [];
|
||||
const snapshot = value as unknown as Record<string, unknown>;
|
||||
if (!["fiscal_rule_qa_preview", "fiscal_rule_published"].includes(String(snapshot.scenario_kind))) return [];
|
||||
const published = snapshot.scenario_kind === "fiscal_rule_published";
|
||||
const provenance = snapshot.fiscal_provenance as
|
||||
Record<string, unknown> | undefined;
|
||||
const rule = provenance?.rule as Record<string, unknown> | undefined;
|
||||
const source = rule?.source as Record<string, unknown> | undefined;
|
||||
const parameters = rule?.parameters as
|
||||
Record<string, Record<string, unknown>> | undefined;
|
||||
const intermediate = snapshot.intermediate as Record<
|
||||
string,
|
||||
unknown
|
||||
> | null;
|
||||
const text = (item: unknown) =>
|
||||
typeof item === "string" || typeof item === "number"
|
||||
? String(item)
|
||||
: "não disponível";
|
||||
const monthly = snapshot.monthly_projection as {rows?: Array<Record<string,unknown>>; cash_treatment?: string} | undefined;
|
||||
return [
|
||||
...modeloAComponentRows(snapshot),
|
||||
...(monthly?.rows ? [
|
||||
["Projecção mensal", "Vendas tributáveis sem IVA → taxa aplicada → IVA calculado → total com IVA; não é apuramento final"],
|
||||
...monthly.rows.map((row) => [`Mês ${text(row.date)}`, `Vendas declaradas: ${text(row.sales_declared)}; base líquida: ${text(row.net_sales)}; taxa: ${row.rate == null ? 'não aplicada' : `${text(row.rate)}%`}; IVA: ${text(row.vat)}; total com IVA: ${text(row.gross_sales)}; ${row.status === 'calculated' ? 'calculado' : row.status === 'missing_sales' ? 'sem informação de vendas' : row.status === 'unsupported_basis' ? 'base líquida não determinada' : 'enquadramento ou regra indeterminados'}`]),
|
||||
["Tratamento na tesouraria", monthly.cash_treatment ?? "IVA separado; não é custo nem imposto a pagar"],
|
||||
] as Array<[string,string]> : []),
|
||||
["Relatório", published ? "IVA pela versão publicada; ver enquadramento abaixo" : "Pré-visualização fiscal QA de rascunho — sem activação"],
|
||||
[
|
||||
"IVA calculado (exacto; não é saldo a pagar)",
|
||||
value.result.value ?? "Indeterminado",
|
||||
],
|
||||
["Explicação", value.result.explanation],
|
||||
["Base tributável declarada", text(snapshot.base)],
|
||||
["Taxa cadastrada (%)", text(parameters?.IVA_GENERAL_RATE?.value)],
|
||||
["Moeda explícita", text(value.context.currency)],
|
||||
[monthly ? "Início da projecção mensal" : "Data de exigibilidade declarada", value.context.reference_date],
|
||||
[
|
||||
"Enquadramento",
|
||||
provenance?.scope_status === "undetermined" ? "Indeterminado: falta declaração do enquadramento" : "Declarado pelo utilizador; não validado juridicamente",
|
||||
],
|
||||
[
|
||||
"Regra / versão / estado",
|
||||
`${text(rule?.rule_code)} / ${text(rule?.rule_version)} / ${published ? "publicada no momento da admissão" : "rascunho"}`,
|
||||
],
|
||||
[
|
||||
"Vigência cadastrada",
|
||||
`${text(rule?.valid_from)} até ${rule?.valid_until === null ? "sem termo indicado" : text(rule?.valid_until)}`,
|
||||
],
|
||||
[
|
||||
"Fonte da taxa",
|
||||
`${text(source?.diploma)} — ${text(source?.article)}`,
|
||||
],
|
||||
["Hash do exemplar da taxa", text(rule?.source_sha256)],
|
||||
["Representação racional exacta", text(intermediate?.result)],
|
||||
[
|
||||
"Arredondamento",
|
||||
"Não aplicado. Valor calculado preservado sem truncar ou arredondar; apresentação monetária separada.",
|
||||
],
|
||||
[
|
||||
"Limitações",
|
||||
published ? "Cobertura de 2026; operação interna geral em MZN; sem excepções, deduções, declaração ou saldo a pagar." : "Regra não publicada; cobertura demonstrada em 2026; sem deduções, declaração ou saldo a pagar.",
|
||||
],
|
||||
];
|
||||
}
|
||||
@@ -1,3 +1,7 @@
|
||||
import type {
|
||||
Preparation,
|
||||
PreparationDocument,
|
||||
} from '@/components/private-investment-preparation';
|
||||
function csrfToken(): string {
|
||||
return decodeURIComponent(
|
||||
document.cookie
|
||||
@@ -56,6 +60,8 @@ export type DossierContext = {
|
||||
dependency_hash?: string;
|
||||
};
|
||||
export type ContextView = {
|
||||
private_investment?: Preparation | null;
|
||||
preparation_documents?: PreparationDocument[];
|
||||
simulator_context_revision?: number | null;
|
||||
case: { id: string; title: string; version: number };
|
||||
context: DossierContext;
|
||||
@@ -73,6 +79,7 @@ export type ContextView = {
|
||||
notice: string;
|
||||
};
|
||||
export type ContextPlan = {
|
||||
private_investment?: { before: Preparation; after: Preparation } | null;
|
||||
preview_hash: string;
|
||||
version: number;
|
||||
revision: number;
|
||||
@@ -119,6 +126,20 @@ export async function contextApi<T>(
|
||||
return data as T;
|
||||
}
|
||||
export const contextFieldNames: Record<string, string> = {
|
||||
investment_form: 'Tipo do investimento',
|
||||
investment_preparation: 'Preparação do pedido',
|
||||
investment_activity_scope: 'Natureza da actividade',
|
||||
implementation_mode: 'Modalidade da empresa',
|
||||
capital_origin: 'Origem dos capitais',
|
||||
site_tenure: 'Utilização das instalações',
|
||||
project_concession: 'PPP ou concessão empresarial',
|
||||
significant_implications: 'Implicações significativas',
|
||||
land_extension: 'Extensão de terra',
|
||||
forest_concession: 'Concessão florestal',
|
||||
forest_concession_hectares: 'Área florestal em hectares',
|
||||
total_investment: 'Valor total do investimento',
|
||||
area_required: 'Área de terra requerida',
|
||||
area_unit: 'Unidade da área',
|
||||
title: 'Título do investimento',
|
||||
territory_id: 'Localização documentada',
|
||||
location_source: 'Origem da localização',
|
||||
@@ -134,4 +155,19 @@ export const contextFieldNames: Record<string, string> = {
|
||||
province: 'Província',
|
||||
district: 'Distrito',
|
||||
location: 'Localização',
|
||||
currency: 'Moeda do projecto',
|
||||
person_type: 'Tipo de pessoa',
|
||||
fiscal_period: 'Ano fiscal',
|
||||
fiscal_periodicity: 'Periodicidade',
|
||||
fiscal_month: 'Mês fiscal',
|
||||
fiscal_residency_country: 'País de residência fiscal declarado',
|
||||
activity_id: 'Actividade',
|
||||
project_type: 'Modalidade do investimento',
|
||||
maturity_stage: 'Fase do investimento',
|
||||
expected_start_date: 'Início previsto',
|
||||
investor_origin: 'Origem do investidor',
|
||||
entity_stage: 'Situação da entidade',
|
||||
tax_residency_country: 'Residência fiscal declarada',
|
||||
investment_regime: 'Enquadramento territorial consultado',
|
||||
special_zone_id: 'ZEE, ZFI ou parque',
|
||||
};
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
export type IvaIssue = {
|
||||
category: 'missing_field' | 'invalid_value' | 'out_of_scope' | 'period_unavailable' | 'system_configuration';
|
||||
field: string | null;
|
||||
label: string;
|
||||
received: unknown;
|
||||
expected: string;
|
||||
message: string;
|
||||
};
|
||||
export type IvaDiagnostic = {
|
||||
label: string;
|
||||
rule_version: number;
|
||||
validity: { start: string; end: string | null };
|
||||
source: { title?: string; diploma?: string; article?: string; url?: string };
|
||||
issues: IvaIssue[];
|
||||
};
|
||||
export const ivaIssueCategories = {
|
||||
missing_field: 'Campo em falta', invalid_value: 'Valor inválido', out_of_scope: 'Fora do âmbito',
|
||||
period_unavailable: 'Regra indisponível para o período', system_configuration: 'Configuração do sistema',
|
||||
};
|
||||
export function ivaReceived(value: unknown): string {
|
||||
const labels: Record<string, string> = { domestic: 'Operação interna', internal_general_net: 'Operação interna à taxa geral, sobre base líquida', import: 'Importação', export: 'Exportação', refund: 'Reembolso', unknown: 'Não sei', non_resident: 'Operação com não residente', other: 'Outra operação' };
|
||||
return typeof value === 'string' && value !== '' ? (labels[value] ?? value) : 'Não recebido';
|
||||
}
|
||||
export function ivaIssueText(issue: IvaIssue): string {
|
||||
return `${issue.label}: ${ivaIssueCategories[issue.category]}. Recebido: ${ivaReceived(issue.received)}. Esperado: ${issue.expected}. ${issue.message}`;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { monthlyProjectedPurchaseOrders, restoreBusinessPlanSnapshot } from './business-financial-plan.ts';
|
||||
|
||||
/** Correspondence to the Tempo exemplar, not an electronic AT declaration or a new tax ruleset. */
|
||||
export const modeloAReference = {
|
||||
file: 'Novo Modelo A.docx',
|
||||
sha256: '62c198e90d8c6792f7d5ccefc6386df2e4b6089573067d3a6e1eae7fc2b9572b',
|
||||
edition: 'Edição/vigência do formulário não estabelecida; original preservado na biblioteca QA.',
|
||||
status: 'Simulação parcial: campos 01/02. Não é declaração submetida ou aceite pela Autoridade Tributária.',
|
||||
};
|
||||
|
||||
export type ModeloAField = { field: string; purpose: string; origin: string; calculation: string; basis: string; status: string };
|
||||
const pending = 'Em falta; não assumir zero';
|
||||
export const modeloAFields: ModeloAField[] = [
|
||||
{field:'01',purpose:'Base das operações à taxa geral',origin:'Vendas mensais já projectadas, declaradas sem IVA e no âmbito geral',calculation:'Base líquida do mês',basis:'CIVA arts.15 e 17; Lei22/2022; regra publicada existente',status:'Implementado no âmbito declarado'},
|
||||
{field:'02',purpose:'IVA à taxa geral',origin:'Resultado mensal congelado no servidor',calculation:'Campo01 × taxa da versão publicada /100',basis:'CIVA art.17(1); mesma regra/versionamento do consumidor',status:'Implementado; componente do apuramento'},
|
||||
{field:'03',purpose:'Base de operações específicas à taxa reduzida',origin:'Operações separadas e enquadradas; ainda não recolhidas',calculation:'Base das operações elegíveis',basis:'Exemplar e CIVA art.17-A, aditado pela Lei22/2022; enquadramento a cadastrar',status:pending},
|
||||
{field:'04',purpose:'IVA à taxa reduzida',origin:'Campo03 e futura versão fundamentada',calculation:'Campo03 × taxa reduzida aplicável; não activa pelo exemplar',basis:'CIVA art.17-A; âmbito/versão temporal a integrar',status:pending},
|
||||
{field:'05',purpose:'Vendas isentas com direito à dedução',origin:'Vendas classificadas por operação e fundamento',calculation:'Soma das bases classificadas; sem taxa presumida',basis:'Exemplar; CIVA arts.9–14 e 18–20 a consolidar por operação',status:pending},
|
||||
{field:'06',purpose:'Operações da alínea b), n.º1, art.18, conforme exemplar',origin:'Operações classificadas com referência verificada',calculation:'Soma separada; não inferir do nome do sector',basis:'Remissão do exemplar ao art.18(1)(b); cotejo do âmbito necessário',status:pending},
|
||||
{field:'07',purpose:'Isentas sem direito à dedução',origin:'Vendas classificadas e uso das aquisições',calculation:'Soma das bases elegíveis',basis:'Exemplar; incidência/isenções e exclusões de dedução a fundamentar',status:pending},
|
||||
{field:'08',purpose:'IVA dedutível — imobilizado',origin:'Aquisições/facturas de activos, não o valor global do investimento',calculation:'Apenas imposto elegível documentado',basis:'CIVA arts.18–21; art.18(2), Lei10/2025: documentos legais e fornecedor inscrito',status:pending},
|
||||
{field:'09',purpose:'IVA dedutível — existências',origin:'Compras projectadas reutilizáveis; falta IVA, documento e elegibilidade',calculation:'IVA elegível das existências, não compras × taxa geral',basis:'CIVA arts.18–21; condições/exclusões, afectação e período',status:pending},
|
||||
{field:'10',purpose:'IVA dedutível — outros bens/serviços',origin:'Aquisições discriminadas; despesas agregadas não provam IVA',calculation:'IVA elegível; excluir operações sem direito',basis:'CIVA arts.18–21; art.19(2), Lei10/2025, inclui exclusão das operações art.17-A',status:pending},
|
||||
{field:'11',purpose:'IVA dedutível — importações',origin:'Documento aduaneiro legal e afectação; ainda não recolhidos',calculation:'Imposto aduaneiro elegível, não taxa sobre orçamento',basis:'CIVA art.18(2), Lei10/2025; demais condições/exclusões a cadastrar',status:pending},
|
||||
{field:'12',purpose:'Regularizações a favor do sujeito passivo',origin:'Ajustamentos individualizados, documento e motivo',calculation:'Soma de regularizações elegíveis',basis:'Exemplar tem remissão interna 11/12 divergente das caixas12/13; fundamento por motivo por conferir',status:pending},
|
||||
{field:'13',purpose:'Regularizações a favor do Estado',origin:'Ajustamentos individualizados, documento e motivo',calculation:'Soma separada das regularizações elegíveis',basis:'Mesma divergência; não corrigir o original nem inventar incidência',status:pending},
|
||||
{field:'14',purpose:'Soma das bases do quadro de apuramento',origin:'Campos01,03,05,06,07 completos',calculation:'01+03+05+06+07',basis:'Fórmula impressa no exemplar; não é apenas vendas tributáveis',status:'Estrutura mapeada; total indeterminado'},
|
||||
{field:'15',purpose:'Total a favor do sujeito passivo',origin:'Campos08–12 completos',calculation:'08+09+10+11+12',basis:'Fórmula do exemplar; depende de deduções/regularizações fundamentadas',status:'Estrutura mapeada; total indeterminado'},
|
||||
{field:'16',purpose:'Total a favor do Estado',origin:'Campos02,04,13 completos',calculation:'02+04+13',basis:'Fórmula impressa no exemplar; 02 sozinho não equivale a16',status:'Estrutura mapeada; total indeterminado'},
|
||||
{field:'17',purpose:'Diferença devedora antes de créditos anteriores',origin:'Totais15/16 completos',calculation:'16−15 quando16>15',basis:'Instrução do exemplar; não é automaticamente pagamento',status:'Indeterminado; sem consolidação completa'},
|
||||
{field:'18',purpose:'Diferença credora antes de créditos anteriores',origin:'Totais15/16 completos',calculation:'15−16 quando15>16',basis:'Instrução do exemplar; não constitui reembolso reconhecido',status:'Indeterminado; sem consolidação completa'},
|
||||
{field:'19',purpose:'Excesso a reportar do período anterior',origin:'Histórico fiscal anterior confirmado, separado de estimativas',calculation:'Crédito anterior utilizável; sem copiar crédito estimado',basis:'Quadro de utilização de créditos do exemplar; condições/prazo por conferir',status:pending},
|
||||
{field:'20',purpose:'Créditos comunicados pelos serviços',origin:'Comunicação efectiva da Administração Fiscal',calculation:'Crédito efectivamente comunicado e utilizável',basis:'Exemplar; não converter pedido ou crédito estimado em comunicação',status:pending},
|
||||
{field:'21',purpose:'Diferencial de declaração de substituição',origin:'Valores17/18 da declaração actual e da substituída',calculation:'Casos impressos no quadro9, com sinal devedor/credor',basis:'Exemplar; ligação às declarações efectivas e regras de substituição em falta',status:pending},
|
||||
{field:'22',purpose:'IVA a entregar no contexto da declaração',origin:'Apuramento e créditos/substituição completos',calculation:'Não derivar apenas do campo02',basis:'Quadro10 do exemplar; procedimento aplicável por fundamentar',status:pending},
|
||||
{field:'23',purpose:'Juros compensatórios',origin:'Factos e períodos que legalmente os determinem',calculation:'Sem taxa, mora ou obrigação presumidas',basis:'Quadro10; fundamento específico não analisado neste lote',status:pending},
|
||||
{field:'24',purpose:'Importância a pagar',origin:'Campos22/23 completos',calculation:'22+23',basis:'Fórmula do exemplar; não é ordem nem execução de pagamento',status:pending},
|
||||
{field:'25',purpose:'Crédito de imposto do quadro11',origin:'Apuramento completo e tratamento dos créditos anteriores',calculation:'Montante recuperável no contexto próprio, por fundamentar',basis:'Caixa25 confirmada visualmente no bloco de recuperação; condições por consolidar',status:pending},
|
||||
{field:'26',purpose:'Reporte para período seguinte',origin:'Crédito utilizável e opção no quadro11',calculation:'Separado de pedido e recebimento',basis:'Quadro11; cotejo de condições/prazos e articulação19/20 necessário',status:pending},
|
||||
{field:'27',purpose:'Pedido de reembolso',origin:'Crédito elegível e pedido, sem presumir reconhecimento/caixa',calculation:'Pedido distinto de aprovação e recebimento',basis:'Exemplar cita art.20; Lei10/2025 altera reembolsos no art.21. Divergência registada.',status:pending},
|
||||
];
|
||||
|
||||
export function modeloAComponentRows(snapshot: Record<string, unknown>): Array<[string,string]> {
|
||||
if (!snapshot.monthly_projection) return [];
|
||||
return [
|
||||
['Correspondência Modelo A',modeloAReference.status],
|
||||
['Campos01/02', 'Base líquida e IVA mensal da versão publicada. Os restantes campos não foram presumidos iguais a zero.'],
|
||||
['Apuramento do período — campos14–18', 'Indeterminado: faltam componentes e enquadramentos do Modelo A. IVA das vendas não é imposto final a pagar.'],
|
||||
['Origem dos dados','Projecção do negócio; não são operações efectivas nem declaração fiscal.'],
|
||||
['Exemplar de referência',`${modeloAReference.file}; SHA256 ${modeloAReference.sha256}. ${modeloAReference.edition}`],
|
||||
];
|
||||
}
|
||||
|
||||
export function modeloAPurchaseReferences(draft: Record<string, unknown>): Array<{month:number; declared:string|null; net:null; vat:null; gross:null; deductible:null}> {
|
||||
if (!draft.business_plan_snapshot) return [];
|
||||
const plan = restoreBusinessPlanSnapshot(String(draft.business_plan_snapshot));
|
||||
if (!plan) throw new Error('Não foi possível recuperar as aquisições projectadas. O original guardado foi preservado.');
|
||||
return monthlyProjectedPurchaseOrders(plan).map((amount, index) => ({month:index+1, declared:amount, net:null, vat:null, gross:null, deductible:null}));
|
||||
}
|
||||
@@ -1,5 +1,73 @@
|
||||
import { csvCell } from './csv.js';
|
||||
|
||||
export function defaultCashflowDraftInputs() {
|
||||
return {
|
||||
openingCash: '0',
|
||||
monthlyRevenue: '200000',
|
||||
monthlyOperatingCosts: '150000',
|
||||
monthlyDebtService: '0',
|
||||
monthlyTaxAssessed: '',
|
||||
monthlyTaxSchedule: Array.from({ length: 12 }, () => ''),
|
||||
vatCreditEstimated: '0',
|
||||
vatRefundRequested: '0',
|
||||
vatRefundRecognized: '0',
|
||||
vatRefundExpectedReceipt: '0',
|
||||
vatRefundExpectedMonth: '',
|
||||
};
|
||||
}
|
||||
|
||||
export type CashflowDraftSnapshot = {
|
||||
version: 1;
|
||||
taxMode: 'fixed' | 'schedule';
|
||||
inputs: ReturnType<typeof defaultCashflowDraftInputs>;
|
||||
};
|
||||
|
||||
/** Preserve editable strings, including unanswered and invalid amounts, without inventing zero. */
|
||||
export function restoreCashflowDraftSnapshot(
|
||||
snapshot: string | undefined,
|
||||
): CashflowDraftSnapshot | null {
|
||||
if (!snapshot || snapshot.length > 20000) return null;
|
||||
try {
|
||||
const parsed = JSON.parse(snapshot);
|
||||
if (
|
||||
!parsed ||
|
||||
parsed.version !== 1 ||
|
||||
!['fixed', 'schedule'].includes(parsed.taxMode) ||
|
||||
!parsed.inputs ||
|
||||
typeof parsed.inputs !== 'object' ||
|
||||
Array.isArray(parsed.inputs)
|
||||
)
|
||||
return null;
|
||||
const keys = Object.keys(defaultCashflowDraftInputs());
|
||||
if (Object.keys(parsed.inputs).some((key) => !keys.includes(key)))
|
||||
return null;
|
||||
for (const key of keys) {
|
||||
const value = parsed.inputs[key];
|
||||
if (key === 'monthlyTaxSchedule') {
|
||||
if (
|
||||
!Array.isArray(value) ||
|
||||
value.length !== 12 ||
|
||||
value.some(
|
||||
(item) => typeof item !== 'string' || item.length > 160,
|
||||
)
|
||||
)
|
||||
return null;
|
||||
} else if (typeof value !== 'string' || value.length > 160)
|
||||
return null;
|
||||
}
|
||||
return { version: 1, taxMode: parsed.taxMode, inputs: parsed.inputs };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function serializeCashflowDraftSnapshot(
|
||||
taxMode: CashflowDraftSnapshot['taxMode'],
|
||||
inputs: CashflowDraftSnapshot['inputs'],
|
||||
): string {
|
||||
return JSON.stringify({ version: 1, taxMode, inputs });
|
||||
}
|
||||
|
||||
export type MonthlyCashflowInput = {
|
||||
openingCash: string;
|
||||
monthlyRevenue: string;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user