diff --git a/.env.example b/.env.example index 60ee2f9d..0eb72f1a 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/app/Console/Commands/RegisterFiscalQaDraft.php b/app/Console/Commands/RegisterFiscalQaDraft.php new file mode 100644 index 00000000..34b0cc47 --- /dev/null +++ b/app/Console/Commands/RegisterFiscalQaDraft.php @@ -0,0 +1,50 @@ +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; + } +} diff --git a/app/Http/Controllers/Api/V1/SimulationController.php b/app/Http/Controllers/Api/V1/SimulationController.php index bf0d3e03..3f752588 100644 --- a/app/Http/Controllers/Api/V1/SimulationController.php +++ b/app/Http/Controllers/Api/V1/SimulationController.php @@ -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'])); + $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) { diff --git a/app/Http/Controllers/InvestmentLegalSourceController.php b/app/Http/Controllers/InvestmentLegalSourceController.php index 30e53e38..89f5a433 100644 --- a/app/Http/Controllers/InvestmentLegalSourceController.php +++ b/app/Http/Controllers/InvestmentLegalSourceController.php @@ -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') { diff --git a/app/Http/Controllers/SimulationTaxRuleGovernanceController.php b/app/Http/Controllers/SimulationTaxRuleGovernanceController.php index 888115d0..be22df54 100644 --- a/app/Http/Controllers/SimulationTaxRuleGovernanceController.php +++ b/app/Http/Controllers/SimulationTaxRuleGovernanceController.php @@ -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([ diff --git a/app/Http/Requests/StoreSimulationRequest.php b/app/Http/Requests/StoreSimulationRequest.php index ed8df982..736bd3e3 100644 --- a/app/Http/Requests/StoreSimulationRequest.php +++ b/app/Http/Requests/StoreSimulationRequest.php @@ -11,10 +11,47 @@ 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.', + ]; + } + + 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']; + } + /** @return array> */ public function rules(): array { + if ($this->input('scenario_kind') === 'fiscal_rule_published') { + 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'], diff --git a/app/Jobs/RunSimulation.php b/app/Jobs/RunSimulation.php index 4fea21e4..92fb5f1a 100644 --- a/app/Jobs/RunSimulation.php +++ b/app/Jobs/RunSimulation.php @@ -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); diff --git a/app/Services/SimulationExecution.php b/app/Services/SimulationExecution.php index de242a4a..0bea6edb 100644 --- a/app/Services/SimulationExecution.php +++ b/app/Services/SimulationExecution.php @@ -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); diff --git a/app/Services/SimulationFiscalQaPreview.php b/app/Services/SimulationFiscalQaPreview.php new file mode 100644 index 00000000..4dbb4e51 --- /dev/null +++ b/app/Services/SimulationFiscalQaPreview.php @@ -0,0 +1,50 @@ +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); + }); + } +} diff --git a/app/Services/SimulationFiscalSnapshot.php b/app/Services/SimulationFiscalSnapshot.php new file mode 100644 index 00000000..5902e42e --- /dev/null +++ b/app/Services/SimulationFiscalSnapshot.php @@ -0,0 +1,58 @@ + $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; + } +} diff --git a/app/Services/SimulationPublishedFiscal.php b/app/Services/SimulationPublishedFiscal.php new file mode 100644 index 00000000..6416fbb7 --- /dev/null +++ b/app/Services/SimulationPublishedFiscal.php @@ -0,0 +1,82 @@ +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): array + { + // 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 = []; + $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); + } +} diff --git a/app/Services/SimulationTaxParameterResolver.php b/app/Services/SimulationTaxParameterResolver.php index a8881434..43edea14 100644 --- a/app/Services/SimulationTaxParameterResolver.php +++ b/app/Services/SimulationTaxParameterResolver.php @@ -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; @@ -126,14 +127,30 @@ final class SimulationTaxParameterResolver * @return list> */ 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['status'] ?? null) !== $requiredStatus || ($version['source']['validation_status'] ?? null) !== 'validated' || (int) ($version['source_version_no'] ?? 0) !== (int) ($version['source']['version_no'] ?? -1) || ! $this->overlaps($version, $start, $end) diff --git a/app/Services/SimulationTaxRuleGovernance.php b/app/Services/SimulationTaxRuleGovernance.php index 831e60e4..5bc0f4aa 100644 --- a/app/Services/SimulationTaxRuleGovernance.php +++ b/app/Services/SimulationTaxRuleGovernance.php @@ -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]; diff --git a/config/simulation-fiscal-consumer.php b/config/simulation-fiscal-consumer.php new file mode 100644 index 00000000..8480badb --- /dev/null +++ b/config/simulation-fiscal-consumer.php @@ -0,0 +1,8 @@ + env('INVESTMOZ_IVA_CONSUMER_ENABLED', false), + 'rule_id' => env('INVESTMOZ_IVA_CONSUMER_RULE_ID'), + 'rule_version' => (int) env('INVESTMOZ_IVA_CONSUMER_RULE_VERSION', 0), +]; diff --git a/database/qa/iva-general-net-v1.json b/database/qa/iva-general-net-v1.json new file mode 100644 index 00000000..f800568c --- /dev/null +++ b/database/qa/iva-general-net-v1.json @@ -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" + } +} diff --git a/docs/investmoz/simulations/ficha-IVA-GERAL-BASE-LIQUIDA-v1.md b/docs/investmoz/simulations/ficha-IVA-GERAL-BASE-LIQUIDA-v1.md new file mode 100644 index 00000000..6883d437 --- /dev/null +++ b/docs/investmoz/simulations/ficha-IVA-GERAL-BASE-LIQUIDA-v1.md @@ -0,0 +1,98 @@ +# 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 + +**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. diff --git a/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json b/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json index d7305b5d..e6764dc6 100644 --- a/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json +++ b/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json @@ -319,5 +319,274 @@ "AUD-SIM-INT07": "demonstrated within bounded scope; see existing manual for limits", "AUD-SIM-INT08": "demonstrated within bounded scope; see existing manual for limits" } + }, + "integration_review_20261021": { + "functional_candidate": "13f68f1b", + "delivery_label": "melhorias de planeamento financeiro, recuperacao, exportacao e idioma", + "attachment_limit_bytes_before_and_after": 15728640, + "refused_fixture_bytes": 16777216, + "lower_limit_introduced": false, + "user_read_local_record": false, + "main_integration": false, + "fiscal_first_batch_proposal": { + "rule": "IVA general rate on net taxable base; provisional identifier only", + "demonstrated_coverage": "2026-01-01 through 2026-12-31; not invented legal expiry", + "data_work_hours": [ + 4, + 8 + ], + "code_and_acceptance_hours": [ + 12, + 20 + ], + "total_hours": [ + 16, + 28 + ], + "scope_and_rounding_evidence_required": true, + "rules_created": 0, + "rules_activated": 0 + } + }, + "fiscal_qa_v12_20261021": { + "instruction": "Tempo: exact decimal IVA calculation without automatic rounding; presentation distinct; no activation or main integration", + "supersedes": "v1.1 rounding-as-blocker and remaining estimate; no reopening accepted evidence", + "status": "QA scoped implementation verified; fiscal product remains partial AUD-SIM-INT02", + "proof": { + "synthetic_qa": true, + "database": "simulator_fiscal_delta_20261021_test", + "rule_id": "01a15d4e-c69b-7264-bdee-1e56ee88bf94", + "rule_status": "draft", + "registration_reused": true, + "simulation_id": "842e05f9-2b6e-4853-8ff5-81650633f6c4", + "report_id": "49cac0bd-f49c-4613-a8f0-1ee5c285cf81", + "exact_value": "0.0048", + "rounding_applied": false, + "report_sha256": "8ed8155a56fdf98ad26e2f01435644e062f27ca6b067ce3553415e974261c687", + "result_report_equal": true, + "previous_report_unchanged": true, + "published_rules": 0, + "source_reference_is_qa_fixture": true, + "human_legal_approval": false + }, + "tests": { + "feature": 5, + "feature_assertions": 51, + "resolver_unit": 7, + "resolver_assertions": 25, + "frontend": 3, + "typescript": "passed", + "build": "passed", + "pint": "passed" + }, + "browser_observation": "Actual component SSR and native report HTML fiscal delta observed in Edge; not a newly saved native PDF or complete browser history flow", + "limits": [ + "synthetic source reference and QA actors; not human legal approval", + "QA PostgreSQL role bypasses RLS; application denials only", + "no payment or return balance; scope declared", + "no activation, migration, main merge or deployment" + ], + "remaining_hours_scoped_qa_engineering": 0, + "previous_15_26_hours": "Historical conditional estimate includes work now performed, not measured hours; review/activation and fiscal product scope separate", + "record": "docs/investmoz/simulations/ficha-IVA-GERAL-BASE-LIQUIDA-v1.md" + }, + "review_83d2f97d_20261021": { + "scope": "Consolidated review; accepted evidence reused; only missing full interface journey targeted", + "legal_scope": "Internal general-rate IVA on previously determined net taxable base; context declared, not automatic legal incidence", + "period": "Rate chain since 2023-01-01; demonstrated coverage 2026, no artificial legal expiry", + "excluded": [ + "gross-price extraction", + "reduced rates/exemptions", + "special bases", + "imports/digital/nonresidents", + "deduction/credit/refund", + "incentives", + "other taxes" + ], + "concrete_gap": "83d2f97d had QA API and rendering but no input/recovery review screen", + "delta": "Protected QA-only Inertia review page calling existing calculation/history/immutable-report APIs", + "new_checks": { + "route_test": 1, + "assertions": 17, + "typescript": "passed", + "build": "passed" + }, + "interface_journey": { + "observed_at_utc": "2026-10-21T07:39:15+00:00", + "scope": "synthetic QA interface journey only", + "database": "simulator_fiscal_delta_20261021_test", + "simulation_id": "fa6d17f2-5f47-4cb6-b8cb-40850c12505f", + "report_id": "d13e2ad5-3ba8-4d01-9d30-5815496da774", + "report_sha256": "2e5e260f47ee5c849a34453fad248009e607ef8679dc56b7a1c58730f13e5c3b", + "input": { + "base": "1000.03", + "currency": "MZN", + "reference_date": "2026-10-21", + "scope_status": "user_declared" + }, + "calculated_value": "160.0048", + "status": "calculated", + "rounding_applied": false, + "browser_observed": [ + "normal_login_and_mfa", + "explicit_inputs", + "calculate_and_save", + "hard_reload", + "load_history", + "recover_same_id", + "read_immutable_report_same_value_and_provenance" + ], + "personal_simulation_count": 1, + "result_report_hash_equal": true, + "rule_status": "draft", + "published_rules": 0, + "tool_interruption": "Browser connection unavailable during initial authentication; later restored; no simulator failure attributed", + "initial_invalid_attempt": "Date editing was not committed initially; history checked empty before retry; one completed execution persisted", + "not_legal_applicability_validation": true, + "main_integration": false, + "rule_publication_or_activation": false, + "proof_files": [ + "fiscal-ui-complete-20261021.txt", + "fiscal-ui-complete-20261021.jpg", + "fiscal-ui-report-20261021.jpg" + ] + }, + "integration": "Separate Tempo decision for exact candidate including reviewed UI delta; origin/main read as 13f68f1b; auto deployment on push; no migration or rule transition", + "publication": "Existing submit/review/approve/publish with version/evidence; published parameter becomes visible to resolver API", + "activation": "No public fiscal consumer switch in 83d2f97d; QA draft preview must not be activated by removing guards or changing active APP_ENV", + "deactivation": "Existing suspend excludes published version from new resolution; snapshots/reports/history remain; QA draft preview stopped by closing QA exposure", + "status": "Consolidated scoped QA review demonstrated, including complete interface journey; no further generic preparation identified", + "user_legal_validation": false, + "main_merge": false, + "rule_publication_or_activation": false, + "tool_interruption_resolved": true + }, + "public_consumer_delta_proposal_20261021": { + "reviewed_head": "d186f942", + "status": "Bounded product code delta required; proposal only", + "existing_reused": [ + "permissions and anonymous token/organization ownership", + "rule governance", + "temporal resolver and expression evaluator", + "exact decimal arithmetic", + "execution idempotency and immutable history/reports" + ], + "operations": [ + "integrate compatible code with consumer disabled", + "publish exact rule version through existing governance", + "activate explicit consumer binding to authorized published version" + ], + "coverage": "IVA general net taxable base, MZN, declared internal operation, 2026 only", + "suspension": "No new admissions; previously admitted frozen executions may complete; historical reads and exports preserved", + "accepted_evidence_repeated": false, + "main_integration": false, + "rule_publication": false, + "consumer_activation": false, + "user_legal_validation": false, + "record": "docs/investmoz/unification/traceability-and-manual.md: Proposta unica - consumidor IVA" + }, + "published_consumer_qa_20261021": { + "proposal_base": "2ced7b79", + "status": "Implemented and demonstrated in normal QA simulator; separate integration/publication/activation decisions pending", + "consumer_default_enabled": false, + "scope": "IVA general net taxable base, declared internal general operation, MZN, exigibility 2026 only", + "feature_tests": { + "passed": 8, + "assertions": 75, + "isolated_database": "simulator_fiscal_checks_20261021_test", + "rls_non_superuser": true, + "separate_connection_lock_probe": true + }, + "frontend_new_link_tests_passed": 2, + "typescript": "passed", + "build": "passed", + "pint": "passed", + "accepted_evidence_reused": [ + "decimal calculation", + "exporters", + "native PDF pagination", + "financial planning and recovery" + ], + "normal_simulator_browser_journey": { + "database": "simulator_fiscal_delta_20261021_test", + "url": "http://127.0.0.1:18580/simulador-fiscal", + "fixture_only": true, + "user_id": 167, + "rule_id": "01a15ddc-b0c5-727f-bb24-d806a69bd1f5", + "rule_status": "suspended", + "suspended_at": "2026-10-21T09:52:16.000000Z", + "executions": [ + { + "id": "53663785-2e5d-4be2-8a05-c0c96a57896d", + "status": "completed", + "base": "1000.03", + "value": null, + "result_status": "indeterminate", + "scope_status": "undetermined", + "admitted_at": "2026-10-21T09:50:53.949756Z", + "frozen_rule_status": "published", + "rule_id": "01a15ddc-b0c5-727f-bb24-d806a69bd1f5", + "rule_version": 2, + "report_id": "63d427ac-e6ae-481c-adad-e5f39fcc3c15", + "report_sha256": "d405705f6ce050518a0b400bed842b2aeaec143af3d78c54088100417d326c64", + "result_report_hash_equal": true + }, + { + "id": "5fa778f1-413e-4643-8c67-1b93ab290da7", + "status": "completed", + "base": "1000.03", + "value": "160.0048", + "result_status": "calculated", + "scope_status": "user_declared", + "admitted_at": "2026-10-21T09:51:11.307106Z", + "frozen_rule_status": "published", + "rule_id": "01a15ddc-b0c5-727f-bb24-d806a69bd1f5", + "rule_version": 2, + "report_id": "533b60df-ce44-40fd-9911-c7ed492dbd7e", + "report_sha256": "2c5cc798c06920aee0707465ab8b7fee517783db24939b3921c76a70999dad77", + "result_report_hash_equal": true + }, + { + "id": "be1e8399-ea95-4aa7-9ac0-7fa392932ac1", + "status": "completed", + "base": "1001.03", + "value": "160.1648", + "result_status": "calculated", + "scope_status": "user_declared", + "admitted_at": "2026-10-21T09:52:11.782758Z", + "frozen_rule_status": "published", + "rule_id": "01a15ddc-b0c5-727f-bb24-d806a69bd1f5", + "rule_version": 2, + "report_id": "5e416a48-db07-449b-8eae-aa51a1538f04", + "report_sha256": "997f026665e586c662ae574fd2ff7f62e0254819adb26aea920575e47e0490a3", + "result_report_hash_equal": true + } + ], + "execution_count": 3, + "pending_queue_records": 0, + "main_integration": false, + "homologation_publication_or_activation": false + }, + "observed": [ + "initial disabled consumer", + "insufficient scope persisted as indeterminate", + "declared scope calculated through published server binding", + "repeat without duplicate", + "database queue admitted before suspension and processed after suspension", + "new input refused with values preserved", + "hard reload and history recovery of calculated and indeterminate snapshots", + "CSV control enabled without rerunning download" + ], + "final_qa_fixture_status": "suspended", + "final_qa_switch_enabled": false, + "historical_records_preserved": true, + "main_local_head": "13f68f1b", + "main_integration": false, + "homologation_rule_publication": false, + "homologation_consumer_activation": false, + "user_legal_validation": false, + "user_local_file_review": false, + "procedure": "Existing manual: Procedimento unico das tres operacoes - agora executavel apos decisao", + "proofs_private": true } } diff --git a/docs/investmoz/simulations/source-archive/BR-16-I-Serie-2008-04-16.pdf b/docs/investmoz/simulations/source-archive/BR-16-I-Serie-2008-04-16.pdf new file mode 100644 index 00000000..22e40710 Binary files /dev/null and b/docs/investmoz/simulations/source-archive/BR-16-I-Serie-2008-04-16.pdf differ diff --git a/docs/investmoz/simulations/source-archive/Lei-13-2016-CIVA-BR-156-10-suplemento.pdf b/docs/investmoz/simulations/source-archive/Lei-13-2016-CIVA-BR-156-10-suplemento.pdf new file mode 100644 index 00000000..4b7bc52e Binary files /dev/null and b/docs/investmoz/simulations/source-archive/Lei-13-2016-CIVA-BR-156-10-suplemento.pdf differ diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index ce647e09..943fa454 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -1714,3 +1714,193 @@ A aceitação das fontes oficiais determinada pela Tempo em08/10 prevalece sobre 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=` e `INVESTMOZ_IVA_CONSUMER_RULE_VERSION=`; 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. diff --git a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv index c71ad17c..69444bfe 100644 --- a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv +++ b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv @@ -10652,10 +10652,10 @@ AUD-SEARCH-T10,Consistência transversal da pesquisa e navegação — Bibliotec AUD-SEARCH-T11,Consistência transversal da pesquisa e navegação — Legislação,Instruções expressas da Tempo na conversa de 20/10/2026; AGENTS.md; RF-MN-010 e AUD-GEO-01 preservados.,navegação/pesquisa,/legislacao,portal/services; portal/document — Reutiliza a correcção da biblioteca e preserva filtros jurídicos/documentais pertinentes.,Sem alteração remota nesta execução; revisão apenas na branch codex/opportunity-search.,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,QA — delta delimitado; revisão Tempo pendente,"Situação jurídica, consulta e cálculo continuam independentes.",,"Filtros, contagem, resultados e URL coerentes; universos identificados; erro distinto de vazio; permissões e contexto conservados.",P1,,RF-MN-010; AUD-GEO-01,,,,,,,,,,,Mesmo padrão da biblioteca; não deve expor um universo concorrente.,"Situação jurídica, consulta e cálculo continuam independentes.",portal/services; portal/document; SearchState; SearchFeedback; TerritoryFields; portalNavigation,,"Integração em main apenas após revisão; assistência NLP, contratos e P7 separados.",Correcção/simplificação em QA,,HTTP/Inertia: resources.total=0; consumidor e query pública partilhados.,Pesquisa transversal QA 20/10/2026,20/10/2026,,,,,,,,,,"Provas comuns cobrem o componente reutilizado; HTTP vazio não demonstra operações de gestão ou publicação. Situação jurídica, consulta e cálculo continuam independentes.",,falha funcional; navegação; apresentação visual,,Disponível para revisão na aplicação QA local. AUD-GUIDE-CARD-20261020,"Related educational cards: image, source summary, clear actions",Tempo 20/10/2026; independent delta after 0e7d51f18fb4,educational presentation,/sectores/{slug}; /explorar/{group}; articles; /oportunidades and detail,EducationalArticleCard; EducationalGuides; LegacyContent; SectorInvestment,QA only,1 test / 8 assertions; PHP syntax; TypeScript/build; desktop2048/mobile390; keyboard,QA review pending,Normal QA sector and article verified against existing library resource 10 and local image 25; previous dossier proof retained,,Source-derived summary; public images; conditional opportunity action; context preserved,P1,,,,,,,,,,,,,,,,,,presentation,,,,20/10/2026,,,,,,,,,,,,content; navigation; visual presentation,,Ready for QA review AUD-SIM-INT01,Invalid one-off fiscal date can throw instead of returning validation state,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Fixed in QA; invalid-date checks; 38 directed frontend tests passed,Previous inline-date proof retained; unchanged delta not repeated,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,, | [Saldo consolidado21/10/2026] Concluído no recorte. Nenhum delta de validação de data por demonstrar; prova anterior preservada. Ver saldo dos oito itens no manual existente; sem fecho global por esta reconciliação.,,20/10/2026,,,,,,,,,,,,,, -AUD-SIM-INT02,Review actual fiscal parameters sources periods and eligibility,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Verification pending; no parameter activation,QA zero rules verified with full visibility; prepared archive and analyses reused; current homologation read awaits SSH authentication,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,," | [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. | [Saldo consolidado21/10/2026] 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. Ver saldo dos oito itens no manual existente; sem fecho global por esta reconciliação. | [Candidata21/10/2026] Diagnóstico fiscal separado no manual existente: fontes/analises já disponíveis; zero parâmetros executáveis na QA e homologação por leitura de todos os estados; governação/resolvedor temporal/DSL existentes; RunSimulation ainda aritmética personalizada sem consumir regras fiscais. Saldo: cadastro fundamentado, orquestração, contrato de resultado/histórico e ligação ao dossier/relatório, com casos por componente/período. Nenhuma regra criada nem activada; implementação fiscal integral aberta e excluída das1–2h de exportações.",,20/10/2026,,,,,,,,,,,,,, +AUD-SIM-INT02,Review actual fiscal parameters sources periods and eligibility,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Verification pending; no parameter activation,Parcial: primeiro consumidor IVA funcional no simulador normal em QA; cobertura fiscal integral e disponibilizacao na homologacao permanecem separadas.,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P0,,,,,,,,,,,,,,"Ligacao IVA do lote 2026 demonstrada em QA. Decisoes pendentes distintas: integrar candidata, publicar versao real e activar consumidor na homologacao. Demais componentes fiscais fora deste recorte.",,,"Tempo decide integracao do codigo, publicacao da versao real e activacao do consumidor conforme procedimento unico; nenhuma executada nesta passagem.",Ligacao de consumidor publicada concluida em QA; decisoes operacionais separadas.,Sem saldo de implementacao identificado para este delta QA; nao reutilizar estimativas anteriores como horas ainda em falta; nao estima cobertura fiscal integral.," | [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. | [Saldo consolidado21/10/2026] 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. Ver saldo dos oito itens no manual existente; sem fecho global por esta reconciliação. | [Candidata21/10/2026] Diagnóstico fiscal separado no manual existente: fontes/analises já disponíveis; zero parâmetros executáveis na QA e homologação por leitura de todos os estados; governação/resolvedor temporal/DSL existentes; RunSimulation ainda aritmética personalizada sem consumir regras fiscais. Saldo: cadastro fundamentado, orquestração, contrato de resultado/histórico e ligação ao dossier/relatório, com casos por componente/período. Nenhuma regra criada nem activada; implementação fiscal integral aberta e excluída das1–2h de exportações. | [Proposta21/10/2026] Primeiro lote proposto: uma regra IVA geral sobre base liquida/MZN, cobertura demonstrada2026 sem inventar caducidade legal. Reutilizar fontes/analises/governacao/resolvedor/avaliador existentes; arredondamento e ambito aplicavel explicitamente por fundamentar antes de publicar. Cadastro4-8h e codigo/aceitacao12-20h, total16-28h condicionado ao acervo, separado das1-2h de exportacao. Criterios e exclusoes no manual existente. Nenhuma regra cadastrada/activada; nao e implementacao fiscal integral. | [QA IVA v1.2 21/10/2026] Orientacao posterior Tempo: calcular com precisao decimal exacta sem arredondamento automatico, separando apresentacao monetaria; arredondamento generico deixa de ser bloqueio deste lote. Cadastro draft versionado em clone QA; resolvedor/avaliador existentes; proveniencia, historico e relatorio imutavel ligados. 5 feature (51 assercoes), 7 unit (25), 3 frontend; types/build/Pint aprovados. Contexto declarado; nao e saldo a pagar. Zero regras publicadas; sem main/activacao. Recorte QA autorizado verificado (0h engenharia identificada restante); produto fiscal integral continua parcial. Limites e provas no mesmo manual e ficha v1.2; sem atribuicao de validacao juridica ao utilizador. | [Revisao 83d2f97d] Ambito/periodo/exclusoes e procedimentos de integracao, publicacao, activacao e suspensao consolidados no mesmo manual. Lacuna concreta de ecra QA fechada: entradas 1000.03/MZN/2026-10-21; calculo exacto 160.0048; gravacao fa6d17f2-5f47-4cb6-b8cb-40850c12505f; recarga; recuperacao do mesmo ID; relatorio d13e2ad5-3ba8-4d01-9d30-5815496da774 com mesmo valor/proveniencia e hash igual ao resultado. Uma execucao persistida, draft e zero regras publicadas. 1 teste novo de rota/17 assercoes, TypeScript/build e browser aprovados; restantes provas reutilizadas. Interrupcao inicial da ferramenta resolvida, sem atribuicao de falha ao simulador. Revisao do recorte demonstrada; sem nova preparacao generica identificada, main ou activacao; nao fecha aplicabilidade fiscal universal nem simulador integral. | 21/10/2026: proposta do consumidor IVA no simulador existente no manual: falta ligacao a regra publicada e activacao explicita. Integrar codigo desligado, publicar versao e activar consumidor sao tres operacoes distintas. Reutiliza permissoes, governanca, historico e provas aceites; sem ampliar cobertura ou executar estas operacoes. | 21/10/2026: implementado delta de 2ced7b79 no simulador normal QA, activacao desligada por omissao. 8 testes/75 assercoes, 2 testes frontend novos, TypeScript/build/Pint. Edge: indeterminacao, calculo, repeticao sem duplicar, fila admitida antes de suspensao concluida depois, nova execucao recusada e historicos/relatorios preservados. Mesmo manual e recibo consolidam provas e tres operacoes. Fixture isolada suspensa; sem merge ou publicacao/activacao na homologacao.",,20/10/2026,,,,,,,,,,,,,, AUD-SIM-INT03,Review isolation recovery across projects and sessions,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing controls and accepted proofs to reconcile; no leak established,Two dossiers and new login recovered in browser; cashflow persistence corrected and HTTP-tested; new visual cashflow delta pending,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P0,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,"[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. | [Saldo consolidado21/10/2026] 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. Ver saldo dos oito itens no manual existente; sem fecho global por esta reconciliação.",,20/10/2026,,,,,,,,,,,,,, AUD-SIM-INT04,Review stale and historical results and correct dossier,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing versions/fingerprints; targeted verification pending,Historical results visible after fix; 12 and 24 recovered; hashes unchanged; synthetic temporal resolver versions verified in memory only,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,," | [Saldo consolidado21/10/2026] 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. Ver saldo dos oito itens no manual existente; sem fecho global por esta reconciliação.",,20/10/2026,,,,,,,,,,,,,, AUD-SIM-INT05,Independent financial controls unknown zero units and currencies,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Reconciliation pending; no invented tax assumptions,Independent business-plan and cashflow ledgers passed; complete credit recipe included in screen and exports,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,"[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. | [Saldo consolidado21/10/2026] 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. Ver saldo dos oito itens no manual existente; sem fecho global por esta reconciliação.",,20/10/2026,,,,,,,,,,,,,, AUD-SIM-INT06,Generated reports screen download and pagination,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing exporters; examples for this review still pending,Normal UI CSV and DOCX match shared values; all six DOCX pages checked; native Edge PDF awaits user save; new cashflow visual output pending,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,,"[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. | [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. | [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é 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 consolidado21/10/2026] 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. Ver saldo dos oito itens no manual existente; sem fecho global por esta reconciliação.",,20/10/2026,,,,,,,,,,,,,, -AUD-SIM-INT07,Explicit validate calculate save export errors and retry,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing idempotency; consumer-level verification pending,Calculation navigation and failed-save retry fixed and browser-verified; normal downloads verified; interactive attachment blocked by extension permission,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,," | [Saldo consolidado21/10/2026] 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. Ver saldo dos oito itens no manual existente; sem fecho global por esta reconciliação. | [Candidata21/10/2026] Concluído o recorte dirigido em QA: Word/PDF/ZIP falha única injectada→mensagem portuguesa→nova tentativa no mesmo botão. ZIP preserva título editado, secção desmarcada e anexo; arquivo real contém uma só cópia original/hash igual. Recusa16MB observada mantendo1anexo; nova selecção válida limpa erro e mantém1entrada. Guarda síncrona impede concorrência/duplicação; teste de falha de leitura preserva lista/legenda e permite retoma.4testes dirigidos passaram, TypeScript/build passaram. UI usa componente/geradores reais num invólucro QA sintético; a injecção e a omissão da impressão automática nesse ensaio não integram a entrega. Não repetidas10páginas/valores/CSV aceites.",,20/10/2026,,,,,,,,,,,,,, +AUD-SIM-INT07,Explicit validate calculate save export errors and retry,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Existing idempotency; consumer-level verification pending,Calculation navigation and failed-save retry fixed and browser-verified; normal downloads verified; interactive attachment blocked by extension permission,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,," | [Saldo consolidado21/10/2026] 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. Ver saldo dos oito itens no manual existente; sem fecho global por esta reconciliação. | [Candidata21/10/2026] Concluído o recorte dirigido em QA: Word/PDF/ZIP falha única injectada→mensagem portuguesa→nova tentativa no mesmo botão. ZIP preserva título editado, secção desmarcada e anexo; arquivo real contém uma só cópia original/hash igual. Recusa16MB observada mantendo1anexo; nova selecção válida limpa erro e mantém1entrada. Guarda síncrona impede concorrência/duplicação; teste de falha de leitura preserva lista/legenda e permite retoma.4testes dirigidos passaram, TypeScript/build passaram. UI usa componente/geradores reais num invólucro QA sintético; a injecção e a omissão da impressão automática nesse ensaio não integram a entrega. Não repetidas10páginas/valores/CSV aceites. | [Limite conferido21/10/2026] c22003a3 ja definia15*1024*1024 bytes por anexo,10anexos/50MiB total;13f68f1b nao reduz esses limites. Teste16MiB=16777216bytes excede15MiB=15728640bytes. Limite2MiB de incorporacao textual distinto de aceitar original no ZIP. Origem: escolha tecnica preexistente, sem inferir aprovacao organizacional especifica. Entrega: melhorias de planeamento financeiro, recuperacao, exportacao e idioma; revisao antes de main.",,20/10/2026,,,,,,,,,,,,,, AUD-SIM-INT08,Portuguese across portal and simulator outputs,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,"Partial QA fix: rate labels, password confirmation/security and spinner; wider inventory pending",Generic HTTP and fiscal-expression validation messages translated; status/correlation verified; selectable translations and source titles preserved,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P1,,,,,,,,,,,,,,,,,,diagnosis and targeted corrections,," | [Saldo consolidado21/10/2026] 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. Ver saldo dos oito itens no manual existente; sem fecho global por esta reconciliação. | [Candidata21/10/2026] Mensagens dinâmicas dos exportadores Word/ZIP/PDF/CSV e leitura de anexos usam texto português por operação, preservação e nova tentativa com referênciaEXP. Excepção original só na consola técnica local com referência/operação; sem telemetria nem auditoria remota novas. UI confirmou português nas3falhas de geração;4testes verificam separação mensagem/diagnóstico e correlação em5operações. Traduções/404/validações anteriores reutilizados. Fecho dos consumidores afectados, não certificação de todos os textos de catálogo ou nova sessão administrativa de governação.",,20/10/2026,,,,,,,,,,,,,, diff --git a/resources/js/components/fiscal-qa-preview.tsx b/resources/js/components/fiscal-qa-preview.tsx new file mode 100644 index 00000000..dfdca391 --- /dev/null +++ b/resources/js/components/fiscal-qa-preview.tsx @@ -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 ( +
+
+ {rows[0][1]} — cálculo exacto sem arredondamento +
+
+ {rows.map(([label, value]) => ( +
+
{label}
+
{value}
+
+ ))} +
+
+ ); +} diff --git a/resources/js/components/simulation-form.tsx b/resources/js/components/simulation-form.tsx index f3e51e69..5500ae73 100644 --- a/resources/js/components/simulation-form.tsx +++ b/resources/js/components/simulation-form.tsx @@ -1,9 +1,11 @@ +import { SimulationPublishedIva, type PublishedIvaInput } from './simulation-published-iva'; 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, @@ -886,6 +888,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; @@ -2157,15 +2160,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]}`) @@ -2176,7 +2179,7 @@ export function SimulationForm() { setIsCalculating(true); try { const xsrf = csrfToken(); - const body = { + const body = publishedInput ?? { base: draft.base, rate: draft.rate, reduction: draft.reduction, @@ -2284,7 +2287,7 @@ export function SimulationForm() { } } if ( - JSON.stringify({ + publishedInput || JSON.stringify({ base: draft.base, rate: draft.rate, reduction: draft.reduction, @@ -2304,7 +2307,7 @@ export function SimulationForm() { 'O cálculo não foi concluído. Os dados introduzidos foram preservados.', ); } - sessionStorage.removeItem(idempotencyStorageKey); + if (!publishedInput) sessionStorage.removeItem(idempotencyStorageKey); } catch (cause) { setServerError( cause instanceof Error @@ -3598,6 +3601,7 @@ export function SimulationForm() { } /> )} + {current.id === 'tax' && } {current.id === 'tax' && (

- {current.id === 'tax' + {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'}

@@ -3769,7 +3773,7 @@ export function SimulationForm() { submetido.

)} - {result.status === 'completed' && ( + {result.status === 'completed' && !isFiscalQaPreview && (

Resultado aritmético indicativo:{' '} @@ -3806,9 +3810,10 @@ export function SimulationForm() { taxas legais, elegibilidade ou obrigação fiscal.

+ {isFiscalQaPreview && } {result.status === 'completed' && (
- {arithmeticResult.integrity !== + {!isFiscalQaPreview && arithmeticResult.integrity !== 'invalid' && (
@@ -4179,8 +4184,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 === @@ -4419,7 +4423,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.

+ + {error &&

{error}

} + ; +} diff --git a/resources/js/lib/fiscal-qa-preview.ts b/resources/js/lib/fiscal-qa-preview.ts new file mode 100644 index 00000000..50fda83d --- /dev/null +++ b/resources/js/lib/fiscal-qa-preview.ts @@ -0,0 +1,60 @@ +import { isApiSimulationResultSnapshotV1 } from "./simulation-contract.ts"; + +export function fiscalQaRows(value: unknown): Array<[string, string]> { + if (!isApiSimulationResultSnapshotV1(value)) return []; + const snapshot = value as unknown as Record; + 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 | undefined; + const rule = provenance?.rule as Record | undefined; + const source = rule?.source as Record | undefined; + const parameters = rule?.parameters as + Record> | 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"; + return [ + ["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)], + ["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.", + ], + ]; +} diff --git a/resources/js/lib/project-dossier-report.ts b/resources/js/lib/project-dossier-report.ts index 7d732ff9..fa791d37 100644 --- a/resources/js/lib/project-dossier-report.ts +++ b/resources/js/lib/project-dossier-report.ts @@ -1,4 +1,5 @@ import { displayValue } from './display-value.ts'; +import { fiscalQaRows } from './fiscal-qa-preview.ts'; import { buildProjectDossierRows, type ProjectDossierRow, @@ -344,6 +345,11 @@ function makeSections( } const finance = sections.get('finance'); + if (finance && ['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(currentCalculation?.snapshot?.scenario_kind)) && currentCalculation?.snapshot) { + for (const [field, value] of fiscalQaRows(currentCalculation.snapshot)) { + finance.rows.push({ field, value, origin: 'Snapshot fiscal preservado; sem recálculo', classification: 'Cálculo exacto; ver estado da regra no snapshot; não é saldo a pagar' }); + } + } if (finance && currentCalculation) { const snapshot = currentCalculation.snapshot; const result = @@ -417,10 +423,12 @@ function makeSections( { field: 'Fórmula e valores usados', value: - snapshot && typeof snapshot.formula_version === 'string' + ['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(snapshot?.scenario_kind)) + ? `Base tributável declarada × taxa cadastrada ÷ 100; cálculo decimal exacto, sem arredondamento. Ver regra e proveniência acima.` + : snapshot && typeof snapshot.formula_version === 'string' ? `Fórmula ${snapshot.formula_version}; base ${displayValue(snapshot.base, 'não disponível')}; taxa ${displayValue(snapshot.rate_percent, 'não disponível')}%; redução ${displayValue(snapshot.reduction_percent, 'não disponível')}%; moeda ${displayValue(snapshot.currency, 'não indicada')}` : 'A fórmula não foi identificada no snapshot; ver explicação da execução', - origin: 'Snapshot guardado; sem aplicação de regra legal', + origin: ['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(snapshot?.scenario_kind)) ? (snapshot?.scenario_kind === 'fiscal_rule_published' ? 'Versão publicada na admissão; sem validação jurídica da operação' : 'Snapshot QA de regra em rascunho; sem activação ou validação jurídica da operação') : 'Snapshot guardado; sem aplicação de regra legal', classification: 'Reutilização do resultado existente', }, { diff --git a/resources/js/lib/simulation-form.ts b/resources/js/lib/simulation-form.ts index d3d78df5..a64d1327 100644 --- a/resources/js/lib/simulation-form.ts +++ b/resources/js/lib/simulation-form.ts @@ -1,4 +1,5 @@ import { csvCell } from './csv.js'; +import { fiscalQaRows } from './fiscal-qa-preview.ts'; import { isApiSimulationResultSnapshotV1 } from './simulation-contract.ts'; export type SimulationDraft = Record; @@ -1674,6 +1675,19 @@ export function readArithmeticSnapshot( } const result = snapshot as Record; const value = result.indicative_total; + if (['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(result.scenario_kind))) { + const presentation = result.presentation as Record | undefined; + if (isApiSimulationResultSnapshotV1(result) && fiscalQaRows(result).length > 0 && + result.result.value === value && presentation?.value === value && + presentation?.rounding_applied === false && result.context.currency === 'MZN' && + result.result.unit === 'MZN' && + ((result.result.status === 'calculated' && typeof value === 'string' && /^\d+(?:\.\d+)?$/.test(value)) || + (result.result.status === 'indeterminate' && value === null && result.result.missing_inputs.length > 0))) { + return { integrity: 'versioned', value: value as string | null, + context: result.context as unknown as Record, explanation: result.result.explanation }; + } + return { integrity: 'invalid', value: null, context: null, explanation: null }; + } if (typeof value !== 'string' || !/^(?:\d+)(?:\.\d+)?$/.test(value)) { return { integrity: 'invalid', @@ -1848,6 +1862,13 @@ export function exportCompletedArithmeticCsv( ); } const result = simulation.result_snapshot as Record; + if (['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(result.scenario_kind))) { + const rows = fiscalQaRows(result); + const report = readVersionedArithmeticReportReference(simulation); + if (!rows.length || !report) throw new Error('A pré-visualização fiscal não corresponde ao relatório preservado.'); + return `\uFEFF${[...rows, ['ID do relatório', report.id], ['Hash do relatório', report.report_sha256], ['Data de exportação', exportedAt]] + .map((field) => field.map(csvCell).join(',')).join('\r\n')}`; + } const decimal = (value: unknown, label: string) => { if (typeof value !== 'string' || !/^(?:\d+)(?:\.\d+)?$/.test(value)) throw new Error( diff --git a/resources/js/pages/management/fiscal-qa-review.tsx b/resources/js/pages/management/fiscal-qa-review.tsx new file mode 100644 index 00000000..0874213d --- /dev/null +++ b/resources/js/pages/management/fiscal-qa-review.tsx @@ -0,0 +1,356 @@ +import { Head } from "@inertiajs/react"; +import { useRef, useState, type FormEvent } from "react"; +import FiscalQaPreview from "@/components/fiscal-qa-preview"; +import { readVersionedArithmeticReportReference } from "@/lib/simulation-form"; +import { + isApiSimulationCreateResponseV1, + isApiSimulationReadResponseV1, + isApiSimulationHistoryResponseV1, + isApiSimulationResultSnapshotV1, + type ApiSimulationExecutionV1, + type ApiSimulationHistoryEntryV1, +} from "@/lib/simulation-contract"; + +class ReviewOperationError extends Error {} + +/** Review-only UI: it calls the existing authenticated QA API and immutable report reader. */ +export default function FiscalQaReview({ + rule, +}: { + rule: { id: string; code: string; version: number }; +}) { + const [base, setBase] = useState(""); + const [date, setDate] = useState(""); + const [currency, setCurrency] = useState(""); + const [declared, setDeclared] = useState(false); + const [selected, setSelected] = useState( + null, + ); + const [history, setHistory] = useState([]); + const [report, setReport] = useState<{ + id: string; + hash: string; + snapshot: unknown; + } | null>(null); + const [busy, setBusy] = useState(false); + const [message, setMessage] = useState(""); + const key = useRef(null); + const gate = useRef(false); + const changed = () => { + key.current = null; + }; + const statusLabel = (status: string) => + ({ + queued: "na fila", + running: "em processamento", + completed: "concluída", + failed: "não concluída", + cancelled: "cancelada", + })[status] ?? "recebida"; + + async function request( + url: string, + body?: Record, + ): Promise { + const xsrf = decodeURIComponent( + document.cookie + .split("; ") + .find((part) => part.startsWith("XSRF-TOKEN=")) + ?.slice(11) ?? "", + ); + const response = await fetch(url, { + method: body ? "POST" : "GET", + credentials: "same-origin", + headers: { + Accept: "application/json", + ...(body + ? { + "Content-Type": "application/json", + "X-XSRF-TOKEN": xsrf, + "Idempotency-Key": key.current!, + } + : {}), + }, + ...(body ? { body: JSON.stringify(body) } : {}), + }); + if (!response.ok) + throw new ReviewOperationError( + response.status === 401 || response.status === 419 + ? "A sessão expirou. Inicie sessão e consulte o histórico antes de voltar a calcular." + : "Não foi possível concluir a operação. Reveja as entradas e consulte o histórico antes de tentar novamente.", + ); + return response.json(); + } + + async function action(work: () => Promise) { + if (gate.current) return; + gate.current = true; + setBusy(true); + setMessage(""); + try { + await work(); + } catch (cause) { + console.error("[fiscal-qa-review]", cause); + setMessage( + cause instanceof ReviewOperationError + ? cause.message + : "Não foi possível concluir a ligação ao servidor. Consulte o histórico antes de tentar novamente.", + ); + } finally { + gate.current = false; + setBusy(false); + } + } + + async function recover(id: string) { + const payload = await request(`/api/v1/simulations/${id}`); + if (!isApiSimulationReadResponseV1(payload)) + throw new ReviewOperationError( + "O resultado guardado tem formato incompatível.", + ); + setSelected(payload.data); + setReport(null); + return payload.data; + } + + async function calculate(event: FormEvent) { + event.preventDefault(); + await action(async () => { + key.current ??= crypto.randomUUID(); + const payload = await request( + `/api/v2/investment/admin/tax-rules/${rule.id}/qa-preview`, + { + base, + currency, + reference_date: date, + operation: "internal_general_net", + }, + ); + if (!isApiSimulationCreateResponseV1(payload)) + throw new ReviewOperationError( + "A execução devolveu um formato incompatível.", + ); + let simulation = await recover(payload.data.id); + for ( + let attempt = 0; + attempt < 10 && + ["queued", "running"].includes(simulation.status); + attempt++ + ) { + await new Promise((resolve) => setTimeout(resolve, 500)); + simulation = await recover(payload.data.id); + } + setMessage( + simulation.status === "completed" + ? "Execução concluída e gravada no histórico. Pode recuperar esta execução e consultar o relatório guardado." + : "A execução foi gravada e continua pendente. Consulte o histórico para a recuperar.", + ); + }); + } + + async function loadHistory() { + const payload = await request( + "/api/v1/simulations/history?per_page=10", + ); + if (!isApiSimulationHistoryResponseV1(payload)) + throw new ReviewOperationError( + "O histórico devolveu um formato incompatível.", + ); + setHistory(payload.data); + } + + async function loadReport() { + const reference = readVersionedArithmeticReportReference(selected); + if (!selected || !reference) + throw new ReviewOperationError( + "O relatório ainda não está disponível para esta execução.", + ); + const payload = (await request( + `/api/v1/simulations/${selected.id}/reports/${reference.id}`, + )) as { + data?: { + id?: string; + report_sha256?: string; + report_snapshot?: unknown; + }; + }; + if ( + payload.data?.id !== reference.id || + payload.data.report_sha256 !== reference.report_sha256 || + !isApiSimulationResultSnapshotV1(payload.data.report_snapshot) + ) { + throw new ReviewOperationError( + "O relatório guardado não corresponde à referência desta execução.", + ); + } + setReport({ + id: reference.id, + hash: reference.report_sha256, + snapshot: payload.data.report_snapshot, + }); + } + + const field = "mt-1 block w-full rounded border border-slate-400 p-2"; + const button = + "rounded border border-emerald-800 px-4 py-2 font-semibold disabled:opacity-50"; + return ( +
+ +

+ Revisão do lote IVA em QA +

+

+ {rule.code} · versão {rule.version} · rascunho, sem activação. + Cobertura demonstrada: 2026. +

+

+ Este percurso calcula IVA sobre uma base já apurada, em contexto + declarado. Não determina elegibilidade, isenção, saldo de + declaração ou valor a pagar. O cálculo exacto não aplica + arredondamento automático. +

+
void calculate(event)} + className="space-y-4 rounded border p-4" + > +
+ + + + + +
+
+ {message && ( +

+ {message} +

+ )} + {selected && ( +
+

Execução seleccionada

+

+ ID: {selected.id} · estado:{" "} + {statusLabel(selected.status)} +

+ + +
+ )} +
+

Histórico da conta

+ +
    + {history.map((entry) => ( +
  • + {entry.id} · {statusLabel(entry.status)}{" "} + +
  • + ))} +
+
+ {report && ( +
+

+ Relatório guardado +

+

ID do relatório: {report.id}

+

SHA256: {report.hash}

+

+ Conteúdo recuperado do relatório imutável, sem novo + cálculo. +

+ +
+ )} +
+ ); +} diff --git a/routes/api.php b/routes/api.php index 7ef162ec..ee9b7010 100644 --- a/routes/api.php +++ b/routes/api.php @@ -13,6 +13,7 @@ use App\Http\Controllers\PortalServicesController; use App\Http\Controllers\PublicBusinessOpportunityController; use App\Http\Middleware\CorrelationId; use App\Http\Middleware\EnsureSimulationRecoveryCookie; +use App\Services\SimulationPublishedFiscal; use Illuminate\Support\Facades\Route; Route::prefix('v1')->middleware(['throttle:api', CorrelationId::class])->group(function () { @@ -38,6 +39,7 @@ Route::prefix('v1')->middleware(['throttle:api', CorrelationId::class])->group(f }); Route::prefix('v1')->middleware(['web', 'throttle:api', CorrelationId::class])->group(function () { + Route::get('/fiscal-consumer', fn (SimulationPublishedFiscal $consumer) => response()->json(['data' => $consumer->availability()])->header('Cache-Control', 'no-store')); Route::post('/tax-parameters/resolve', [SimulationTaxParameterController::class, 'resolve'])->middleware('throttle:20,1'); Route::get('/questionnaires/{code}', [SimulationController::class, 'questionnaire']); Route::middleware(EnsureSimulationRecoveryCookie::class)->group(function () { diff --git a/routes/investment-zones.php b/routes/investment-zones.php index b293b1ac..78b3752e 100644 --- a/routes/investment-zones.php +++ b/routes/investment-zones.php @@ -27,6 +27,8 @@ Route::middleware([CorrelationId::class, 'auth', 'verified', \App\Http\Middlewar Route::post('/api/v2/investment/admin/tax-rules', [SimulationTaxRuleGovernanceController::class, 'store']); Route::get('/api/v2/investment/admin/tax-rules/{id}', [SimulationTaxRuleGovernanceController::class, 'show'])->whereUuid('id'); Route::put('/api/v2/investment/admin/tax-rules/{id}', [SimulationTaxRuleGovernanceController::class, 'update'])->whereUuid('id'); + Route::post('/api/v2/investment/admin/tax-rules/{id}/qa-preview', [SimulationTaxRuleGovernanceController::class, 'qaPreview'])->whereUuid('id'); + Route::get('/management/investment/tax-rules/{id}/qa-review', [SimulationTaxRuleGovernanceController::class, 'qaReview'])->whereUuid('id'); Route::post('/api/v2/investment/admin/tax-rules/{id}/{action}', [SimulationTaxRuleGovernanceController::class, 'transition']) ->whereUuid('id')->whereIn('action', ['submit', 'review', 'reject', 'approve', 'publish', 'suspend', 'archive']); Route::get('/management/investment/zones/operational-install',function(\Illuminate\Http\Request $r,\App\Services\InvestmentZoneOperationalDetailsInstallation $s){return response()->json(['plan'=>$s->preview($r->user())]);}); diff --git a/tests/Feature/SimulationFiscalQaPreviewTest.php b/tests/Feature/SimulationFiscalQaPreviewTest.php new file mode 100644 index 00000000..d499ac69 --- /dev/null +++ b/tests/Feature/SimulationFiscalQaPreviewTest.php @@ -0,0 +1,161 @@ +editor = User::factory()->withTwoFactor()->create(); + config(['portal.managers' => [$this->editor->email], 'simulation-tax-governance.editors' => [$this->editor->email], + 'simulation-tax-governance.publishers' => [$this->editor->email]]); + $this->source = InvestmentLegalSource::create([ + 'title' => 'QA fixture: Lei22/2022 republicada; sem activação', + 'url' => 'https://www.inm.gov.mz/', 'diploma' => 'Lei 22/2022', 'article' => 'Artigo 17(1)', + 'competent_entity' => 'Assembleia da República', 'valid_from' => '2023-01-01', 'valid_until' => null, + 'validation_status' => 'validated', 'owner_id' => $this->editor->id, 'version_no' => 1, + 'validator_id' => User::factory()->create()->id, 'validated_at' => now(), + ]); + $this->artisan('fiscal:qa-register-iva', ['actor' => $this->editor->id, 'source' => $this->source->id])->assertSuccessful(); + $this->ruleId = SimulationTaxRuleVersion::where('rule_code', 'IVA-GERAL-BASE-LIQUIDA')->value('id'); + } + + private function preview(array $overrides = [], ?string $key = null) + { + return $this->actingAs($this->editor)->postJson('/api/v2/investment/admin/tax-rules/'.$this->ruleId.'/qa-preview', [ + 'base' => '0.03', 'currency' => 'MZN', 'reference_date' => '2026-10-21', 'operation' => 'internal_general_net', ...$overrides, + ], ['Idempotency-Key' => $key ?? (string) Str::uuid()]); + } + + private function finish(string $id): array + { + (new RunSimulation($id, $this->editor->id, null))->handle(app(SimulationExecution::class)); + + return Simulation::findOrFail($id)->result_snapshot; + } + + public function test_draft_registration_reuses_version_and_never_exposes_a_published_parameter(): void + { + $this->artisan('fiscal:qa-register-iva', ['actor' => $this->editor->id, 'source' => $this->source->id])->assertSuccessful(); + $this->assertSame(1, SimulationTaxRuleVersion::where('rule_code', 'IVA-GERAL-BASE-LIQUIDA')->count()); + $this->assertDatabaseHas('simulation_tax_rule_versions', ['id' => $this->ruleId, 'status' => 'draft']); + $resolved = app(SimulationTaxParameterResolver::class)->resolvePublishedForPeriod('IVA', '2026-10-21', '2026-10-21', + ['operation' => 'internal_general_net', 'currency' => 'MZN']); + $this->assertSame([], $resolved); + $this->actingAs($this->editor)->postJson('/api/v2/investment/admin/tax-rules/'.$this->ruleId.'/publish', ['version_no' => 1, 'evidence' => 'QA attempt must not publish an incomplete fiscal rule.'])->assertUnprocessable(); + } + + public function test_fractional_result_is_exact_and_snapshot_survives_rule_edits_and_new_session(): void + { + $key = (string) Str::uuid(); + $id = $this->preview([], $key)->assertAccepted()->json('data.id'); + $this->preview([], $key)->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('data.id', $id); + $this->preview(['base' => '1'], $key)->assertConflict(); + $snapshot = $this->finish($id); + $this->assertSame('0.0048', $snapshot['result']['value']); + $this->assertSame('calculated', $snapshot['result']['status']); + $this->assertSame('0.0048', $snapshot['presentation']['value']); + $this->assertFalse($snapshot['presentation']['rounding_applied']); + $this->assertEquals(0, BigRational::of($snapshot['intermediate']['result'])->compareTo('0.0048')); + $this->assertSame('draft', $snapshot['fiscal_provenance']['rule']['status']); + $this->assertSame('user_declared', $snapshot['fiscal_provenance']['scope_status']); + $this->assertSame([], $snapshot['result']['missing_inputs']); + $rule = SimulationTaxRuleVersion::findOrFail($this->ruleId); + $params = $rule->parameters; + $params['IVA_GENERAL_RATE']['value'] = '17'; + app(SimulationTaxRuleGovernance::class)->update($this->editor, $this->ruleId, 1, [ + 'rule_code' => $rule->rule_code, 'rule_version' => 1, 'component' => 'IVA', 'scope_key' => $rule->scope_key, + 'valid_from' => '2023-01-01', 'valid_until' => null, 'legal_source_id' => $this->source->id, + 'source_sha256' => $rule->source_sha256, 'rule_definition' => $rule->rule_definition, 'parameters' => $params, + 'rounding_policy' => $rule->rounding_policy, + ]); + auth()->logout(); + $this->actingAs(User::findOrFail($this->editor->id))->getJson('/api/v1/simulations/'.$id)->assertOk() + ->assertJsonPath('data.result_snapshot.fiscal_provenance.rule.parameters.IVA_GENERAL_RATE.value', '16'); + $reportId = DB::table('simulation_reports')->where('simulation_id', $id)->value('id'); + $this->getJson('/api/v1/simulations/'.$id.'/reports/'.$reportId)->assertOk()->assertJsonPath('data.report_snapshot', $snapshot); + $this->getJson('/api/v1/simulations/history')->assertOk()->assertJsonPath('meta.total', 1); + $this->assertDatabaseHas('simulation_result_sources', ['source_type' => 'tax_rule_draft', 'source_id' => $this->ruleId]); + $resultId = DB::table('simulation_results')->where('simulation_id', $id)->value('id'); + $declared = json_decode(DB::table('simulation_result_sources')->where('result_id', $resultId)->where('source_type', 'user_input')->value('source_snapshot'), true); + $this->assertSame('0.03', $declared['base']); + $this->assertArrayNotHasKey('fiscal_preview', $declared); + $other = User::factory()->create(['email_verified_at' => now()]); + $this->actingAs($other)->getJson('/api/v1/simulations/'.$id)->assertNotFound(); + $this->getJson('/api/v1/simulations/'.$id.'/reports/'.$reportId)->assertNotFound(); + $this->getJson('/api/v1/simulations/history')->assertOk()->assertJsonPath('meta.total', 0); + } + + public function test_coverage_and_source_version_mismatch_stay_indeterminate_without_an_intermediate(): void + { + $id = $this->preview(['reference_date' => '2025-12-31'])->assertAccepted()->json('data.id'); + $snapshot = $this->finish($id); + $this->assertContains('demonstrated_coverage', $snapshot['result']['missing_inputs']); + $this->assertNull($snapshot['intermediate']); + $this->source->update(['version_no' => 2]); + $id = $this->preview()->assertAccepted()->json('data.id'); + $snapshot = $this->finish($id); + $this->assertContains('eligible_parameter', $snapshot['result']['missing_inputs']); + $this->assertNull($snapshot['intermediate']); + } + + public function test_zero_is_an_explicit_input_missing_currency_is_rejected_and_non_qa_preview_is_unavailable(): void + { + $id = $this->preview(['base' => '0'])->assertAccepted()->json('data.id'); + $snapshot = $this->finish($id); + $this->assertEquals(0, BigRational::of($snapshot['intermediate']['result'])->compareTo(0)); + $this->assertSame('0', $snapshot['result']['value']); + $largeId = $this->preview(['base' => '9007199254740993.0003'])->assertAccepted()->json('data.id'); + $this->assertSame('1441151880758558.880048', $this->finish($largeId)['result']['value']); + $this->preview(['currency' => null])->assertUnprocessable(); + config(['database.connections.pgsql.url' => 'not-an-isolated-qa-target']); + $this->preview()->assertNotFound(); + } + + public function test_this_consumer_refuses_a_round_node_instead_of_silently_applying_it(): void + { + $rule = SimulationTaxRuleVersion::findOrFail($this->ruleId); + $definition = $rule->rule_definition; + $definition['nodes']['rounded'] = ['op' => 'round', 'args' => ['intermediate'], 'scale' => 2, 'mode' => 'HALF_UP']; + $definition['result'] = 'rounded'; + // Deliberately altered QA draft; no rule is published by this test. + $rule->update(['rule_definition' => $definition]); + $this->preview()->assertUnprocessable()->assertJsonPath('message', 'Este percurso preserva o valor exacto e não aplica arredondamento automático.'); + } + + public function test_qa_review_page_uses_existing_administration_and_environment_guards(): void + { + $path = '/management/investment/tax-rules/'.$this->ruleId.'/qa-review'; + $this->get($path)->assertRedirect('/login'); + $this->actingAs($this->editor)->get($path)->assertOk()->assertInertia(fn ($page) => $page + ->component('management/fiscal-qa-review')->where('rule.id', $this->ruleId)->where('rule.code', 'IVA-GERAL-BASE-LIQUIDA')); + $this->actingAs(User::factory()->withTwoFactor()->create())->get($path)->assertForbidden(); + config(['database.connections.pgsql.url' => 'not-an-isolated-qa-target']); + $this->actingAs($this->editor)->get($path)->assertNotFound(); + } +} diff --git a/tests/Feature/SimulationPublishedFiscalTest.php b/tests/Feature/SimulationPublishedFiscalTest.php new file mode 100644 index 00000000..b7df3fc5 --- /dev/null +++ b/tests/Feature/SimulationPublishedFiscalTest.php @@ -0,0 +1,214 @@ +consumer = User::factory()->create(['email_verified_at' => now()]); + $actors = []; + foreach (['editors', 'reviewers', 'approvers', 'publishers'] as $role) { + $actors[$role] = User::factory()->withTwoFactor()->create(); + config(['simulation-tax-governance.'.$role => [$actors[$role]->email]]); + } + $this->publisher = $actors['publishers']; + $this->source = InvestmentLegalSource::create([ + 'title' => 'Synthetic QA fixture, not a homologation publication', 'url' => 'https://www.inm.gov.mz/', + 'diploma' => 'Lei 22/2022', 'article' => 'Artigo 17(1)', 'competent_entity' => 'Assembleia da República', + 'valid_from' => '2023-01-01', 'valid_until' => null, 'validation_status' => 'validated', + 'owner_id' => $actors['editors']->id, 'version_no' => 1, 'validator_id' => $actors['reviewers']->id, 'validated_at' => now(), + ]); + $data = json_decode(file_get_contents(database_path('qa/iva-general-net-v1.json')), true, flags: JSON_THROW_ON_ERROR); + $data['legal_source_id'] = $this->source->id; + $gov = app(SimulationTaxRuleGovernance::class); + $this->rule = $gov->create($actors['editors'], $data); + foreach (['submit' => 'editors', 'review' => 'reviewers', 'approve' => 'approvers', 'publish' => 'publishers'] as $action => $role) { + $this->rule = $gov->transition($actors[$role], $this->rule->id, $this->rule->version_no, $action, + 'Synthetic isolated QA fixture for consumer integration only.'); + } + config(['simulation-fiscal-consumer.enabled' => true, 'simulation-fiscal-consumer.rule_id' => $this->rule->id, + 'simulation-fiscal-consumer.rule_version' => 1]); + } + + private function input(array $overrides = []): array + { + return ['scenario_kind' => 'fiscal_rule_published', 'base' => '1000.03', 'currency' => 'MZN', + 'reference_date' => '2026-10-21', 'operation' => 'internal_general_net', 'scope_declared' => true, ...$overrides]; + } + + private function create(array $overrides = [], ?string $key = null) + { + return $this->actingAs($this->consumer)->postJson('/api/v1/simulations', $this->input($overrides), + ['Idempotency-Key' => $key ?? (string) Str::uuid()]); + } + + private function finish(string $id): Simulation + { + (new RunSimulation($id, $this->consumer->id, null))->handle(app(SimulationExecution::class)); + + return Simulation::with('reports')->findOrFail($id); + } + + public function test_normal_simulator_admits_published_rule_and_reuses_history_and_reports(): void + { + $key = (string) Str::uuid(); + $this->getJson('/api/v1/fiscal-consumer')->assertOk()->assertJsonPath('data.enabled', true); + $id = $this->create([], $key)->assertAccepted()->json('data.id'); + $this->create([], $key)->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('data.id', $id); + $this->create(['base' => '1001'], $key)->assertConflict(); + $simulation = $this->finish($id); + $snapshot = $simulation->result_snapshot; + $this->assertSame('fiscal_rule_published', $snapshot['scenario_kind']); + $this->assertSame('calculated', $snapshot['result']['status']); + $this->assertSame($this->rule->id, $snapshot['fiscal_provenance']['rule']['id']); + $this->assertSame('published', $snapshot['fiscal_provenance']['rule']['status']); + $this->assertArrayNotHasKey('review_evidence', $snapshot['fiscal_provenance']['rule']); + $this->assertArrayNotHasKey('author_id', $snapshot['fiscal_provenance']['rule']); + $this->getJson('/api/v1/simulations/history')->assertOk()->assertJsonPath('meta.total', 1); + $this->getJson('/api/v1/simulations/'.$id)->assertOk()->assertJsonPath('data.result_snapshot', $snapshot); + $report = $simulation->reports->first(); + $this->getJson('/api/v1/simulations/'.$id.'/reports/'.$report->id)->assertOk()->assertJsonPath('data.report_snapshot', $snapshot); + $this->assertSame($report->report_sha256, DB::table('simulation_results')->where('simulation_id', $id)->value('result_sha256')); + $this->assertSame(1, Simulation::where('owner_user_id', $this->consumer->id)->count()); + $this->assertDatabaseHas('simulation_result_sources', ['source_id' => $this->rule->id, 'source_type' => 'tax_rule_published']); + $this->actingAs(User::factory()->create(['email_verified_at' => now()]))->getJson('/api/v1/simulations/'.$id)->assertNotFound(); + $this->getJson('/api/v1/simulations/'.$id.'/reports/'.$report->id)->assertNotFound(); + } + + public function test_missing_scope_or_changed_source_is_indeterminate_not_zero(): void + { + $id = $this->create(['scope_declared' => false])->assertAccepted()->json('data.id'); + $snapshot = $this->finish($id)->result_snapshot; + $this->assertSame('indeterminate', $snapshot['result']['status']); + $this->assertNull($snapshot['result']['value']); + $this->assertContains('operation_scope', $snapshot['result']['missing_inputs']); + $this->assertSame('undetermined', $snapshot['fiscal_provenance']['scope_status']); + $this->source->update(['version_no' => 2]); + $id = $this->create()->assertAccepted()->json('data.id'); + $snapshot = $this->finish($id)->result_snapshot; + $this->assertSame('indeterminate', $snapshot['result']['status']); + $this->assertNull($snapshot['result']['value']); + $this->assertContains('eligible_parameter', $snapshot['result']['missing_inputs']); + } + + public function test_suspension_refuses_new_admissions_but_frozen_queue_and_replays_survive(): void + { + $key = (string) Str::uuid(); + $id = $this->create([], $key)->assertAccepted()->json('data.id'); + $this->assertDatabaseHas('simulations', ['id' => $id, 'status' => 'queued']); + app(SimulationTaxRuleGovernance::class)->transition($this->publisher, $this->rule->id, $this->rule->version_no, + 'suspend', 'QA: suspend after admission and before worker execution.'); + $this->getJson('/api/v1/fiscal-consumer')->assertOk()->assertJsonPath('data.enabled', false); + $this->create()->assertStatus(503); + $this->create([], $key)->assertOk()->assertJsonPath('data.id', $id)->assertJsonPath('replayed', true); + config(['simulation-fiscal-consumer.enabled' => false]); + $this->create()->assertStatus(503); + $simulation = $this->finish($id); + $this->assertSame('completed', $simulation->status); + $this->assertSame('published', $simulation->result_snapshot['fiscal_provenance']['rule']['status']); + $this->assertSame($this->rule->id, $simulation->result_snapshot['fiscal_provenance']['rule']['id']); + $this->assertSame('suspended', $this->rule->fresh()->status); + $this->create([], $key)->assertOk()->assertJsonPath('data.status', 'completed'); + $this->getJson('/api/v1/simulations/'.$id.'/reports/'.$simulation->reports->first()->id)->assertOk() + ->assertJsonPath('data.report_snapshot', $simulation->result_snapshot); + $this->getJson('/api/v1/simulations/history')->assertOk()->assertJsonPath('meta.total', 1); + } + + public function test_server_selection_cannot_be_overridden_and_coverage_is_bounded(): void + { + $this->create(['rate' => '0'])->assertUnprocessable(); + $this->create(['rule_id' => (string) Str::uuid()])->assertUnprocessable(); + $this->create(['reference_date' => '2027-01-01'])->assertUnprocessable(); + $this->create(['currency' => 'USD'])->assertUnprocessable(); + config(['simulation-fiscal-consumer.rule_version' => 2]); + $this->create()->assertStatus(503); + config(['simulation-fiscal-consumer.rule_id' => 'invalid-configuration']); + $this->getJson('/api/v1/fiscal-consumer')->assertOk()->assertJsonPath('data.enabled', false); + $this->assertSame(0, Simulation::where('owner_user_id', $this->consumer->id)->count()); + } + + public function test_anonymous_cookie_token_ownership_is_reused(): void + { + $key = (string) Str::uuid(); + $response = $this->postJson('/api/v1/simulations', $this->input(), ['Idempotency-Key' => $key]); + $id = $response->assertAccepted()->json('data.id'); + $token = $response->json('access_token'); + $this->getJson('/api/v1/simulations/'.$id, ['X-Simulation-Token' => $token])->assertOk(); + $this->getJson('/api/v1/simulations/'.$id, ['X-Simulation-Token' => str_repeat('x', 40)])->assertNotFound(); + $this->getJson('/api/v1/simulations/history')->assertUnauthorized(); + } + + public function test_disabled_consumer_does_not_disable_custom_planning(): void + { + config(['simulation-fiscal-consumer.enabled' => false]); + $this->create()->assertStatus(503); + $this->actingAs($this->consumer)->postJson('/api/v1/simulations', ['base' => '100', 'rate' => '5', 'reduction' => '0'], + ['Idempotency-Key' => (string) Str::uuid()])->assertAccepted()->assertJsonPath('data.input_snapshot.scenario_kind', 'custom_planning'); + } + + public function test_public_consumer_works_under_existing_rls_without_governance_context(): void + { + // Role and grants roll back with the isolated test transaction. No application permission is added. + DB::statement('CREATE ROLE qa_fiscal_consumer_rls_20261021 NOLOGIN NOSUPERUSER NOBYPASSRLS'); + DB::statement('GRANT USAGE ON SCHEMA public TO qa_fiscal_consumer_rls_20261021'); + DB::statement('GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO qa_fiscal_consumer_rls_20261021'); + DB::statement('GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO qa_fiscal_consumer_rls_20261021'); + DB::statement("SELECT set_config('app.simulation_tax_governance', 'off', true)"); + DB::statement('SET LOCAL ROLE qa_fiscal_consumer_rls_20261021'); + try { + $this->assertSame(1, SimulationTaxRuleVersion::whereKey($this->rule->id)->count()); + $id = $this->create()->assertAccepted()->json('data.id'); + $this->assertNotEmpty($id); + $this->assertSame(0, DB::table('simulation_tax_rule_events')->where('rule_version_id', $this->rule->id)->count()); + // An UPDATE scope is not granted by the public SELECT policy. + $this->assertSame(0, SimulationTaxRuleVersion::whereKey($this->rule->id)->update(['status' => 'suspended'])); + } finally { + DB::statement('RESET ROLE'); + } + } + + public function test_admission_holds_shared_locks_until_transaction_end(): void + { + $this->create()->assertAccepted(); + config(['database.connections.fiscal_lock_probe' => config('database.connections.pgsql')]); + $probe = DB::connection('fiscal_lock_probe'); + $probe->beginTransaction(); + try { + // A concurrent suspension/source transition cannot cross an admitted transaction. + $rule = $probe->selectOne('select pg_try_advisory_xact_lock(261021, hashtext(?)) as acquired', [$this->rule->id]); + $source = $probe->selectOne('select pg_try_advisory_xact_lock(261022, hashtext(?)) as acquired', [$this->source->id]); + $this->assertFalse($rule->acquired); + $this->assertFalse($source->acquired); + } finally { + $probe->rollBack(); + DB::purge('fiscal_lock_probe'); + } + } +} diff --git a/tests/frontend/fiscal-qa-preview.test.mjs b/tests/frontend/fiscal-qa-preview.test.mjs new file mode 100644 index 00000000..364a9dd7 --- /dev/null +++ b/tests/frontend/fiscal-qa-preview.test.mjs @@ -0,0 +1,176 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { fiscalQaRows } from "../../resources/js/lib/fiscal-qa-preview.ts"; +import { exportCompletedArithmeticCsv, readArithmeticSnapshot } from "../../resources/js/lib/simulation-form.ts"; +import { createProjectDossierReport } from "../../resources/js/lib/project-dossier-report.ts"; + +function fixture() { + const snapshot = { + contract_version: 1, + scenario_kind: "fiscal_rule_qa_preview", + base: "0.03", + currency: "MZN", + indicative_total: "0.0048", + legal_interpretation: false, + formula_version: "investmoz.tax.v1", + context: { + reference_date: "2026-10-21", + currency: "MZN", + currency_fraction_digits: 2, + scenario_id: "61000100-0000-4000-8000-000000000099", + scenario_version: 1, + engine_version: "fiscal-draft-preview-v1", + ruleset_version: "IVA-GERAL-BASE-LIQUIDA:1", + }, + result: { + status: "calculated", + value: "0.0048", + unit: "MZN", + explanation: + "Cálculo exacto QA; enquadramento declarado; não é saldo a pagar.", + missing_inputs: [], + source_ids: ["source-fixture"], + }, + intermediate: { result: "48/10000" }, + fiscal_provenance: { + rule: { + rule_code: "IVA-GERAL-BASE-LIQUIDA", + rule_version: 1, + valid_from: "2023-01-01", + valid_until: null, + source_sha256: "a".repeat(64), + source: { diploma: "Lei 22/2022", article: "Artigo 17(1)" }, + }, + }, + }; + return { + id: "61000100-0000-4000-8000-000000000088", + status: "completed", + version_no: 2, + result_snapshot: snapshot, + reports: [ + { + id: "61000100-0000-4000-8000-000000000077", + version_no: 1, + status: "ready", + report_snapshot: structuredClone(snapshot), + report_sha256: "b".repeat(64), + }, + ], + }; +} + +test("screen rows preserve sub-cent IVA as a string and distinguish calculation from payment", () => { + const simulation = fixture(); + const before = JSON.stringify(simulation); + const rows = fiscalQaRows(simulation.result_snapshot); + assert.equal( + rows.find(([label]) => label.startsWith("IVA calculado"))[1], + "0.0048", + ); + assert.match( + rows.find(([label]) => label === "Arredondamento")[1], + /Não aplicado/, + ); + assert.match( + rows.find(([label]) => label === "Enquadramento")[1], + /não validado juridicamente/, + ); + assert.equal(JSON.stringify(simulation), before); +}); + +test("CSV preserves the exact amount and refuses a mismatched immutable report", () => { + const simulation = fixture(); + const csv = exportCompletedArithmeticCsv( + simulation, + "2026-10-21T09:00:00Z", + ); + assert.match(csv, /0\.0048/); + assert.doesNotMatch(csv, /Resultado fiscal final,Indeterminado/); + simulation.reports[0].report_snapshot.result.value = "0.00"; + assert.throws( + () => exportCompletedArithmeticCsv(simulation), + /relatório preservado/, + ); +}); + +test("dossier report uses the same exact snapshot and preserves genuine indetermination", () => { + const simulation = fixture(); + function report() { + return createProjectDossierReport( + { project_title: "QA fiscal", currency: "MZN" }, + { + selectedSections: new Set(["finance"]), + currentCalculation: { + status: "completed", + version: 2, + snapshot: simulation.result_snapshot, + }, + }, + ); + } + const rows = report().sections.find( + (section) => section.id === "finance", + ).rows; + assert.equal( + rows.find((row) => row.field.startsWith("IVA calculado")).value, + "0.0048", + ); + assert.equal( + rows.find((row) => row.field === "Fonte da taxa").value, + "Lei 22/2022 — Artigo 17(1)", + ); + simulation.result_snapshot.result.status = "indeterminate"; + simulation.result_snapshot.result.value = null; + simulation.result_snapshot.indicative_total = null; + simulation.result_snapshot.result.missing_inputs = ["eligible_parameter"]; + assert.equal( + report() + .sections.find((section) => section.id === "finance") + .rows.find((row) => row.field.startsWith("IVA calculado")).value, + "Indeterminado", + ); +}); + +test("published consumer routes the frozen snapshot through screen CSV and dossier without QA labels", () => { + const simulation = fixture(); + const snapshot = simulation.result_snapshot; + snapshot.scenario_kind = "fiscal_rule_published"; + snapshot.context.engine_version = "fiscal-published-v1"; + snapshot.presentation = { value: snapshot.result.value, rounding_applied: false }; + snapshot.result.explanation = "IVA pela versão publicada; enquadramento declarado."; + snapshot.fiscal_provenance.rule.status = "published"; + snapshot.fiscal_provenance.scope_status = "user_declared"; + snapshot.fiscal_provenance.mode = "published_version"; + simulation.reports[0].report_snapshot = structuredClone(snapshot); + const before = JSON.stringify(simulation); + const rows = fiscalQaRows(snapshot); + assert.equal(readArithmeticSnapshot(snapshot).integrity, "versioned"); + assert.match(rows[0][1], /publicada/); + assert.doesNotMatch(JSON.stringify(rows), /rascunho|não publicada|QA/); + const csv = exportCompletedArithmeticCsv(simulation, "2026-10-21T12:00:00Z"); + for (const [label, value] of rows) { + assert.ok(csv.includes(label)); + assert.ok(csv.includes(value.replaceAll('"', '""'))); + } + const report = createProjectDossierReport({ project_title: "Published IVA QA", currency: "MZN" }, { + selectedSections: new Set(["finance"]), currentCalculation: { status: "completed", version: 2, snapshot }, + }); + const finance = report.sections.find((section) => section.id === "finance"); + for (const [field, value] of rows) assert.ok(finance.rows.some((row) => row.field === field && row.value === value)); + assert.doesNotMatch(JSON.stringify(finance), /rascunho|proveniência QA/); + assert.equal(JSON.stringify(simulation), before); +}); + +test("published consumer indeterminate snapshot is valid and a mismatched presentation is refused", () => { + const snapshot = fixture().result_snapshot; + snapshot.scenario_kind = "fiscal_rule_published"; + snapshot.result.status = "indeterminate"; + snapshot.result.value = null; + snapshot.result.missing_inputs = ["operation_scope"]; + snapshot.indicative_total = null; + snapshot.presentation = { value: null, rounding_applied: false }; + assert.equal(readArithmeticSnapshot(snapshot).integrity, "versioned"); + snapshot.presentation.value = "0"; + assert.equal(readArithmeticSnapshot(snapshot).integrity, "invalid"); +});