From 83d2f97d820492af811ae5765a13149368a7e0c5 Mon Sep 17 00:00:00 2001 From: nala Date: Wed, 21 Oct 2026 09:22:11 +0200 Subject: [PATCH] feat: preparar lote IVA exacto em QA sem activacao --- .../Commands/RegisterFiscalQaDraft.php | 50 ++++++ .../SimulationTaxRuleGovernanceController.php | 20 +++ app/Jobs/RunSimulation.php | 67 ++++--- app/Services/SimulationExecution.php | 31 +++- app/Services/SimulationFiscalQaPreview.php | 81 +++++++++ .../SimulationTaxParameterResolver.php | 19 +- database/qa/iva-general-net-v1.json | 38 ++++ .../ficha-IVA-GERAL-BASE-LIQUIDA-v1.md | 168 +++++++----------- ...-review-continuation-20261020-receipt.json | 42 +++++ .../unification/traceability-and-manual.md | 20 +++ .../v2/auditoria-portal/matriz-requisitos.csv | 2 +- resources/js/components/fiscal-qa-preview.tsx | 24 +++ resources/js/components/simulation-form.tsx | 12 +- resources/js/lib/fiscal-qa-preview.ts | 59 ++++++ resources/js/lib/project-dossier-report.ts | 12 +- resources/js/lib/simulation-form.ts | 8 + routes/investment-zones.php | 1 + .../Feature/SimulationFiscalQaPreviewTest.php | 150 ++++++++++++++++ tests/frontend/fiscal-qa-preview.test.mjs | 133 ++++++++++++++ 19 files changed, 799 insertions(+), 138 deletions(-) create mode 100644 app/Console/Commands/RegisterFiscalQaDraft.php create mode 100644 app/Services/SimulationFiscalQaPreview.php create mode 100644 database/qa/iva-general-net-v1.json create mode 100644 resources/js/components/fiscal-qa-preview.tsx create mode 100644 resources/js/lib/fiscal-qa-preview.ts create mode 100644 tests/Feature/SimulationFiscalQaPreviewTest.php create mode 100644 tests/frontend/fiscal-qa-preview.test.mjs 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/SimulationTaxRuleGovernanceController.php b/app/Http/Controllers/SimulationTaxRuleGovernanceController.php index 888115d0..25aed706 100644 --- a/app/Http/Controllers/SimulationTaxRuleGovernanceController.php +++ b/app/Http/Controllers/SimulationTaxRuleGovernanceController.php @@ -2,9 +2,11 @@ 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; @@ -13,6 +15,24 @@ use InvalidArgumentException; class SimulationTaxRuleGovernanceController extends Controller { + 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/Jobs/RunSimulation.php b/app/Jobs/RunSimulation.php index 4fea21e4..d3f5dc6e 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,37 @@ 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 (($input['scenario_kind'] ?? null) === 'fiscal_rule_qa_preview') { + abort_unless(IsolatedQaDatabase::allowsSchemaChanges(), 404); + $result = $input['fiscal_preview']; + $engineVersion = 'fiscal-draft-preview-v1'; + $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 +105,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' => 'user_declared', + ] : $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' => '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..8d30a3e9 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,11 +18,27 @@ 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); + } + + private function createPayload(array $payload, ?int $actorId, ?string $organizationId, ?string $token, string $key): 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)); @@ -44,7 +61,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' => $payload['scenario_kind'] === 'fiscal_rule_qa_preview' + ? ['engine' => 'fiscal-draft-preview-v1', 'mode' => 'qa_draft_only', '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 +104,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(($simulation->input_snapshot['scenario_kind'] ?? null) === 'fiscal_rule_qa_preview', 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..dc9bcdbc --- /dev/null +++ b/app/Services/SimulationFiscalQaPreview.php @@ -0,0 +1,81 @@ +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']]); + } + } + $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' => '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' => 'fiscal-draft-preview-v1', + 'ruleset_version' => $rule->rule_code.':'.$rule->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) $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' => 'qa_draft_only', 'scope_status' => 'user_declared', 'context' => $context, + 'rounding_policy' => $rule->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.', + ], + ]; + + return app(SimulationExecution::class)->createFiscalQaPreview($snapshot, $actor->id, $key); + }); + } +} 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/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 index 6b9c36e5..fd106d09 100644 --- a/docs/investmoz/simulations/ficha-IVA-GERAL-BASE-LIQUIDA-v1.md +++ b/docs/investmoz/simulations/ficha-IVA-GERAL-BASE-LIQUIDA-v1.md @@ -1,122 +1,86 @@ -# IVA-GERAL-BASE-LIQUIDA — ficha documental v1.1 +# IVA-GERAL-BASE-LIQUIDA — ficha documental v1.2 -Data: 21/10/2026. Referência: AUD-SIM-INT02 na matriz existente. Origem: proposta `0fafd348`; preparação solicitada pela Tempo. Estado: **rascunho documental para revisão; não cadastrado, não publicado, não activo**. Esta versão identifica fundamento e lacunas; não certifica uma regra executável completa. +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. -## Âmbito e período +**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. -Percurso proposto: base tributável líquida identificada × taxa geral / 100, para operação interna abrangida pela taxa geral de IVA, em MZN explicitamente indicado. Cobertura de ensaio proposta: 01/01/2026–31/12/2026. É uma delimitação técnica do lote, não uma data de caducidade da lei nem uma restrição organizacional. +## Âmbito e contrato -Não determina automaticamente incidência, qualidade do sujeito passivo, localização, isenção ou regime especial. A base fiscal não pode ser deduzida de uma rubrica de orçamento. Âmbito apenas declarado pelo utilizador deve aparecer como declarado; ausência ou incompatibilidade produz indeterminação. A fundamentação da taxa não prova a sujeição de uma operação concreta. +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: imposto já incluído no preço, taxas reduzidas, isenções/listas, importações, digital/não residentes, deduções, crédito/reembolso, incentivos territoriais e outros impostos. Permanecem no inventário existente, sem declaração de cobertura deste lote. +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. -## Fontes e aplicação - -As fontes oficiais são aceites pela decisão da Tempo de 08/10/2026. Os vínculos abaixo são rastreabilidade e aplicação das fontes; não se exige nova certificação de proveniência. Reutilizar [análise primária](primary-text-review-2025-2026.md), [registo de fontes](official-source-register.md) e [análise da reforma](fiscal-reform-2026-review.md). Exigências históricas genéricas de autenticação adicional ou de um novo papel organizacional não prevalecem sobre essa decisão. - -| Elemento | Fundamento / exemplar | Consequência nesta ficha | -|---|---|---| -| Taxa geral 16% | [Lei22/2022 republicada](source-archive/Lei-22-2022-IVA-republicada-BR-250-2-suplemento.pdf), art.17(1), página3 do PDF, BR2252-(27). SHA256 `EA1B0DC6F5CF6E595F6D9A19E7042F398999E0087CAEF1785F6E8FAC571B5F53`. | Valor nominal fundamentado; ainda não é parâmetro executável cadastrado. | -| Eficácia desde 01/01/2023 | Mesmo exemplar, art.6 da Lei22/2022, página4 do PDF. | Preservar início legal; não substituir pelo início da cobertura de ensaio de2026. | -| Alterações para2026 | [BR248, suplemento1/2025](source-archive/BR-248-I-Suplemento-1-2025.pdf), Lei10/2025, art.1 na página12 e art.5 na página15. SHA256 `CCF2A8DEECA820F2DD076B4B2141B1EFE3F6A806568C3A587CA59A8E92F4C251`. | Eficácia em01/01/2026; lista de artigos alterados não inclui17. Não equivale a consolidação completa de incidência/regimes. | -| Preço com imposto incluído | Mesmo BR248, art.34 do CIVA, página14: divisão por116 ×100 e arredondamento do resultado para a unidade mais próxima. | Regula extracção da base a partir do preço com imposto. Fora deste percurso. Não fundamenta arredondamento do imposto calculado sobre base líquida; não inferir tratamento de empates. | -| Rectificação de2017 | [BR89/2017](source-archive/BR-89-I-Serie-2017-06-08.pdf), aviso relativo à Lei13/2016; análise existente identifica art.6(5)c/d,14(2)c e anexos de isenção. | Preservar cadeia documental. A taxa geral não dispensa determinar enquadramento da operação. Listas de isenção não foram consolidadas por esta ficha. | - -Foi recuperado do inventário existente o exemplar `Lei 13_2016_Reimpressao Codigo do IVA.pdf`, em `\\Mac\Home\Documents\TEMPO\Portal Tempo\Legislacao\`, 32páginas, 2.127.554bytes, SHA256 `B90DC7C14ECEA2E378C4A62E8E5A47D54A5644C457BAEB8B8053B92FD2A5710B`. É digitalizado, sem camada textual nas32páginas. A identificação e integridade foram registadas; não se afirma leitura integral nem fundamento de âmbito/arredondamento a partir deste exemplar nesta versão. Não o confundir com outra cópia de15páginas mencionada em análises anteriores. O cotejo visual dos dispositivos relevantes e das alterações/regulamentação aplicáveis continua trabalho documental concreto, sem revalidar a credibilidade oficial nem repetir todo o inventário. - -## Parâmetros, lacunas e contrato de resultado - -| Campo | Situação | +| Campo | Estado actual | |---|---| -| Identificador / versão | `IVA-GERAL-BASE-LIQUIDA` / documental-v1.1; identificador provisório, sem UUID de base. | -| Taxa / unidade | 16 / percent, fundamento acima. | -| Base / moeda / data | Entradas obrigatórias, com origem e contexto; MZN explícito, sem conversão implícita. Não substituir ausência por zero. | -| Enquadramento | Identificação de operação interna à taxa geral e estado declarado/comprovado. Falta fechar correspondência normativa das condições do percurso. | -| Arredondamento | **Indeterminado nesta versão:** precisão, modo, empates e incidência por linha/documento/apuramento não fundamentados para base líquida. Não adoptar HALF_UP ou duas casas por conveniência técnica. | -| Expressão conceptual | `base_tributavel * taxa_percentual / 100`; não constitui definição DSL publicada. Conservar exactidão intermédia; sem total fiscal final enquanto faltar política aplicável. | -| Cobertura | Ensaio2026; vigência legal separada e sem expiração artificial. Fora da cobertura demonstrada é mensagem de cobertura, não revogação da lei. | -| Proveniência | Resultado deverá fixar regra/versão, fonte/hash/artigo, data/período, entradas/unidades, contexto e seu estado, passos, arredondamento e lacunas. | +| 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. | -Casos documentais iniciais, sem pretender validar incidência: base100MZN e taxa16% dão intermédio exacto16MZN; base0,01MZN dá intermédio0,0016MZN, cujo valor fiscal final fica indeterminado sem política. São referências aritméticas, não provas de obrigação fiscal. Acrescentar casos independentes de fronteira e empates depois de fundamentar a política. +## Fontes reutilizadas e disposições documentadas -## Integração e estimativa separadas +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. -Reutilizar `SimulationTaxRuleGovernance`, `SimulationTaxParameterResolver` e `SimulationTaxRuleExpression`. Não reconstruir governação, resolvedor temporal ou avaliador. O fluxo actual de `RunSimulation` é planeamento personalizado; não o renomear como resultado fiscal verificado. +| 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. | -| Frente | Trabalho restante do lote | Estimativa proposta | +| 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 | |---|---|---| -| Aplicação documental e cadastro | Fechar condições e arredondamento com citação; completar versão e casos; transcrever parâmetros/vínculos como rascunho QA quando autorizado. Cadastro não exige criar um novo motor. | 4–8h | -| Código e aceitação técnica | Orquestrar contexto/período → resolvedor → avaliador; resultado e proveniência imutáveis; ligar interface, dossier, histórico e relatórios; ensaios dirigidos. Confirmar necessidade real de alteração de schema antes de a propor. | 12–20h | +|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. | -Total original:16–28h, estimativa condicional, não medição de horas já consumidas nem novo compromisso de prazo. Espera por elemento documental concreto, revisão e activação não incluídas. Esta ficha não reduz automaticamente a estimativa; recalibrar após fechar as duas lacunas identificadas. As antigas1–2h eram exclusivamente do recorte de exportações já concluído. +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. -## Aceitação antes de publicação +## Implementação, limites e integração -1. Condições, período e política de arredondamento ligados aos dispositivos aplicáveis; lacunas resolvidas sem valores inventados. -2. No QA isolado, resolução inequívoca por contexto/data, recusa explícita de ausência, ambiguidade e cobertura incompatível; preservar mecanismos temporais existentes. -3. Casos independentes exactos, fraccionários e de fronteira da política; distinguir zero de ausência e exigir moeda/unidades. -4. Snapshot reprodutível com fontes e versões; nova sessão preserva resultado histórico; verificar apenas o novo consumidor quanto ao isolamento. -5. Ecrã e relatório distinguem estimativas do utilizador, contexto declarado e cálculo com regra verificada. Conferir apenas novas linhas/deltas; reutilizar provas aceites de valores, CSV, exportações e paginação. -6. Revisão e publicação pelos mecanismos/papéis existentes da Tempo, separadas do cadastro e da publicação documental. Nenhuma activação nesta preparaçã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`. -A entrega `13f68f1b` — melhorias de planeamento financeiro, recuperação, exportação e idioma — foi integrada após autorização da Tempo e não depende destas lacunas fiscais. `0fafd348` e esta ficha permanecem na branch fiscal, separados de `main`. +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. -## Adenda v1.1 — leitura dirigida de enquadramento e arredondamento +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. -21/10/2026. Esta adenda substitui o estado de «não consultado» dos dispositivos abaixo, mantendo o histórico da v1 nas secções anteriores. Não atribui ao utilizador conferência local nem validação jurídica da ficha. Não altera a base de dados, o código, a governação ou o estado de publicação. +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. -### Exemplares efectivamente consultados +## Estimativa e revisão -- [CIVA republicado pela Lei13/2016, BR156, 10.º suplemento, 30/12/2016](source-archive/Lei-13-2016-CIVA-BR-156-10-suplemento.pdf): cópia byte a byte do exemplar já inventariado acima, mesmo hash `B90DC7C14ECEA2E378C4A62E8E5A47D54A5644C457BAEB8B8053B92FD2A5710B`. Leitura visual dirigida das páginas6–8,13–14,17–18; folhas de contacto para localizar dispositivos. Não se afirma análise integral dos anexos. -- [BR16, I série, 16/04/2008, Decreto7/2008 e regulamento anexo](source-archive/BR-16-I-Serie-2008-04-16.pdf): recuperado do inventário existente, 28páginas, 2.267.865bytes, SHA256 `FC373FF5637EDF75EC6A9595DEC8763A8503D4EE786E3B7D8F1C742EAE9D80EE`. Texto pesquisável; página2 conferida visualmente para distinguir pagamento de liquidação. O BR contém também outros impostos: resultados de pesquisa fora do regulamento IVA não são fundamento deste lote. -- Republicação Lei22/2022 e Lei10/2025/BR248: reutilizados os exemplares acima; cotejo textual dos arts.15,17 e27, além da cadeia já analisada. Os sinais «[…]» preservam texto não alterado; não representam ausência de norma. - -O exemplar de06/04/2017 localizado sob BR54 no inventário contém matéria aduaneira e não forneceu o Decreto8/2017 procurado. A coincidência de data não permite afirmar leitura dessa alteração do regulamento. Não foi localizado pelo nome/data, no inventário disponível, o texto do Decreto31/2012. Estes limites são de disponibilidade/correspondência nesta consulta, não afirmações de inexistência dos diplomas. - -### Enquadramento documental confirmado e fronteira da conclusão - -| Disposição exacta | Conteúdo confirmado por leitura | Aplicação ao percurso proposto | -|---|---|---| -| CIVA art.1(1)(a), PDF6, BR1426-(264) | Incidência sobre transmissões de bens e prestações de serviços onerosas no território nacional, nos termos do art.6, por sujeitos passivos nessa qualidade. | Não basta um montante: exigir natureza, onerosidade, localização e qualidade do sujeito. Importações do art.1(1)(b) ficam fora. | -| CIVA art.2(1)(a), PDF6 | Pessoas residentes ou com estabelecimento/representação e exercício independente habitual de actividades enumeradas. | Contexto do fornecedor tem de ser identificado. Não automatizar elegibilidade fiscal a partir do perfil de investidor. | -| CIVA arts.3(1),4(1), PDF6–7 | Transmissão geral: transferência onerosa de bens corpóreos; serviço: operação onerosa que não seja transmissão ou importação. | Classificar a operação; casos assimilados e especiais exigem enquadramento próprio e não entram implicitamente no lote. | -| CIVA art.6(1), PDF7, BR1426-(265) | Bens situados em território nacional no início do transporte/expedição ou, sem estes, quando postos à disposição. | Condição documental para transmissão interna ordinária. Serviços têm art.6(3) e excepções nos números seguintes; não declarar que a localização do prestador resolve todos os serviços. | -| CIVA art.15(1), PDF13, BR1426-(271) | Base geral é a contraprestação obtida ou a obter do adquirente, destinatário ou terceiro, sem prejuízo dos casos do n.º2. | Fundamenta base líquida tributável identificada, não qualquer receita prevista. Excluir do percurso automático as bases especiais do n.º2. | -| CIVA art.15(5)(a)/(b), PDF14, BR1426-(272) | Inclui outros impostos/direitos/taxas, excepto o próprio IVA, e despesas acessórias debitadas, incluindo comissões, embalagem, transporte e seguros por conta do cliente. | Base sem IVA pode incluir mais que o preço nominal. Entrada deve indicar composição. | -| CIVA art.15(6)(a)–(d), PDF14 | Exclui as indemnizações judiciais indicadas, descontos/abatimentos/bónus concedidos, pagamentos em nome/conta de terceiros registados adequadamente e embalagens retornáveis nas condições aí previstas. | Não excluir rubricas apenas pelo nome; conservar condições e evidência. O lote recebe base já determinada, sem construir um classificador destes casos. | -| CIVA art.17(2), PDF14 | Taxa aplicável é a vigente quando o imposto se torna exigível. | A data relevante não é automaticamente a criação da simulação. Ligar o resolvedor à data de exigibilidade identificada. | -| CIVA art.7(1)(a)/(b), PDF8, BR1426-(266) | Bens: colocação à disposição; serviços: realização ou cobrança/débito anterior nos termos do preceito. Números seguintes contêm situações próprias. | Não usar ano civil como substituto da data fiscal. Adiantamentos, prestações continuadas e outras excepções não são resolvidos por esta fórmula. | -| CIVA art.27(5)(c)/(d), PDF17, BR1426-(275) | Distingue preço líquido e restantes elementos da base da taxa e montante do imposto. Lei10/2025 mantém essas alíneas por «[…]», alterando o n.º10. | Sustenta separação entre base e IVA no resultado. Não fixa nestas alíneas modo/escala de arredondamento. | -| Regulamento, Decreto7/2008, arts.4 e5, PDF1–2 | Remete a matéria colectável para arts.15/16 do Código e o apuramento devido para arts.18–22. | IVA incidente da operação não equivale ao saldo a pagar após deduções. O lote não calcula declaração ou pagamento final. | - -Período: Lei13/2016, art.4, PDF5, determina entrada em vigor em01/01/2017; taxa histórica de17% nesse exemplar não é usada para2026. A Lei22/2022 altera art.17(1) para16% desde01/01/2023 e adiciona art.15(10) para taxa reduzida, preservando os números gerais acima. A Lei10/2025, efectiva em01/01/2026, não lista art.15 ou17 entre os alterados. Esta cadeia suporta o percurso geral condicionado descrito; não certifica que todas as isenções, regimes e alterações regulamentares de2026 estejam consolidados. O intervalo de ensaio continua2026 e não cria expiração legal. - -**Saldo do enquadramento:** fundamento geral da base e requisitos nucleares identificados. A elegibilidade de cada operação e as excepções não estão demonstradas por este facto. Para este lote manter entrada de base já apurada e contexto explícito, com sujeição geral comprovada ou declarada devidamente rotulada. Sem essa identificação, devolver indeterminação; não anunciar um classificador fiscal completo. - -### Arredondamento: disposições encontradas que não podem ser generalizadas - -| Disposição | Âmbito exacto | Conclusão | -|---|---|---| -| CIVA art.34, Lei10/2025, BR248/PDF14 | Extracção da base quando factura/registo já inclui imposto: dividir por116, multiplicar por100, arredondar para unidade mais próxima. | Não é arredondamento do IVA obtido de base líquida. Não transpor nem inferir empates. | -| Regulamento art.9(1)(b), Decreto7/2008, BR16/PDF2, p.100 | Valor do meio de pagamento quando usado cheque ou vale de correio: arredondamento por excesso para meticais. | Texto confirmado nesse exemplar histórico. Não é regra de cálculo por linha/factura nem prova de política geral para pagamento electrónico; vigência consolidada específica não fechada. Não aplicar ao imposto incidente do lote. | - -Não foi encontrado, nos dispositivos efectivamente consultados, fundamento suficiente para fixar escala, modo, tratamento de empates e agregação do IVA calculado sobre base líquida. Isto **não prova inexistência** de instrução aplicável. Não adoptar convenções de software, ofícios portugueses ou páginas de calculadoras como norma moçambicana. - -A [comunicação oficial do Governo de30/09/2026](https://portaldogoverno.gov.mz/governo-introduz-alteracoes-nos-regulamentos-fiscais-para-reforcar-eficiencia-tributaria/) anuncia alteração dos arts.3,7,8,15,16-A,18,21,23,28,36 e37 do regulamento, referindo Decreto7/2008 e alterações31/2012 e8/2017. A comunicação não fornece o articulado publicado nem a eficácia de cada disposição. Serve para identificar a dependência concreta de consolidação; não substitui o decreto publicado nem permite presumir que afecte ou resolva o arredondamento do lote. - -**Questão concreta para esclarecimento documental:** qual o diploma, despacho, instrução/circular da AT ou disposição aplicável em2026 que determina, para IVA incidente sobre base líquida em factura, (a) número de casas/unidade, (b) modo e empates, (c) arredondamento por linha, por taxa agrupada ou por documento e (d) relação com os totais declarados? Obter o dispositivo e período, incluindo alterações pertinentes do regulamento e instruções de facturação. Não é um pedido para escolher uma convenção técnica nem para reconfirmar autenticidade oficial. Nenhuma mensagem foi enviada a terceiros. - -### Exemplos documentais condicionados - -Pressupostos explícitos para os exemplos: operação interna onerosa à taxa geral, sujeito no regime aplicável, sem isenção/redução/base especial, exigibilidade em2026 e valores MZN. São exemplos aritméticos de aplicação dos preceitos, não operações reais classificadas ou casos finais de aceitação. - -| Exemplo | Base nos termos do art.15 | IVA exacto a16% | Estado | -|---|---|---|---| -| Preço100 + transporte debitado10 − desconto concedido5; sem outras rubricas | 105, se verificadas as condições dos n.os5 e6 | 16,8 | Intermédio exacto; não chamar saldo a pagar. | -| Base já apurada100 | 100 | 16 | Exacto; não resolve política geral de arredondamento. | -| Base já apurada0,03 | 0,03 | 0,0048 | Valor fiscal apresentado após arredondamento indeterminado. | -| Duas linhas de base0,03 cada | 0,06 no total | 0,0048 por linha; 0,0096 no agregado | A convenção hipotética de arredondar ao cêntimo mais próximo produziria0,00 por linha e0,01 no agregado; diferença demonstra a dependência, não autoriza essa convenção. | - -### Estimativa actualizada após a consulta - -O enquadramento geral avançou; a dependência de arredondamento permanece concreta e a consolidação regulamentar pertinente não foi fechada. **Não é possível dar prazo incondicional de activação.** Estimativa restante de execução após disponibilização/esclarecimento desses elementos: **3–6h para concluir aplicação documental, ficha/casos e cadastro em rascunho QA; 12–20h para integração e aceitação técnica, total15–26h**. É reestimativa de engenharia, não horas medidas nem promessa de solução documental nesse intervalo. A espera por documento/esclarecimento e revisão/activação continuam excluídas. Se o fundamento introduzir agregação ou condições fora do contrato previsto, reestimar antes de ampliar o lote. Não houve implementação de código, cadastro ou activação nesta adenda. +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. diff --git a/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json b/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json index 580d5d5c..0d7e4636 100644 --- a/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json +++ b/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json @@ -347,5 +347,47 @@ "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" } } diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index 04d53834..473d19d1 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -1762,3 +1762,23 @@ A Tempo autorizou posteriormente a integração de `13f68f1b`. O commit foi inte 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. diff --git a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv index ed7c8cfa..10ebdd17 100644 --- a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv +++ b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv @@ -10652,7 +10652,7 @@ 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. | [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.",,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. | [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.",,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,,,,,,,,,,,,,, diff --git a/resources/js/components/fiscal-qa-preview.tsx b/resources/js/components/fiscal-qa-preview.tsx new file mode 100644 index 00000000..ab65b3b3 --- /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 ( +
+
+ Pré-visualização fiscal QA — 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..018bf85f 100644 --- a/resources/js/components/simulation-form.tsx +++ b/resources/js/components/simulation-form.tsx @@ -4,6 +4,7 @@ import { } 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 +887,7 @@ export function SimulationForm() { () => readVersionedArithmeticReportReference(result), [result], ); + const isFiscalQaPreview = arithmeticSnapshot?.scenario_kind === 'fiscal_rule_qa_preview'; const versionedReportMissing = arithmeticResult.integrity === 'versioned' && arithmeticReportReference === null; @@ -3705,7 +3707,7 @@ export function SimulationForm() { {(current.id === 'tax' || result) && (

- {current.id === 'tax' + {isFiscalQaPreview ? 'Pré-visualização fiscal QA seleccionada' : current.id === 'tax' ? 'Cenário personalizado de planeamento' : 'Resultado histórico seleccionado'}

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

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

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

+ {isFiscalQaPreview && } {result.status === 'completed' && (
- {arithmeticResult.integrity !== + {!isFiscalQaPreview && arithmeticResult.integrity !== 'invalid' && (
@@ -4179,8 +4182,7 @@ export function SimulationForm() { 'não identificada', 'não identificada', )}{' '} - · resultado - aritmético{' '} + · {source.result_snapshot.scenario_kind === 'fiscal_rule_qa_preview' ? 'IVA calculado exacto em QA' : 'resultado aritmético'}{' '} {displayScalar( sourceResult.value ?? (sourceResult.integrity === diff --git a/resources/js/lib/fiscal-qa-preview.ts b/resources/js/lib/fiscal-qa-preview.ts new file mode 100644 index 00000000..eda5b382 --- /dev/null +++ b/resources/js/lib/fiscal-qa-preview.ts @@ -0,0 +1,59 @@ +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 (snapshot.scenario_kind !== "fiscal_rule_qa_preview") return []; + 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", "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", + "Declarado pelo utilizador; não validado juridicamente", + ], + [ + "Regra / versão / estado", + `${text(rule?.rule_code)} / ${text(rule?.rule_version)} / 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", + "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..7f3297fd 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 && currentCalculation?.snapshot?.scenario_kind === 'fiscal_rule_qa_preview') { + for (const [field, value] of fiscalQaRows(currentCalculation.snapshot)) { + finance.rows.push({ field, value, origin: 'Snapshot fiscal QA preservado; sem recálculo', classification: 'Rascunho QA; cálculo exacto sem arredondamento automático; 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' + snapshot?.scenario_kind === 'fiscal_rule_qa_preview' + ? `Base tributável declarada × taxa cadastrada ÷ 100; cálculo decimal exacto, sem arredondamento. Ver regra e proveniência QA 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: snapshot?.scenario_kind === 'fiscal_rule_qa_preview' ? '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..f224c010 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; @@ -1848,6 +1849,13 @@ export function exportCompletedArithmeticCsv( ); } const result = simulation.result_snapshot as Record; + if (result.scenario_kind === 'fiscal_rule_qa_preview') { + 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/routes/investment-zones.php b/routes/investment-zones.php index b293b1ac..6516396e 100644 --- a/routes/investment-zones.php +++ b/routes/investment-zones.php @@ -27,6 +27,7 @@ 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::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..16826791 --- /dev/null +++ b/tests/Feature/SimulationFiscalQaPreviewTest.php @@ -0,0 +1,150 @@ +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.'); + } +} diff --git a/tests/frontend/fiscal-qa-preview.test.mjs b/tests/frontend/fiscal-qa-preview.test.mjs new file mode 100644 index 00000000..2a483ef3 --- /dev/null +++ b/tests/frontend/fiscal-qa-preview.test.mjs @@ -0,0 +1,133 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { fiscalQaRows } from "../../resources/js/lib/fiscal-qa-preview.ts"; +import { exportCompletedArithmeticCsv } 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", + ); +});