feat: connect published IVA consumer to normal simulator in QA
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-10-21 11:59:50 +02:00
parent 2ced7b790c
commit 226fd308dd
24 changed files with 747 additions and 74 deletions
+5
View File
@@ -4,6 +4,11 @@ APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost:8000
# Separate from publication. Bind only an explicitly authorised published rule version.
INVESTMOZ_IVA_CONSUMER_ENABLED=false
INVESTMOZ_IVA_CONSUMER_RULE_ID=
INVESTMOZ_IVA_CONSUMER_RULE_VERSION=0
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
@@ -37,11 +37,17 @@ class SimulationController extends Controller
$token = $actorId === null ? $request->header('X-Simulation-Token') : null;
abort_if($actorId === null && (! is_string($token) || strlen($token) < 32 || strlen($token) > 256), 422, 'X-Simulation-Token opaco gerado pelo cliente é obrigatório para execução anónima.');
$validated = $request->validated();
$data = ['base' => (string) $validated['base'], 'rate' => (string) $validated['rate'],
'reduction' => (string) ($validated['reduction'] ?? '0'), 'organization_id' => $validated['organization_id'] ?? null];
$organizationId = $data['organization_id'] ?? null;
$organizationId = $validated['organization_id'] ?? null;
$execution->authorizeOrganization($actorId, $organizationId);
$result = $execution->create($data, $actorId, $organizationId, $token, $key);
if (($validated['scenario_kind'] ?? null) === 'fiscal_rule_published') {
$input = array_intersect_key($validated, array_flip(['base', 'currency', 'reference_date', 'operation', 'scope_declared']));
$input['scope_declared'] = (bool) $input['scope_declared'];
$result = $execution->createPublishedFiscal($input, $actorId, $organizationId, $token, $key);
} else {
$data = ['base' => (string) $validated['base'], 'rate' => (string) $validated['rate'],
'reduction' => (string) ($validated['reduction'] ?? '0')];
$result = $execution->create($data, $actorId, $organizationId, $token, $key);
}
$simulation = $result['simulation'];
$body = ['data' => $simulation, 'replayed' => $result['replayed']];
if ($actorId === null) {
@@ -40,6 +40,7 @@ class InvestmentLegalSourceController extends Controller
$data = $request->validate(['version_no' => ['required', 'integer', 'min:1']]);
return DB::transaction(function () use ($request, $id, $action, $data) {
DB::select('select pg_advisory_xact_lock(261022, hashtext(?))', [$id]);
$source = InvestmentLegalSource::whereKey($id)->lockForUpdate()->firstOrFail();
abort_if((int) $source->version_no !== (int) $data['version_no'], 409, 'Versão alterada.');
if ($action === 'validate') {
@@ -11,10 +11,47 @@ class StoreSimulationRequest extends FormRequest
return true;
}
public function messages(): array
{
return [
'required' => 'Preencha o campo :attribute. As entradas foram preservadas.',
'string' => 'Indique :attribute em formato textual.',
'regex' => 'Indique uma base decimal positiva ou zero, com ponto decimal e sem separadores de milhares.',
'max' => 'O campo :attribute excede o tamanho permitido.',
'in' => 'A opção indicada em :attribute não pertence a este cálculo.',
'date_format' => 'Indique uma data de exigibilidade válida.',
'boolean' => 'Indique se declara o enquadramento da operação.',
'prohibited' => 'O campo :attribute não pode ser definido pelo cliente neste cálculo.',
];
}
public function attributes(): array
{
return ['base' => 'base tributável', 'currency' => 'moeda', 'reference_date' => 'data de exigibilidade',
'operation' => 'operação', 'scope_declared' => 'declaração de enquadramento', 'scenario_kind' => 'tipo de cálculo',
'rule_id' => 'regra', 'rule_version' => 'versão da regra', 'rate' => 'taxa', 'reduction' => 'redução'];
}
/** @return array<string, list<mixed>> */
public function rules(): array
{
if ($this->input('scenario_kind') === 'fiscal_rule_published') {
return [
'scenario_kind' => ['required', 'in:fiscal_rule_published'],
'base' => ['required', 'string', 'max:40', 'regex:/^\d+(?:\.\d+)?$/'],
'currency' => ['required', 'in:MZN'],
'reference_date' => ['required', 'date_format:Y-m-d'],
'operation' => ['required', 'in:internal_general_net'],
'scope_declared' => ['required', 'boolean'],
'organization_id' => ['nullable', 'uuid', 'exists:organizations,id'],
'rate' => ['prohibited'], 'reduction' => ['prohibited'],
'rule_id' => ['prohibited'], 'rule_version' => ['prohibited'],
'fiscal_preview' => ['prohibited'],
];
}
return [
'scenario_kind' => ['sometimes', 'in:custom_planning'],
'base' => ['required', 'numeric', 'min:0'],
'rate' => ['required', 'numeric', 'between:0,100'],
'reduction' => ['required', 'numeric', 'between:0,100'],
+7 -5
View File
@@ -65,10 +65,12 @@ class RunSimulation implements ShouldQueue
$input = json_decode($scenario->parameters, true, flags: JSON_THROW_ON_ERROR);
$resultId = (string) Str::uuid();
$engineVersion = 'portal-arithmetic-v1+simulation-v'.$simulation->version_no;
if (($input['scenario_kind'] ?? null) === 'fiscal_rule_qa_preview') {
abort_unless(IsolatedQaDatabase::allowsSchemaChanges(), 404);
if (in_array($input['scenario_kind'] ?? null, ['fiscal_rule_qa_preview', 'fiscal_rule_published'], true)) {
if ($input['scenario_kind'] === 'fiscal_rule_qa_preview') {
abort_unless(IsolatedQaDatabase::allowsSchemaChanges(), 404);
}
$result = $input['fiscal_preview'];
$engineVersion = 'fiscal-draft-preview-v1';
$engineVersion = $result['context']['engine_version'];
$result['context']['scenario_id'] = (string) $scenario->id;
$result['context']['scenario_version'] = (int) $scenario->version_no;
} else {
@@ -108,7 +110,7 @@ class RunSimulation implements ShouldQueue
$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',
'context' => $result['fiscal_provenance']['context'], 'scope_status' => $result['fiscal_provenance']['scope_status'],
] : $input, JSON_THROW_ON_ERROR);
$canonicalSource = DB::selectOne('select CAST(? AS jsonb)::text as canonical', [$sourceJson])->canonical;
DB::table('simulation_result_sources')->insertOrIgnore(['id' => (string) Str::uuid(), 'result_id' => $resultId,
@@ -119,7 +121,7 @@ class RunSimulation implements ShouldQueue
$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',
'id' => (string) Str::uuid(), 'result_id' => $resultId, 'source_type' => $input['scenario_kind'] === 'fiscal_rule_published' ? 'tax_rule_published' : 'tax_rule_draft',
'source_id' => $provenance['rule']['id'], 'source_version' => (string) $provenance['rule']['rule_version'],
'source_snapshot' => $provenanceJson, 'source_sha256' => hash('sha256', $canonicalProvenance), 'created_at' => now(),
]);
+17 -5
View File
@@ -34,7 +34,15 @@ final class SimulationExecution
$actorId, null, null, $key);
}
private function createPayload(array $payload, ?int $actorId, ?string $organizationId, ?string $token, string $key): array
public function createPublishedFiscal(array $input, ?int $actorId, ?string $organizationId, ?string $token, string $key): array
{
$payload = ['scenario_kind' => 'fiscal_rule_published', 'fiscal_input' => $input, 'organization_id' => $organizationId];
return $this->createPayload($payload, $actorId, $organizationId, $token, $key,
fn (): array => app(SimulationPublishedFiscal::class)->admit($input));
}
private function createPayload(array $payload, ?int $actorId, ?string $organizationId, ?string $token, string $key, ?callable $admit = null): array
{
$tokenHash = $actorId === null ? hash_hmac('sha256', (string) $token, (string) config('app.key')) : null;
$scopeHash = hash('sha256', $actorId !== null ? (string) $actorId : ($tokenHash ?? 'anonymous'));
@@ -42,7 +50,7 @@ final class SimulationExecution
ksort($payload);
$payloadHash = hash('sha256', json_encode($payload, JSON_THROW_ON_ERROR));
return DB::transaction(function () use ($actorId, $organizationId, $token, $tokenHash, $scopeHash, $keyHash, $payloadHash, $payload): array {
return DB::transaction(function () use ($actorId, $organizationId, $token, $tokenHash, $scopeHash, $keyHash, $payloadHash, $payload, $admit): array {
$this->setContext($actorId, $tokenHash);
DB::select('select pg_advisory_xact_lock(261001, hashtext(?))', [$scopeHash.$keyHash]);
$receipt = DB::table('simulation_idempotency')->where('scope_hash', $scopeHash)->where('key_hash', $keyHash)->first();
@@ -54,6 +62,10 @@ final class SimulationExecution
return ['simulation' => Simulation::with(['scenarios', 'reports'])->whereKey((string) $receipt->simulation_id)->firstOrFail(), 'token' => null, 'replayed' => true];
}
if ($admit !== null) {
$payload['fiscal_preview'] = $admit();
}
$plainToken = $actorId === null ? $token : null;
$tokenHash = $actorId === null ? hash_hmac('sha256', $plainToken, (string) config('app.key')) : null;
$this->setContext($actorId, $tokenHash);
@@ -61,8 +73,8 @@ 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' => $payload['scenario_kind'] === 'fiscal_rule_qa_preview'
? ['engine' => 'fiscal-draft-preview-v1', 'mode' => 'qa_draft_only', 'arithmetic' => 'exact_decimal_no_rounding']
'input_snapshot' => $payload, 'calculation_snapshot' => in_array($payload['scenario_kind'], ['fiscal_rule_qa_preview', 'fiscal_rule_published'], true)
? ['engine' => $payload['fiscal_preview']['context']['engine_version'], 'mode' => $payload['fiscal_preview']['fiscal_provenance']['mode'], 'arithmetic' => 'exact_decimal_no_rounding']
: ['engine' => 'portal-arithmetic-v1', 'formula' => 'base * rate / 100 * (1 - reduction / 100)', 'legal_rate_source' => null],
]);
$scenarioId = (string) Str::uuid();
@@ -104,7 +116,7 @@ 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,
abort_if(in_array($simulation->input_snapshot['scenario_kind'] ?? null, ['fiscal_rule_qa_preview', 'fiscal_rule_published'], true), 422,
'Crie uma nova pré-visualização fiscal; este snapshot conserva a regra e as entradas originais.');
$this->requireMatch($request, $simulation);
$scenario = DB::table('simulation_scenarios')->where('simulation_id', $id)->where('id', $scenarioId)->lockForUpdate()->first();
+1 -32
View File
@@ -7,7 +7,6 @@ namespace App\Services;
use App\Models\InvestmentLegalSource;
use App\Models\User;
use App\Support\IsolatedQaDatabase;
use Brick\Math\BigRational;
use Illuminate\Support\Facades\DB;
/** Exact operation-level IVA calculation against an explicit QA draft; never a payment/return total. */
@@ -43,37 +42,7 @@ final class SimulationFiscalQaPreview
['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.',
],
];
$snapshot = app(SimulationFiscalSnapshot::class)->build($version, $input, $resolution, $evaluation, $missing);
return app(SimulationExecution::class)->createFiscalQaPreview($snapshot, $actor->id, $key);
});
+58
View File
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace App\Services;
use Brick\Math\BigRational;
/** Shared snapshot representation; eligibility is resolved by each admission adapter. */
final class SimulationFiscalSnapshot
{
public function build(array $version, array $input, array $resolution, ?array $evaluation, array $missing, bool $published = false): array
{
$date = $input['reference_date'];
$context = ['operation' => $input['operation'], 'currency' => $input['currency']];
$exact = ($evaluation['status'] ?? null) === 'determinate'
? (string) BigRational::of($evaluation['result'])->toBigDecimal()
: null;
if ($exact === null && $missing === []) {
$missing = $evaluation['missing'] ?? ['eligible_parameter'];
}
$snapshot = [
'contract_version' => 1, 'scenario_kind' => $published ? 'fiscal_rule_published' : 'fiscal_rule_qa_preview',
'base' => $input['base'], 'currency' => $input['currency'], 'indicative_total' => $exact,
'legal_interpretation' => false, 'formula_version' => 'investmoz.tax.v1',
'context' => [
'reference_date' => $date, 'currency' => $input['currency'], 'currency_fraction_digits' => 2,
'scenario_id' => null, 'scenario_version' => 1, 'engine_version' => $published ? 'fiscal-published-v1' : 'fiscal-draft-preview-v1',
'ruleset_version' => $version['rule_code'].':'.$version['rule_version'],
],
'result' => [
'status' => $exact === null ? 'indeterminate' : 'calculated', 'value' => $exact, 'unit' => 'MZN',
'explanation' => $exact === null
? 'IVA indeterminado: o contexto, a fonte ou o período não permitem resolver o parâmetro deste lote QA.'
: 'IVA calculado com precisão decimal exacta, sem arredondamento. Ensaio QA de regra em rascunho; enquadramento declarado, não validado. Não é saldo de declaração nem valor a pagar.',
'missing_inputs' => $missing, 'source_ids' => [(string) $version['legal_source_id']],
],
'presentation' => ['value' => $exact, 'currency' => $input['currency'], 'rounding_applied' => false, 'purpose' => 'operation_tax_not_payment'],
// Exact rational steps supplement the decimal result; no float conversion.
'intermediate' => $evaluation,
'fiscal_provenance' => [
'mode' => $published ? 'published_version' : 'qa_draft_only', 'scope_status' => ($input['scope_declared'] ?? true) ? 'user_declared' : 'undetermined', 'context' => $context,
'rounding_policy' => $version['rounding_policy'], 'resolution' => $resolution,
'rule' => $version, 'source_acceptance' => 'Fonte oficial aceite pela decisão Tempo de 08/10/2026; não constitui aprovação jurídica desta operação ou publicação da regra.',
],
];
if ($published) {
$snapshot['result']['explanation'] = $exact === null
? 'IVA indeterminado: falta contexto, fonte ou parâmetro elegível para esta operação.'
: 'IVA calculado pela versão publicada identificada, sem arredondamento. Enquadramento declarado, não validado. Não é saldo de declaração nem valor a pagar.';
$snapshot['fiscal_provenance']['admitted_at'] = now()->toISOString();
$snapshot['fiscal_provenance']['source_acceptance'] = 'Fonte oficial aceite pela Tempo em 08/10/2026; não constitui validação jurídica desta operação.';
}
return $snapshot;
}
}
@@ -0,0 +1,82 @@
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\InvestmentLegalSource;
use App\Models\SimulationTaxRuleVersion;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
/** Admission freezes eligibility without granting the public consumer governance write access. */
final class SimulationPublishedFiscal
{
public function availability(): array
{
$enabled = (bool) config('simulation-fiscal-consumer.enabled');
$eligible = $enabled && $this->boundRule()->where('status', 'published')->exists();
return ['enabled' => $eligible, 'coverage_start' => '2026-01-01', 'coverage_end' => '2026-12-31',
'message' => $eligible ? 'IVA geral sobre base líquida disponível; enquadramento declarado.'
: 'O cálculo de IVA geral sobre base líquida está indisponível para novas execuções. Os históricos mantêm-se acessíveis.'];
}
private function boundRule(): Builder
{
if (! Str::isUuid((string) config('simulation-fiscal-consumer.rule_id')) || config('simulation-fiscal-consumer.rule_version') < 1) {
return SimulationTaxRuleVersion::query()->whereRaw('false');
}
return SimulationTaxRuleVersion::query()->whereKey(config('simulation-fiscal-consumer.rule_id'))
->where('rule_version', config('simulation-fiscal-consumer.rule_version'))
->where('rule_code', 'IVA-GERAL-BASE-LIQUIDA')->where('component', 'IVA')
->where('scope_key', 'mz.internal-general-net.mzn');
}
public function admit(array $input): array
{
// Called inside createPayload, after idempotent replay, never by the worker.
abort_unless(DB::transactionLevel() > 0, 500);
abort_unless(config('simulation-fiscal-consumer.enabled'), 503,
'O cálculo de IVA está temporariamente indisponível. Os dados e históricos foram preservados.');
DB::select('select pg_advisory_xact_lock_shared(261021, hashtext(?))', [(string) config('simulation-fiscal-consumer.rule_id')]);
$rule = $this->boundRule()->first();
abort_unless($rule && $rule->status === 'published', 503,
'A versão autorizada de IVA não está disponível para novas execuções. Os históricos foram preservados.');
abort_if($input['reference_date'] < '2026-01-01' || $input['reference_date'] > '2026-12-31', 422,
'Este cálculo abrange apenas exigibilidade em 2026. As entradas foram preservadas.');
abort_if(collect($rule->rule_definition['nodes'] ?? [])->contains(fn (array $node): bool => ($node['op'] ?? null) === 'round'), 422,
'A versão seleccionada não é compatível com o cálculo exacto deste consumidor.');
DB::select('select pg_advisory_xact_lock_shared(261022, hashtext(?))', [(string) $rule->legal_source_id]);
$source = InvestmentLegalSource::whereKey($rule->legal_source_id)->firstOrFail();
// Publish calculation provenance, not private governance notes or staff identifiers.
$version = array_intersect_key($rule->attributesToArray(), array_flip([
'id', 'rule_code', 'rule_version', 'component', 'scope_key', 'status', 'valid_from', 'valid_until',
'legal_source_id', 'source_version_no', 'source_sha256', 'rule_definition', 'parameters', 'rounding_policy',
]));
$version['source'] = array_intersect_key($source->attributesToArray(), array_flip([
'id', 'title', 'url', 'diploma', 'article', 'competent_entity', 'valid_from', 'valid_until', 'validation_status', 'version_no',
]));
$resolution = app(SimulationTaxParameterResolver::class)->resolvePublishedForPeriod('IVA',
$input['reference_date'], $input['reference_date'], ['currency' => $input['currency'], 'operation' => $input['operation']]);
$rate = collect($resolution)->firstWhere('parameter_code', 'IVA_GENERAL_RATE');
$missing = [];
$evaluation = null;
if (! ($input['scope_declared'] ?? false)) {
$missing[] = 'operation_scope';
}
if (($rate['status'] ?? null) !== 'resolved' || ($rate['unit'] ?? null) !== 'percent'
|| ($rate['rule_id'] ?? null) !== (string) $rule->id
|| (int) ($rate['rule_version'] ?? 0) !== (int) $rule->rule_version) {
$missing[] = 'eligible_parameter';
}
if ($missing === []) {
$evaluation = app(SimulationTaxRuleExpression::class)->evaluate($rule->rule_definition,
['base_tributavel' => $input['base']], ['iva_general_rate' => $rate['value']]);
}
return app(SimulationFiscalSnapshot::class)->build($version, $input, $resolution, $evaluation, $missing, true);
}
}
@@ -65,6 +65,8 @@ final class SimulationTaxRuleGovernance
return DB::transaction(function () use ($actor, $id, $version, $action, $evidence): SimulationTaxRuleVersion {
$this->setContext($actor);
// Serialize public admission with publication/suspension, without widening RLS.
DB::select('select pg_advisory_xact_lock(261021, hashtext(?))', [$id]);
$rule = SimulationTaxRuleVersion::query()->lockForUpdate()->findOrFail($id);
$this->matchVersion($rule, $version);
$next = ['version_no' => $version + 1];
+8
View File
@@ -0,0 +1,8 @@
<?php
return [
// Publication and consumer exposure are separate Tempo decisions.
'enabled' => env('INVESTMOZ_IVA_CONSUMER_ENABLED', false),
'rule_id' => env('INVESTMOZ_IVA_CONSUMER_RULE_ID'),
'rule_version' => (int) env('INVESTMOZ_IVA_CONSUMER_RULE_VERSION', 0),
];
@@ -87,6 +87,8 @@ As15–26h anteriores eram condicionais e incluíam o trabalho de cadastro e int
## Revisão consolidada de83d2f97d
**Actualização técnica de21/10/2026 — delta2ced7b79 implementado em QA:** o consumidor por versão publicada está ligado ao simulador normal, com activação desligada por omissão, selecção da regra no servidor e gravação/recuperação/relatório pelo histórico existente. O mesmo manual regista a demonstração pelo Edge, indeterminação sem declaração de âmbito, repetição sem duplicação, suspensão antes do processamento da fila, conclusão com versão congelada e recusa de novas execuções. Oito testes dirigidos/75asserções e dois testes frontend novos passaram, além deTypeScript/build/Pint. Mantêm-se integralmente o âmbito2026, exclusões, fontes e precisão desta ficha; não se repete a fundamentação jurídica nem se lhe atribui validação da Tempo. O procedimento único foi actualizado para integração, publicação da versão real e activação, decisões ainda não executadas na homologação. A fixture publicada exclusivamente no clone QA foi suspensa após o ensaio; os históricos foram preservados. As notas abaixo sobre ausência do consumidor descrevem o estado anterior à implementação deste delta.
**Disponibilização no simulador existente:** a proposta única «Consumidor IVA no simulador da homologação», no mesmo manual, delimita o código ainda necessário e as três operações distintas: integração com consumidor desligado, publicação da versão pela governação existente e activação da ligação à versão autorizada. O percurso QA aceite não equivale a esse consumidor público. Mantêm-se âmbito2026, exclusões, precisão exacta e históricos; suspensão impede novas admissões e preserva execuções já admitidas. Esta proposta não executa nenhuma das três operações nem amplia a cobertura fiscal.
O [mesmo manual de rastreabilidade](../unification/traceability-and-manual.md) consolida âmbito/período, operações excluídas/indeterminadas e os procedimentos concretos de integração, publicação, activação e desactivação, preservando históricos. Identifica também que83d2f97d não contém um consumidor fiscal público: a publicação torna parâmetros elegíveis na API existente, mas não transforma o percurso QA em cálculo público activado.
@@ -484,5 +484,109 @@
"consumer_activation": false,
"user_legal_validation": false,
"record": "docs/investmoz/unification/traceability-and-manual.md: Proposta unica - consumidor IVA"
},
"published_consumer_qa_20261021": {
"proposal_base": "2ced7b79",
"status": "Implemented and demonstrated in normal QA simulator; separate integration/publication/activation decisions pending",
"consumer_default_enabled": false,
"scope": "IVA general net taxable base, declared internal general operation, MZN, exigibility 2026 only",
"feature_tests": {
"passed": 8,
"assertions": 75,
"isolated_database": "simulator_fiscal_checks_20261021_test",
"rls_non_superuser": true,
"separate_connection_lock_probe": true
},
"frontend_new_link_tests_passed": 2,
"typescript": "passed",
"build": "passed",
"pint": "passed",
"accepted_evidence_reused": [
"decimal calculation",
"exporters",
"native PDF pagination",
"financial planning and recovery"
],
"normal_simulator_browser_journey": {
"database": "simulator_fiscal_delta_20261021_test",
"url": "http://127.0.0.1:18580/simulador-fiscal",
"fixture_only": true,
"user_id": 167,
"rule_id": "01a15ddc-b0c5-727f-bb24-d806a69bd1f5",
"rule_status": "suspended",
"suspended_at": "2026-10-21T09:52:16.000000Z",
"executions": [
{
"id": "53663785-2e5d-4be2-8a05-c0c96a57896d",
"status": "completed",
"base": "1000.03",
"value": null,
"result_status": "indeterminate",
"scope_status": "undetermined",
"admitted_at": "2026-10-21T09:50:53.949756Z",
"frozen_rule_status": "published",
"rule_id": "01a15ddc-b0c5-727f-bb24-d806a69bd1f5",
"rule_version": 2,
"report_id": "63d427ac-e6ae-481c-adad-e5f39fcc3c15",
"report_sha256": "d405705f6ce050518a0b400bed842b2aeaec143af3d78c54088100417d326c64",
"result_report_hash_equal": true
},
{
"id": "5fa778f1-413e-4643-8c67-1b93ab290da7",
"status": "completed",
"base": "1000.03",
"value": "160.0048",
"result_status": "calculated",
"scope_status": "user_declared",
"admitted_at": "2026-10-21T09:51:11.307106Z",
"frozen_rule_status": "published",
"rule_id": "01a15ddc-b0c5-727f-bb24-d806a69bd1f5",
"rule_version": 2,
"report_id": "533b60df-ce44-40fd-9911-c7ed492dbd7e",
"report_sha256": "2c5cc798c06920aee0707465ab8b7fee517783db24939b3921c76a70999dad77",
"result_report_hash_equal": true
},
{
"id": "be1e8399-ea95-4aa7-9ac0-7fa392932ac1",
"status": "completed",
"base": "1001.03",
"value": "160.1648",
"result_status": "calculated",
"scope_status": "user_declared",
"admitted_at": "2026-10-21T09:52:11.782758Z",
"frozen_rule_status": "published",
"rule_id": "01a15ddc-b0c5-727f-bb24-d806a69bd1f5",
"rule_version": 2,
"report_id": "5e416a48-db07-449b-8eae-aa51a1538f04",
"report_sha256": "997f026665e586c662ae574fd2ff7f62e0254819adb26aea920575e47e0490a3",
"result_report_hash_equal": true
}
],
"execution_count": 3,
"pending_queue_records": 0,
"main_integration": false,
"homologation_publication_or_activation": false
},
"observed": [
"initial disabled consumer",
"insufficient scope persisted as indeterminate",
"declared scope calculated through published server binding",
"repeat without duplicate",
"database queue admitted before suspension and processed after suspension",
"new input refused with values preserved",
"hard reload and history recovery of calculated and indeterminate snapshots",
"CSV control enabled without rerunning download"
],
"final_qa_fixture_status": "suspended",
"final_qa_switch_enabled": false,
"historical_records_preserved": true,
"main_local_head": "13f68f1b",
"main_integration": false,
"homologation_rule_publication": false,
"homologation_consumer_activation": false,
"user_legal_validation": false,
"user_local_file_review": false,
"procedure": "Existing manual: Procedimento unico das tres operacoes - agora executavel apos decisao",
"proofs_private": true
}
}
@@ -1864,3 +1864,43 @@ Excluem-se preço com IVA incluído, taxas reduzidas, isenções/listas, bases e
Históricos não são recalculados, apagados ou renomeados como se tivessem usado a versão nova. A suspensão fica no registo de governação, sem alterar retroactivamente o conteúdo/hash do relatório. Retoma exige versão publicada elegível e ligação/activação autorizada; não existe transição directa de suspended parapublished no fluxo actual. Não eliminar a leitura de snapshots fiscais ao reverter código.
**Verificação nova estritamente necessária:** contrato público e permissões existentes; impossibilidade de executar desligado ou com versão não publicada; correspondência temporal/âmbito e precisão na ligação; cenário publicado na interface/histórico/relatório/exportação; suspensão versus admissão concorrente e repetição sem duplicação; preservação da leitura histórica e do modo personalizado. Não repetir as10páginas, valores/CSV ou ensaios QA aceites. Não se cria nova matriz/backlog; AUD-SIM-INT02 conserva o estado parcial do simulador fiscal integral e referencia esta delimitação. As fontes e provas existentes sustentam a proposta; não se atribui ao utilizador conferência local ou validação jurídica.
### Consumidor publicado implementado no simulador normal — fecho QA de21/10/2026
**Autorização:** instrução expressa da Tempo para implementar2ced7b79 em QA, sem nova proposta, integração emmain, publicação de regra real ou activação na homologação. O delta descrito acima está agora implementado. O fecho é da ligação deste único consumidor; mantém âmbito/exclusões da ficha v1.2 e não encerra o simulador fiscal integral.
**Percurso utilizável:** `/simulador-fiscal` → percurso habitual → etapa «Operação fiscal» → «IVA geral sobre base líquida» → base decimal textual, moeda explicitamente seleccionada, exigibilidade e declaração de contexto → «Calcular IVA e gravar». O resultado aparece no próprio simulador e no histórico existente. Pode recuperar-se sem recalcular; relatório, referência/hash e exportações usam o mesmo snapshot. Não é necessário acesso à administração fiscal para calcular. A consulta de parâmetros e o planeamento personalizado conservam os seus percursos e rótulos próprios. A base de orçamento e a taxa manual não são usadas como entradas implícitas deste IVA.
#### Implementação e fronteiras efectivas
- `StoreSimulationRequest` e `SimulationController::store` distinguem `fiscal_rule_published` do planeamento. O servidor recebe apenas entradas declaradas, não taxa/regra/versão/snapshot executável fornecidos pelo cliente; valida formato decimal textual, MZN, data e contexto. Falta de declaração de enquadramento grava indeterminação com valor nulo, nunca zero. Campos inválidos/período fora2026 são recusados com mensagens portuguesas e entradas preservadas.
- `SimulationPublishedFiscal` selecciona o ID/versão configurado no servidor, exige estado publicado e âmbito do lote, reutiliza `SimulationTaxParameterResolver` e `SimulationTaxRuleExpression`, e congela a proveniência antes da fila. `SimulationFiscalSnapshot` partilha a representação decimal com o adaptador QA, cujas guardas não foram removidas. Não há regra fiscal activada por importação ou arranque.
- `SimulationExecution` consulta a chave idempotente antes de verificar nova admissão. Assim, desligamento/suspensão não impedem recuperar o mesmo pedido já admitido. Nova chave exige consumidor ligado e versão publicada elegível; mudar entradas com a mesma chave dá conflito. A interface conserva a chave do pedido fiscal concluído para que repetir o mesmo cálculo não crie uma execução adicional. Novas entradas produzem nova chave/execução.
- Bloqueios consultivos transaccionais partilhados na admissão e exclusivos nas transições de governação/fonte serializam suspensão/revogação e admissão. Não se usa `SELECT FOR SHARE` na regra pública: poderia exigir uma política de alteração, contrariando as permissões existentes. A decisão de usar bloqueios consultivos é escolha de implementação, não nova exigência da Tempo. Não se alteraram políticas/grants nem se acrescentaram papéis de produto.
- `RunSimulation` processa o snapshot fiscal já admitido, sem resolver outra taxa/versão e sem reconsultar a activação. Guarda resultado, relatório e proveniência nos modelos existentes. Notas internas de revisão e identificadores dos funcionários ficam fora da proveniência devolvida pelo novo consumidor. Snapshots QA antigos conservam os seus rótulos/estado originais.
- O leitor de resultados do simulador foi adaptado ao contrato fiscal: o leitor aritmético anterior marcava o novo snapshot como inconsistente e desactivava exportação/conversão. Agora reconhece valor fiscal e indeterminação coerentes, e recusa divergência entre valor calculado e apresentação. CSV e dossier encaminham o novo tipo pelos componentes existentes; não se reescreveram os exportadores nem se repetiram provas aceites de paginação/valores.
- Configuração nova e desligada por omissão: `INVESTMOZ_IVA_CONSUMER_ENABLED=false`, `INVESTMOZ_IVA_CONSUMER_RULE_ID=` e `INVESTMOZ_IVA_CONSUMER_RULE_VERSION=0`, documentadas em `.env.example`. `/api/v1/fiscal-consumer` informa a disponibilidade sem cache; o servidor continua a exigir autorização na execução, independentemente do estado previamente visto no ecrã. A interface actualiza disponibilidade após uma tentativa, preservando entradas quando encontra suspensão. Configuração incompleta não escolhe outra regra automaticamente.
#### Provas novas, sem reabrir o conjunto aceite
Oito testes dirigidos deAPI/serviço,75asserções, passaram no clone isolado `simulator_fiscal_checks_20261021_test`, com transacções revertidas: ligação publicada, identidade/propriedade, token anónimo, idempotência/conflito, histórico/relatório/hash, contexto/fonte insuficientes, contrato/cobertura, suspensão/desligamento e trabalhador congelado, planeamento ainda disponível, configuração inválida e bloqueios de admissão. Um papel PostgreSQL temporário sem superutilizador/bypassRLS comprovou execução com a política pública de leitura, sem acesso aos eventos da governação ou alteração da regra; papel/grants de ensaio foram revertidos com a transacção. Uma segunda ligação comprovou que os bloqueios exclusivos da regra/fonte não atravessam a transacção de admissão. Dois testes frontend dirigidos ao novo tipo publicado passaram: encaminhamento coerente para ecrã/CSV/dossier e aceitação da indeterminação/recusa da apresentação divergente. TypeScript, build ePint passaram. Aviso do build sobre optimização opcional de fontes/fontaine não impediu a compilação.
**Observação no Edge, formulário normal:** porta18580, clone `simulator_fiscal_delta_20261021_test`, conta sintética de utilizador comum167, sem papéis fiscais administrativos. A fixture publicada é a versão sintética2 de `IVA-GERAL-BASE-LIQUIDA`, ID`01a15ddc-b0c5-727f-bb24-d806a69bd1f5`, para não substituir o rascunho QA anterior; fonte/actores sintéticos só neste clone. A governação existente foi usada na preparação da fixture. Não houve publicação na homologação nem declaração de aprovação jurídica da regra real.
1. Observou-se inicialmente o consumidor desligado e o botão indisponível. Habilitou-se somente a fixture local guardada contra qualquer outra base.
2. Base1000.03MZN, exigibilidade2026-10-21, sem declaração de contexto: execução`53663785-2e5d-4be2-8a05-c0c96a57896d`, resultado indeterminado, entradas conservadas e relatório guardado. A mesma base/data/moeda com declaração criou`5fa778f1-413e-4643-8c67-1b93ab290da7`, valor160.0048. Repetir o botão manteve essas duas execuções, sem terceira duplicada.
3. Reteve-se somente a fila QA dedicada `fiscal-consumer-qa-20261021`. A base1001.03 admitiu`be1e8399-ea95-4aa7-9ac0-7fa392932ac1` às09:52:11.782758UTC. Suspendeu-se a fixture pela governação às09:52:16UTC, ainda com essa execução emqueued e snapshot publicado congelado. O trabalhador real `queue:work database --queue=fiscal-consumer-qa-20261021 --once --tries=1 --timeout=30` concluiu-a depois da suspensão, com160.1648 e a mesma versão2. Nenhuma outra fila foi consumida.
4. Nova entrada1002.03 foi recusada: «A versão autorizada de IVA não está disponível para novas execuções. Os históricos foram preservados.» A base1002.03, data, moeda e declaração continuaram no ecrã; não houve quarta execução. Após recarga completa, recuperaram-se pelo histórico tanto o resultado calculado como o indeterminado. Ambos mostraram o snapshot original, sem alertas de inconsistência, com CSV disponível. O ficheiro não foi descarregado novamente; a implementação e as provas aceites do exportador foram reutilizadas.
5. Conferência por leitura: exactamente3execuções da conta, todascompleted; hashes de resultado/relatório iguais nas três; zero trabalhos pendentes na fila dedicada. O relatório da execução que concluiu após suspensão é`5e416a48-db07-449b-8eae-aa51a1538f04`, SHA256`997f026665e586c662ae574fd2ff7f62e0254819adb26aea920575e47e0490a3`. A fixture permanece suspensa e a configuração local foi reposta emdesligado; os históricos ficam acessíveis.
Provas privadas em `outputs/simulator-continuation-20261020`: `published-consumer-tests.txt`, `published-consumer-journey-receipt.json`, `published-consumer-calculated-ui.txt/.png`, `published-consumer-suspended-ui.txt`, `published-consumer-recovered-ui.txt/.png` e `published-consumer-indeterminate-recovered-ui.txt`. O primeiro acesso ao separador antecedeu o arranque do servidor local e apresentou ligação recusada; após arranque normal prosseguiu-se. Não é evidência de falha do simulador. Não se repetiram as10páginas do PDF nativo nem o conjunto aceite de83d2f97d/13f68f1b. A aritmética do exemplo demonstra a ligação e preservação do resultado, não a aplicabilidade fiscal a qualquer operação.
#### Procedimento único das três operações — agora executável após decisão
1. **Integrar código:** a Tempo identifica a referência exacta desta candidata na branch `codex/simulator-integrated-review`. Integrar emmain apenas após decisão; o envio desencadeia disponibilização automática na homologação. Manter `INVESTMOZ_IVA_CONSUMER_ENABLED=false`; não executar fixture, comandoQA ou cadastro fiscal no deploy. Confirmar build servido e aplicação; aplicar o procedimento habitual de recarga da configuração e reinício de trabalhadores persistentes, se existirem, antes de activar o novo tipo de trabalho. Não há migration neste delta. A leitura dos snapshots deve permanecer em qualquer versão de reversão; desligar o consumidor é preferível a voltar a um build que desconheça os históricos novos.
2. **Publicar versão real:** com autorização de escrita, backup/restauro confirmado e acção autenticada, reutilizar a fonte real aceite e os actores/papéis existentes. Cadastrar a versão fundamentada e usar `POST /api/v2/investment/admin/tax-rules/{id}/{action}`, com `version_no` e `evidence`, em `submit` → `review` → `approve` → `publish`. RegistarID e `rule_version` publicados. Não copiar a fixture/versão2/actoresUUID deste ensaio para homologação. Se a fonte já estiver cadastrada correctamente, reutilizá-la; não exigir nova certificação da credibilidade oficial. A publicação disponibiliza parâmetros naAPI existente, mas o consumidor continua desligado.
3. **Activar consumidor:** por decisão específica da Tempo, preencher `INVESTMOZ_IVA_CONSUMER_RULE_ID=<ID real autorizado>` e `INVESTMOZ_IVA_CONSUMER_RULE_VERSION=<rule_version autorizada>`; só então definir `INVESTMOZ_IVA_CONSUMER_ENABLED=true`. Aplicar a configuração pelo procedimento operacional existente (`php artisan config:cache` quando se usar cache de configuração; recarregar processos persistentes conforme o alojamento). Confirmar `/api/v1/fiscal-consumer` e a etapa fiscal do simulador, registando decisão, commit/build, ID/versão e instante de exposição. Não usar `version_no` de concorrência como se fosse `rule_version`. O servidor recusa outra versão em vez de escolher a mais recente silenciosamente.
**Suspender/desactivar:** repor o interruptor emfalse e aplicar a configuração impede novas admissões; para retirar a elegibilidade da própria regra usar a transição existente `suspend`, com versão corrente/evidência e actor autorizado. Pedidos já admitidos concluem pelo snapshot; repetição idempotente recupera-os. Histórico/relatório/exportação continuam acessíveis sem novo cálculo, publicação ou activação. Retoma segue uma versão publicada elegível e activação explícita, nuncaSQL directo sobre o estado suspenso. A janela2026 mantém-se um limite de cobertura, não data de caducidade legal.
**Saldo:** ligação funcional do primeiro consumidor fechada em QA, pronta para revisão e decisão sobre as três operações; sem outra preparação genérica identificada. AUD-SIM-INT02 permanece parcial para cobertura fiscal integral. As estimativas anteriores não constituem horas ainda por executar deste delta já concluído. Main local continua em13f68f1b; sem merge/push, publicação real ou activação na homologação. A Tempo não conferiu os ficheiros locais nem validou juridicamente as fontes nesta execução.
File diff suppressed because one or more lines are too long
@@ -5,11 +5,11 @@ export default function FiscalQaPreview({ snapshot }: { snapshot: unknown }) {
if (rows.length === 0) return null;
return (
<section
aria-label="Pré-visualização fiscal QA"
aria-label="Resultado fiscal preservado"
className="my-3 rounded border border-amber-700 bg-amber-50 p-3"
>
<h5 className="font-semibold">
Pré-visualização fiscal QA — cálculo exacto sem arredondamento
{rows[0][1]} — cálculo exacto sem arredondamento
</h5>
<dl className="mt-2 space-y-2 text-sm">
{rows.map(([label, value]) => (
+17 -13
View File
@@ -1,3 +1,4 @@
import { SimulationPublishedIva, type PublishedIvaInput } from './simulation-published-iva';
import {
PublicDocumentContent,
type PublicDocument,
@@ -887,7 +888,7 @@ export function SimulationForm() {
() => readVersionedArithmeticReportReference(result),
[result],
);
const isFiscalQaPreview = arithmeticSnapshot?.scenario_kind === 'fiscal_rule_qa_preview';
const isFiscalQaPreview = ['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(arithmeticSnapshot?.scenario_kind));
const versionedReportMissing =
arithmeticResult.integrity === 'versioned' &&
arithmeticReportReference === null;
@@ -2159,15 +2160,15 @@ export function SimulationForm() {
);
}
async function runSimulation() {
async function runSimulation(publishedInput?: PublishedIvaInput) {
if (calculationInFlightRef.current) return;
const validation = validateSimulationDraft(draft, 'tax');
if (draft.fiscal_scenario_mode === 'custom_planning') {
const validation = publishedInput ? {} : validateSimulationDraft(draft, 'tax');
if (!publishedInput && draft.fiscal_scenario_mode === 'custom_planning') {
Object.assign(validation, validateCustomFiscalPlanning(draft));
}
setErrors(validation);
setServerError('');
if (draft.fiscal_scenario_mode !== 'custom_planning') return;
if (!publishedInput && draft.fiscal_scenario_mode !== 'custom_planning') return;
if (Object.keys(validation).length) {
document
.getElementById(`field-${Object.keys(validation)[0]}`)
@@ -2178,7 +2179,7 @@ export function SimulationForm() {
setIsCalculating(true);
try {
const xsrf = csrfToken();
const body = {
const body = publishedInput ?? {
base: draft.base,
rate: draft.rate,
reduction: draft.reduction,
@@ -2286,7 +2287,7 @@ export function SimulationForm() {
}
}
if (
JSON.stringify({
publishedInput || JSON.stringify({
base: draft.base,
rate: draft.rate,
reduction: draft.reduction,
@@ -2306,7 +2307,7 @@ export function SimulationForm() {
'O cálculo não foi concluído. Os dados introduzidos foram preservados.',
);
}
sessionStorage.removeItem(idempotencyStorageKey);
if (!publishedInput) sessionStorage.removeItem(idempotencyStorageKey);
} catch (cause) {
setServerError(
cause instanceof Error
@@ -3600,6 +3601,7 @@ export function SimulationForm() {
}
/>
)}
{current.id === 'tax' && <SimulationPublishedIva busy={isCalculating} onCalculate={runSimulation} />}
{current.id === 'tax' && (
<div
id="draft-tool-fiscal"
@@ -3697,8 +3699,8 @@ export function SimulationForm() {
O período e as respostas servem para encaminhar uma
exploração informativa. A residência é auto-indicada ou
fica por determinar; não é inferida por nacionalidade ou
dias de presença. Regras de 2026 continuam pendentes de
validação e este formulário não determina cumprimento
dias de presença. Cada cálculo identifica a regra, versão, cobertura e
limitações; este formulário não determina cumprimento
fiscal. Um crédito estimado de IVA não significa pedido
aprovado nem dinheiro disponível. Não carregue
documentos nesta simulação exploratória.
@@ -3707,7 +3709,7 @@ export function SimulationForm() {
{(current.id === 'tax' || result) && (
<div className="space-y-3 border-t pt-4">
<h4 className="font-semibold">
{isFiscalQaPreview ? 'Pré-visualização fiscal QA seleccionada' : current.id === 'tax'
{isFiscalQaPreview ? (arithmeticSnapshot?.scenario_kind === 'fiscal_rule_published' ? 'IVA pela versão publicada seleccionada' : 'Pré-visualização fiscal QA seleccionada') : current.id === 'tax'
? 'Cenário personalizado de planeamento'
: 'Resultado histórico seleccionado'}
</h4>
@@ -4182,7 +4184,7 @@ export function SimulationForm() {
'não identificada',
'não identificada',
)}{' '}
· {source.result_snapshot.scenario_kind === 'fiscal_rule_qa_preview' ? 'IVA calculado exacto em QA' : 'resultado aritmético'}{' '}
· {['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(source.result_snapshot.scenario_kind)) ? 'IVA calculado exacto' : 'resultado aritmético'}{' '}
{displayScalar(
sourceResult.value ??
(sourceResult.integrity ===
@@ -4421,7 +4423,9 @@ export function SimulationForm() {
e, se já escolhido, o cálculo percentual
personalizado. Consultar parâmetros fiscais
apresenta fontes e lacunas; não determina imposto
devido.
devido. Na etapa Operação fiscal encontra também
o cálculo de IVA geral sobre base líquida, quando
activado, com âmbito e versão identificados.
</p>
<button
type="button"
@@ -0,0 +1,68 @@
import { useEffect, useState } from 'react';
export type PublishedIvaInput = {
scenario_kind: 'fiscal_rule_published';
base: string;
currency: 'MZN';
reference_date: string;
operation: 'internal_general_net';
scope_declared: boolean;
};
export function SimulationPublishedIva({ busy, onCalculate }: {
busy: boolean;
onCalculate: (input: PublishedIvaInput) => Promise<void>;
}) {
const [available, setAvailable] = useState(false);
const [message, setMessage] = useState('A consultar disponibilidade do cálculo de IVA…');
const [base, setBase] = useState('');
const [date, setDate] = useState('');
const [currency, setCurrency] = useState('');
const [declared, setDeclared] = useState(false);
const [error, setError] = useState('');
useEffect(() => {
let active = true;
fetch('/api/v1/fiscal-consumer', { credentials: 'same-origin', headers: { Accept: 'application/json' } })
.then(async (response) => {
if (!response.ok) throw new Error();
const payload = await response.json();
if (!active) return;
setAvailable(payload?.data?.enabled === true);
setMessage(typeof payload?.data?.message === 'string' ? payload.data.message : 'Cálculo de IVA indisponível.');
}).catch(() => { if (active) setMessage('Não foi possível consultar a disponibilidade do IVA. As entradas e históricos foram preservados.'); });
return () => { active = false; };
}, []);
async function calculate() {
if (!/^\d+(?:\.\d+)?$/.test(base) || base.length > 40 || !date || currency !== 'MZN') {
setError('Indique uma base líquida decimal válida, a moeda MZN e a data de exigibilidade. As entradas foram preservadas.');
return;
}
setError('');
await onCalculate({ scenario_kind: 'fiscal_rule_published', base, currency: 'MZN', reference_date: date,
operation: 'internal_general_net', scope_declared: declared });
// Refresh after admission/refusal so a suspension is reflected without discarding inputs.
try {
const response = await fetch('/api/v1/fiscal-consumer', { credentials: 'same-origin', headers: { Accept: 'application/json' } });
if (!response.ok) return;
const payload = await response.json();
setAvailable(payload?.data?.enabled === true);
if (typeof payload?.data?.message === 'string') setMessage(payload.data.message);
} catch { /* The execution flow already preserves and reports connection errors. */ }
}
return <section aria-label="IVA geral sobre base líquida" className="space-y-3 rounded border border-emerald-700 p-4">
<h4 className="font-semibold">IVA geral sobre base líquida</h4>
<p role="status">{message}</p>
<p className="text-sm">Cobertura: exigibilidade em 2026, operação interna à taxa geral. A base tributável é indicada por si; não é copiada do orçamento. Sem isenções, taxas reduzidas, bases especiais, importações, deduções, declaração ou pagamento. O cálculo não valida o enquadramento jurídico.</p>
<fieldset disabled={busy} className="space-y-3">
<label className="block">Base tributável líquida (decimal, com ponto)<input aria-label="Base tributável líquida" inputMode="decimal" value={base} onChange={(e) => setBase(e.target.value)} className="block rounded border p-2" /></label>
<label className="block">Moeda da base<select aria-label="Moeda da base IVA" value={currency} onChange={(e) => setCurrency(e.target.value)} className="block rounded border p-2"><option value="">Seleccione</option><option value="MZN">MZN</option></select></label>
<label className="block">Data de exigibilidade declarada<input aria-label="Data de exigibilidade IVA" type="date" min="2026-01-01" max="2026-12-31" value={date} onChange={(e) => setDate(e.target.value)} className="block rounded border p-2" /></label>
<label className="block"><input type="checkbox" checked={declared} onChange={(e) => setDeclared(e.target.checked)} /> Declaro operação interna à taxa geral, base líquida já apurada e ausência das excepções acima.</label>
<p className="text-sm">Sem esta declaração, o resultado fica indeterminado. O valor calculado conserva a precisão decimal; não é saldo a pagar.</p>
<button type="button" disabled={!available || busy} onClick={() => void calculate()} className="rounded bg-emerald-800 px-4 py-2 text-white disabled:opacity-50">{busy ? 'A calcular…' : 'Calcular IVA e gravar'}</button>
</fieldset>
{error && <p role="alert">{error}</p>}
</section>;
}
+6 -5
View File
@@ -3,7 +3,8 @@ 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<string, unknown>;
if (snapshot.scenario_kind !== "fiscal_rule_qa_preview") return [];
if (!["fiscal_rule_qa_preview", "fiscal_rule_published"].includes(String(snapshot.scenario_kind))) return [];
const published = snapshot.scenario_kind === "fiscal_rule_published";
const provenance = snapshot.fiscal_provenance as
Record<string, unknown> | undefined;
const rule = provenance?.rule as Record<string, unknown> | undefined;
@@ -19,7 +20,7 @@ export function fiscalQaRows(value: unknown): Array<[string, string]> {
? String(item)
: "não disponível";
return [
["Relatório", "Pré-visualização fiscal QA de rascunho — sem activação"],
["Relatório", published ? "IVA pela versão publicada; ver enquadramento abaixo" : "Pré-visualização fiscal QA de rascunho — sem activação"],
[
"IVA calculado (exacto; não é saldo a pagar)",
value.result.value ?? "Indeterminado",
@@ -31,11 +32,11 @@ export function fiscalQaRows(value: unknown): Array<[string, string]> {
["Data de exigibilidade declarada", value.context.reference_date],
[
"Enquadramento",
"Declarado pelo utilizador; não validado juridicamente",
provenance?.scope_status === "undetermined" ? "Indeterminado: falta declaração do enquadramento" : "Declarado pelo utilizador; não validado juridicamente",
],
[
"Regra / versão / estado",
`${text(rule?.rule_code)} / ${text(rule?.rule_version)} / rascunho`,
`${text(rule?.rule_code)} / ${text(rule?.rule_version)} / ${published ? "publicada no momento da admissão" : "rascunho"}`,
],
[
"Vigência cadastrada",
@@ -53,7 +54,7 @@ export function fiscalQaRows(value: unknown): Array<[string, string]> {
],
[
"Limitações",
"Regra não publicada; cobertura demonstrada em 2026; sem deduções, declaração ou saldo a pagar.",
published ? "Cobertura de 2026; operação interna geral em MZN; sem excepções, deduções, declaração ou saldo a pagar." : "Regra não publicada; cobertura demonstrada em 2026; sem deduções, declaração ou saldo a pagar.",
],
];
}
+5 -5
View File
@@ -345,9 +345,9 @@ function makeSections(
}
const finance = sections.get('finance');
if (finance && currentCalculation?.snapshot?.scenario_kind === 'fiscal_rule_qa_preview') {
if (finance && ['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(currentCalculation?.snapshot?.scenario_kind)) && currentCalculation?.snapshot) {
for (const [field, value] of fiscalQaRows(currentCalculation.snapshot)) {
finance.rows.push({ field, value, origin: 'Snapshot fiscal QA preservado; sem recálculo', classification: 'Rascunho QA; cálculo exacto sem arredondamento automático; não é saldo a pagar' });
finance.rows.push({ field, value, origin: 'Snapshot fiscal preservado; sem recálculo', classification: 'Cálculo exacto; ver estado da regra no snapshot; não é saldo a pagar' });
}
}
if (finance && currentCalculation) {
@@ -423,12 +423,12 @@ function makeSections(
{
field: 'Fórmula e valores usados',
value:
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.`
['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(snapshot?.scenario_kind))
? `Base tributável declarada × taxa cadastrada ÷ 100; cálculo decimal exacto, sem arredondamento. Ver regra e proveniência acima.`
: snapshot && typeof snapshot.formula_version === 'string'
? `Fórmula ${snapshot.formula_version}; base ${displayValue(snapshot.base, 'não disponível')}; taxa ${displayValue(snapshot.rate_percent, 'não disponível')}%; redução ${displayValue(snapshot.reduction_percent, 'não disponível')}%; moeda ${displayValue(snapshot.currency, 'não indicada')}`
: 'A fórmula não foi identificada no snapshot; ver explicação da execução',
origin: snapshot?.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',
origin: ['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(snapshot?.scenario_kind)) ? (snapshot?.scenario_kind === 'fiscal_rule_published' ? 'Versão publicada na admissão; sem validação jurídica da operação' : 'Snapshot QA de regra em rascunho; sem activação ou validação jurídica da operação') : 'Snapshot guardado; sem aplicação de regra legal',
classification: 'Reutilização do resultado existente',
},
{
+14 -1
View File
@@ -1675,6 +1675,19 @@ export function readArithmeticSnapshot(
}
const result = snapshot as Record<string, unknown>;
const value = result.indicative_total;
if (['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(result.scenario_kind))) {
const presentation = result.presentation as Record<string, unknown> | undefined;
if (isApiSimulationResultSnapshotV1(result) && fiscalQaRows(result).length > 0 &&
result.result.value === value && presentation?.value === value &&
presentation?.rounding_applied === false && result.context.currency === 'MZN' &&
result.result.unit === 'MZN' &&
((result.result.status === 'calculated' && typeof value === 'string' && /^\d+(?:\.\d+)?$/.test(value)) ||
(result.result.status === 'indeterminate' && value === null && result.result.missing_inputs.length > 0))) {
return { integrity: 'versioned', value: value as string | null,
context: result.context as unknown as Record<string, unknown>, explanation: result.result.explanation };
}
return { integrity: 'invalid', value: null, context: null, explanation: null };
}
if (typeof value !== 'string' || !/^(?:\d+)(?:\.\d+)?$/.test(value)) {
return {
integrity: 'invalid',
@@ -1849,7 +1862,7 @@ export function exportCompletedArithmeticCsv(
);
}
const result = simulation.result_snapshot as Record<string, unknown>;
if (result.scenario_kind === 'fiscal_rule_qa_preview') {
if (['fiscal_rule_qa_preview', 'fiscal_rule_published'].includes(String(result.scenario_kind))) {
const rows = fiscalQaRows(result);
const report = readVersionedArithmeticReportReference(simulation);
if (!rows.length || !report) throw new Error('A pré-visualização fiscal não corresponde ao relatório preservado.');
+2
View File
@@ -13,6 +13,7 @@ use App\Http\Controllers\PortalServicesController;
use App\Http\Controllers\PublicBusinessOpportunityController;
use App\Http\Middleware\CorrelationId;
use App\Http\Middleware\EnsureSimulationRecoveryCookie;
use App\Services\SimulationPublishedFiscal;
use Illuminate\Support\Facades\Route;
Route::prefix('v1')->middleware(['throttle:api', CorrelationId::class])->group(function () {
@@ -38,6 +39,7 @@ Route::prefix('v1')->middleware(['throttle:api', CorrelationId::class])->group(f
});
Route::prefix('v1')->middleware(['web', 'throttle:api', CorrelationId::class])->group(function () {
Route::get('/fiscal-consumer', fn (SimulationPublishedFiscal $consumer) => response()->json(['data' => $consumer->availability()])->header('Cache-Control', 'no-store'));
Route::post('/tax-parameters/resolve', [SimulationTaxParameterController::class, 'resolve'])->middleware('throttle:20,1');
Route::get('/questionnaires/{code}', [SimulationController::class, 'questionnaire']);
Route::middleware(EnsureSimulationRecoveryCookie::class)->group(function () {
@@ -0,0 +1,214 @@
<?php
namespace Tests\Feature;
use App\Jobs\RunSimulation;
use App\Models\InvestmentLegalSource;
use App\Models\Simulation;
use App\Models\SimulationTaxRuleVersion;
use App\Models\User;
use App\Services\SimulationExecution;
use App\Services\SimulationTaxRuleGovernance;
use Illuminate\Foundation\Testing\DatabaseTransactions;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Str;
use Tests\TestCase;
/** Published fixtures exist only in an isolated QA transaction, never in homologation. */
class SimulationPublishedFiscalTest extends TestCase
{
use DatabaseTransactions;
private User $consumer;
private User $publisher;
private SimulationTaxRuleVersion $rule;
private InvestmentLegalSource $source;
protected function setUp(): void
{
parent::setUp();
Queue::fake();
$this->consumer = User::factory()->create(['email_verified_at' => now()]);
$actors = [];
foreach (['editors', 'reviewers', 'approvers', 'publishers'] as $role) {
$actors[$role] = User::factory()->withTwoFactor()->create();
config(['simulation-tax-governance.'.$role => [$actors[$role]->email]]);
}
$this->publisher = $actors['publishers'];
$this->source = InvestmentLegalSource::create([
'title' => 'Synthetic QA fixture, not a homologation publication', 'url' => 'https://www.inm.gov.mz/',
'diploma' => 'Lei 22/2022', 'article' => 'Artigo 17(1)', 'competent_entity' => 'Assembleia da República',
'valid_from' => '2023-01-01', 'valid_until' => null, 'validation_status' => 'validated',
'owner_id' => $actors['editors']->id, 'version_no' => 1, 'validator_id' => $actors['reviewers']->id, 'validated_at' => now(),
]);
$data = json_decode(file_get_contents(database_path('qa/iva-general-net-v1.json')), true, flags: JSON_THROW_ON_ERROR);
$data['legal_source_id'] = $this->source->id;
$gov = app(SimulationTaxRuleGovernance::class);
$this->rule = $gov->create($actors['editors'], $data);
foreach (['submit' => 'editors', 'review' => 'reviewers', 'approve' => 'approvers', 'publish' => 'publishers'] as $action => $role) {
$this->rule = $gov->transition($actors[$role], $this->rule->id, $this->rule->version_no, $action,
'Synthetic isolated QA fixture for consumer integration only.');
}
config(['simulation-fiscal-consumer.enabled' => true, 'simulation-fiscal-consumer.rule_id' => $this->rule->id,
'simulation-fiscal-consumer.rule_version' => 1]);
}
private function input(array $overrides = []): array
{
return ['scenario_kind' => 'fiscal_rule_published', 'base' => '1000.03', 'currency' => 'MZN',
'reference_date' => '2026-10-21', 'operation' => 'internal_general_net', 'scope_declared' => true, ...$overrides];
}
private function create(array $overrides = [], ?string $key = null)
{
return $this->actingAs($this->consumer)->postJson('/api/v1/simulations', $this->input($overrides),
['Idempotency-Key' => $key ?? (string) Str::uuid()]);
}
private function finish(string $id): Simulation
{
(new RunSimulation($id, $this->consumer->id, null))->handle(app(SimulationExecution::class));
return Simulation::with('reports')->findOrFail($id);
}
public function test_normal_simulator_admits_published_rule_and_reuses_history_and_reports(): void
{
$key = (string) Str::uuid();
$this->getJson('/api/v1/fiscal-consumer')->assertOk()->assertJsonPath('data.enabled', true);
$id = $this->create([], $key)->assertAccepted()->json('data.id');
$this->create([], $key)->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('data.id', $id);
$this->create(['base' => '1001'], $key)->assertConflict();
$simulation = $this->finish($id);
$snapshot = $simulation->result_snapshot;
$this->assertSame('fiscal_rule_published', $snapshot['scenario_kind']);
$this->assertSame('calculated', $snapshot['result']['status']);
$this->assertSame($this->rule->id, $snapshot['fiscal_provenance']['rule']['id']);
$this->assertSame('published', $snapshot['fiscal_provenance']['rule']['status']);
$this->assertArrayNotHasKey('review_evidence', $snapshot['fiscal_provenance']['rule']);
$this->assertArrayNotHasKey('author_id', $snapshot['fiscal_provenance']['rule']);
$this->getJson('/api/v1/simulations/history')->assertOk()->assertJsonPath('meta.total', 1);
$this->getJson('/api/v1/simulations/'.$id)->assertOk()->assertJsonPath('data.result_snapshot', $snapshot);
$report = $simulation->reports->first();
$this->getJson('/api/v1/simulations/'.$id.'/reports/'.$report->id)->assertOk()->assertJsonPath('data.report_snapshot', $snapshot);
$this->assertSame($report->report_sha256, DB::table('simulation_results')->where('simulation_id', $id)->value('result_sha256'));
$this->assertSame(1, Simulation::where('owner_user_id', $this->consumer->id)->count());
$this->assertDatabaseHas('simulation_result_sources', ['source_id' => $this->rule->id, 'source_type' => 'tax_rule_published']);
$this->actingAs(User::factory()->create(['email_verified_at' => now()]))->getJson('/api/v1/simulations/'.$id)->assertNotFound();
$this->getJson('/api/v1/simulations/'.$id.'/reports/'.$report->id)->assertNotFound();
}
public function test_missing_scope_or_changed_source_is_indeterminate_not_zero(): void
{
$id = $this->create(['scope_declared' => false])->assertAccepted()->json('data.id');
$snapshot = $this->finish($id)->result_snapshot;
$this->assertSame('indeterminate', $snapshot['result']['status']);
$this->assertNull($snapshot['result']['value']);
$this->assertContains('operation_scope', $snapshot['result']['missing_inputs']);
$this->assertSame('undetermined', $snapshot['fiscal_provenance']['scope_status']);
$this->source->update(['version_no' => 2]);
$id = $this->create()->assertAccepted()->json('data.id');
$snapshot = $this->finish($id)->result_snapshot;
$this->assertSame('indeterminate', $snapshot['result']['status']);
$this->assertNull($snapshot['result']['value']);
$this->assertContains('eligible_parameter', $snapshot['result']['missing_inputs']);
}
public function test_suspension_refuses_new_admissions_but_frozen_queue_and_replays_survive(): void
{
$key = (string) Str::uuid();
$id = $this->create([], $key)->assertAccepted()->json('data.id');
$this->assertDatabaseHas('simulations', ['id' => $id, 'status' => 'queued']);
app(SimulationTaxRuleGovernance::class)->transition($this->publisher, $this->rule->id, $this->rule->version_no,
'suspend', 'QA: suspend after admission and before worker execution.');
$this->getJson('/api/v1/fiscal-consumer')->assertOk()->assertJsonPath('data.enabled', false);
$this->create()->assertStatus(503);
$this->create([], $key)->assertOk()->assertJsonPath('data.id', $id)->assertJsonPath('replayed', true);
config(['simulation-fiscal-consumer.enabled' => false]);
$this->create()->assertStatus(503);
$simulation = $this->finish($id);
$this->assertSame('completed', $simulation->status);
$this->assertSame('published', $simulation->result_snapshot['fiscal_provenance']['rule']['status']);
$this->assertSame($this->rule->id, $simulation->result_snapshot['fiscal_provenance']['rule']['id']);
$this->assertSame('suspended', $this->rule->fresh()->status);
$this->create([], $key)->assertOk()->assertJsonPath('data.status', 'completed');
$this->getJson('/api/v1/simulations/'.$id.'/reports/'.$simulation->reports->first()->id)->assertOk()
->assertJsonPath('data.report_snapshot', $simulation->result_snapshot);
$this->getJson('/api/v1/simulations/history')->assertOk()->assertJsonPath('meta.total', 1);
}
public function test_server_selection_cannot_be_overridden_and_coverage_is_bounded(): void
{
$this->create(['rate' => '0'])->assertUnprocessable();
$this->create(['rule_id' => (string) Str::uuid()])->assertUnprocessable();
$this->create(['reference_date' => '2027-01-01'])->assertUnprocessable();
$this->create(['currency' => 'USD'])->assertUnprocessable();
config(['simulation-fiscal-consumer.rule_version' => 2]);
$this->create()->assertStatus(503);
config(['simulation-fiscal-consumer.rule_id' => 'invalid-configuration']);
$this->getJson('/api/v1/fiscal-consumer')->assertOk()->assertJsonPath('data.enabled', false);
$this->assertSame(0, Simulation::where('owner_user_id', $this->consumer->id)->count());
}
public function test_anonymous_cookie_token_ownership_is_reused(): void
{
$key = (string) Str::uuid();
$response = $this->postJson('/api/v1/simulations', $this->input(), ['Idempotency-Key' => $key]);
$id = $response->assertAccepted()->json('data.id');
$token = $response->json('access_token');
$this->getJson('/api/v1/simulations/'.$id, ['X-Simulation-Token' => $token])->assertOk();
$this->getJson('/api/v1/simulations/'.$id, ['X-Simulation-Token' => str_repeat('x', 40)])->assertNotFound();
$this->getJson('/api/v1/simulations/history')->assertUnauthorized();
}
public function test_disabled_consumer_does_not_disable_custom_planning(): void
{
config(['simulation-fiscal-consumer.enabled' => false]);
$this->create()->assertStatus(503);
$this->actingAs($this->consumer)->postJson('/api/v1/simulations', ['base' => '100', 'rate' => '5', 'reduction' => '0'],
['Idempotency-Key' => (string) Str::uuid()])->assertAccepted()->assertJsonPath('data.input_snapshot.scenario_kind', 'custom_planning');
}
public function test_public_consumer_works_under_existing_rls_without_governance_context(): void
{
// Role and grants roll back with the isolated test transaction. No application permission is added.
DB::statement('CREATE ROLE qa_fiscal_consumer_rls_20261021 NOLOGIN NOSUPERUSER NOBYPASSRLS');
DB::statement('GRANT USAGE ON SCHEMA public TO qa_fiscal_consumer_rls_20261021');
DB::statement('GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO qa_fiscal_consumer_rls_20261021');
DB::statement('GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO qa_fiscal_consumer_rls_20261021');
DB::statement("SELECT set_config('app.simulation_tax_governance', 'off', true)");
DB::statement('SET LOCAL ROLE qa_fiscal_consumer_rls_20261021');
try {
$this->assertSame(1, SimulationTaxRuleVersion::whereKey($this->rule->id)->count());
$id = $this->create()->assertAccepted()->json('data.id');
$this->assertNotEmpty($id);
$this->assertSame(0, DB::table('simulation_tax_rule_events')->where('rule_version_id', $this->rule->id)->count());
// An UPDATE scope is not granted by the public SELECT policy.
$this->assertSame(0, SimulationTaxRuleVersion::whereKey($this->rule->id)->update(['status' => 'suspended']));
} finally {
DB::statement('RESET ROLE');
}
}
public function test_admission_holds_shared_locks_until_transaction_end(): void
{
$this->create()->assertAccepted();
config(['database.connections.fiscal_lock_probe' => config('database.connections.pgsql')]);
$probe = DB::connection('fiscal_lock_probe');
$probe->beginTransaction();
try {
// A concurrent suspension/source transition cannot cross an admitted transaction.
$rule = $probe->selectOne('select pg_try_advisory_xact_lock(261021, hashtext(?)) as acquired', [$this->rule->id]);
$source = $probe->selectOne('select pg_try_advisory_xact_lock(261022, hashtext(?)) as acquired', [$this->source->id]);
$this->assertFalse($rule->acquired);
$this->assertFalse($source->acquired);
} finally {
$probe->rollBack();
DB::purge('fiscal_lock_probe');
}
}
}
+44 -1
View File
@@ -1,7 +1,7 @@
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 { exportCompletedArithmeticCsv, readArithmeticSnapshot } from "../../resources/js/lib/simulation-form.ts";
import { createProjectDossierReport } from "../../resources/js/lib/project-dossier-report.ts";
function fixture() {
@@ -131,3 +131,46 @@ test("dossier report uses the same exact snapshot and preserves genuine indeterm
"Indeterminado",
);
});
test("published consumer routes the frozen snapshot through screen CSV and dossier without QA labels", () => {
const simulation = fixture();
const snapshot = simulation.result_snapshot;
snapshot.scenario_kind = "fiscal_rule_published";
snapshot.context.engine_version = "fiscal-published-v1";
snapshot.presentation = { value: snapshot.result.value, rounding_applied: false };
snapshot.result.explanation = "IVA pela versão publicada; enquadramento declarado.";
snapshot.fiscal_provenance.rule.status = "published";
snapshot.fiscal_provenance.scope_status = "user_declared";
snapshot.fiscal_provenance.mode = "published_version";
simulation.reports[0].report_snapshot = structuredClone(snapshot);
const before = JSON.stringify(simulation);
const rows = fiscalQaRows(snapshot);
assert.equal(readArithmeticSnapshot(snapshot).integrity, "versioned");
assert.match(rows[0][1], /publicada/);
assert.doesNotMatch(JSON.stringify(rows), /rascunho|não publicada|QA/);
const csv = exportCompletedArithmeticCsv(simulation, "2026-10-21T12:00:00Z");
for (const [label, value] of rows) {
assert.ok(csv.includes(label));
assert.ok(csv.includes(value.replaceAll('"', '""')));
}
const report = createProjectDossierReport({ project_title: "Published IVA QA", currency: "MZN" }, {
selectedSections: new Set(["finance"]), currentCalculation: { status: "completed", version: 2, snapshot },
});
const finance = report.sections.find((section) => section.id === "finance");
for (const [field, value] of rows) assert.ok(finance.rows.some((row) => row.field === field && row.value === value));
assert.doesNotMatch(JSON.stringify(finance), /rascunho|proveniência QA/);
assert.equal(JSON.stringify(simulation), before);
});
test("published consumer indeterminate snapshot is valid and a mismatched presentation is refused", () => {
const snapshot = fixture().result_snapshot;
snapshot.scenario_kind = "fiscal_rule_published";
snapshot.result.status = "indeterminate";
snapshot.result.value = null;
snapshot.result.missing_inputs = ["operation_scope"];
snapshot.indicative_total = null;
snapshot.presentation = { value: null, rounding_applied: false };
assert.equal(readArithmeticSnapshot(snapshot).integrity, "versioned");
snapshot.presentation.value = "0";
assert.equal(readArithmeticSnapshot(snapshot).integrity, "invalid");
});