From 4a4d1212751726da6a71d465b3ea2266e720cf28 Mon Sep 17 00:00:00 2001 From: nala Date: Wed, 21 Oct 2026 12:39:07 +0200 Subject: [PATCH] fix: allow authorised fiscal maintenance by the same responsible person --- .../SimulationFiscalSourceController.php | 46 ++++ .../EnsureScopedPortalAdministration.php | 42 ++-- app/Models/InvestmentLegalSource.php | 5 + .../SimulationFiscalSourceAcceptance.php | 20 ++ .../SimulationTaxGovernanceAccess.php | 11 +- .../SimulationTaxParameterResolver.php | 11 +- app/Services/SimulationTaxRuleGovernance.php | 15 +- config/simulation-tax-governance.php | 5 +- ...t_fiscal_maintenance_actor_constraints.php | 68 ++++++ .../ficha-IVA-GERAL-BASE-LIQUIDA-v1.md | 2 + ...-review-continuation-20261020-receipt.json | 151 ++++++++++++ docs/investmoz/simulations/rule-governance.md | 10 + .../unification/traceability-and-manual.md | 39 ++++ .../v2/auditoria-portal/matriz-requisitos.csv | 2 +- routes/investment-zones.php | 1 + .../SimulationFiscalMaintenanceActorTest.php | 217 ++++++++++++++++++ .../SimulationTaxRuleGovernanceTest.php | 9 +- 17 files changed, 615 insertions(+), 39 deletions(-) create mode 100644 app/Http/Controllers/SimulationFiscalSourceController.php create mode 100644 app/Services/SimulationFiscalSourceAcceptance.php create mode 100644 database/migrations/2026_10_21_130000_correct_fiscal_maintenance_actor_constraints.php create mode 100644 tests/Feature/SimulationFiscalMaintenanceActorTest.php diff --git a/app/Http/Controllers/SimulationFiscalSourceController.php b/app/Http/Controllers/SimulationFiscalSourceController.php new file mode 100644 index 00000000..57334045 --- /dev/null +++ b/app/Http/Controllers/SimulationFiscalSourceController.php @@ -0,0 +1,46 @@ +user(), 'editors'); + $data = $request->validate([ + 'title' => ['required', 'string', 'max:255'], 'url' => ['required', 'url:https', 'max:2000'], + 'diploma' => ['required', 'string', 'max:255'], 'article' => ['required', 'string', 'max:255'], + 'competent_entity' => ['required', 'string', 'max:255'], 'valid_from' => ['required', 'date_format:Y-m-d'], + 'valid_until' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:valid_from'], + 'sha256' => ['required', 'regex:/^[a-f0-9]{64}$/D'], + 'citation' => ['required', 'string', 'min:10', 'max:2000'], + 'official_origin' => ['required', 'string', 'min:10', 'max:2000'], + ]); + + return DB::transaction(function () use ($request, $data): JsonResponse { + $actor = $request->user(); + $acceptance = ['scope' => 'fiscal_parameters', 'basis' => 'Tempo-official-sources-20261008', + 'responsible_user_id' => $actor->id, 'sha256' => $data['sha256'], 'citation' => $data['citation'], + 'official_origin' => $data['official_origin'], 'accepted_at' => now()->toISOString(), + 'legal_opinion' => false, 'independent_review' => false]; + unset($data['sha256'], $data['citation'], $data['official_origin']); + $source = InvestmentLegalSource::create([...$data, 'owner_id' => $actor->id, 'validator_id' => null, + 'validation_status' => 'pending', 'validated_at' => null, 'version_no' => 1, 'fiscal_acceptance' => $acceptance]); + DB::table('audit_events')->insert(['source_system' => 'simulation-fiscal-maintenance', + 'entity_type' => 'investment_legal_sources', 'entity_id' => $source->id, 'actor_user_id' => $actor->id, + 'action' => 'official_source_acceptance', 'after_data' => json_encode([ + 'source' => $source->attributesToArray(), 'basis' => $acceptance, + 'sha256' => hash('sha256', $source->toJson()), + ], JSON_THROW_ON_ERROR)]); + + return response()->json($source->fresh(), 201); + }); + } +} diff --git a/app/Http/Middleware/EnsureScopedPortalAdministration.php b/app/Http/Middleware/EnsureScopedPortalAdministration.php index a55d5175..8e90a95a 100644 --- a/app/Http/Middleware/EnsureScopedPortalAdministration.php +++ b/app/Http/Middleware/EnsureScopedPortalAdministration.php @@ -1,31 +1,39 @@ user();abort_unless($actor,403); - $scope=app(ScopedPortalAdministration::class); - $path=$request->path(); - $interests=$request->is('management/interests','management/interests/*','api/v1/investment/admin/interests/*'); - // Exact areas of P1; unrelated management, fiscal rules and private records keep their gate. - $catalogue=$request->is('management/economic-references/lot-01','management/economic-references/lot-01/*', - 'management/education','management/education/*','management/investment/references','management/investment/references/*','api/v1/investment/admin/references','api/v1/investment/admin/references/*', - 'management/investment/zones','management/investment/zones/*','api/v2/investment/admin/zones','api/v2/investment/admin/zones/*', - 'management/balcao/atribuicoes','management/balcao/atribuicoes/*','management/balcao/servicos', - 'management/balcao/fontes') || ($request->isMethod('GET') && $path==='management/balcao-install'); - $allowed=$interests ? $scope->interestOperator($actor) : ($catalogue && $scope->administrative($actor)); - abort_unless($allowed || ($actor->can('manage-portal') && !app(\App\Services\PortalProfileAccess::class)->has($actor,'auditor')),403,'Acesso reservado ao âmbito já designado.'); - $response=$next($request); - if (!$request->isMethod('GET') && $response->getStatusCode()<400) { - app(\App\Services\PortalProfileAccess::class)->audit($actor,'scoped_administration',[ - 'method'=>$request->method(),'route'=>$request->route()?->uri(),'scope'=>$interests?'assigned-interest':'catalogue-administration']); + $actor = $request->user(); + abort_unless($actor, 403); + $scope = app(ScopedPortalAdministration::class); + $path = $request->path(); + $interests = $request->is('management/interests', 'management/interests/*', 'api/v1/investment/admin/interests/*'); + // Fiscal maintenance reuses authorised administration; unrelated gates remain unchanged. + $catalogue = $request->is('management/economic-references/lot-01', 'management/economic-references/lot-01/*', + 'management/education', 'management/education/*', 'management/investment/references', 'management/investment/references/*', 'api/v1/investment/admin/references', 'api/v1/investment/admin/references/*', + 'management/investment/zones', 'management/investment/zones/*', 'api/v2/investment/admin/zones', 'api/v2/investment/admin/zones/*', + 'management/balcao/atribuicoes', 'management/balcao/atribuicoes/*', 'management/balcao/servicos', + 'management/balcao/fontes') || ($request->isMethod('GET') && $path === 'management/balcao-install'); + $fiscal = $request->is('api/v2/investment/admin/tax-rules', 'api/v2/investment/admin/tax-rules/*', + 'api/v2/investment/admin/tax-rule-sources', 'management/investment/tax-rules/*'); + $allowed = ($fiscal && SimulationTaxGovernanceAccess::maintainer($actor)) || ($interests ? $scope->interestOperator($actor) : ($catalogue && $scope->administrative($actor))); + abort_unless($allowed || ($actor->can('manage-portal') && ! app(PortalProfileAccess::class)->has($actor, 'auditor')), 403, 'Acesso reservado ao âmbito já designado.'); + $response = $next($request); + if (! $request->isMethod('GET') && $response->getStatusCode() < 400) { + app(PortalProfileAccess::class)->audit($actor, 'scoped_administration', [ + 'method' => $request->method(), 'route' => $request->route()?->uri(), 'scope' => $fiscal ? 'fiscal-maintenance' : ($interests ? 'assigned-interest' : 'catalogue-administration')]); } + return $response; } } diff --git a/app/Models/InvestmentLegalSource.php b/app/Models/InvestmentLegalSource.php index 8a7c8b56..19f3f53c 100644 --- a/app/Models/InvestmentLegalSource.php +++ b/app/Models/InvestmentLegalSource.php @@ -16,4 +16,9 @@ class InvestmentLegalSource extends Model use HasUuids; protected $guarded = ['id']; + + protected function casts(): array + { + return ['fiscal_acceptance' => 'array']; + } } diff --git a/app/Services/SimulationFiscalSourceAcceptance.php b/app/Services/SimulationFiscalSourceAcceptance.php new file mode 100644 index 00000000..482cfb8f --- /dev/null +++ b/app/Services/SimulationFiscalSourceAcceptance.php @@ -0,0 +1,20 @@ +hasVerifiedEmail() && ! app(PortalProfileAccess::class)->has($user, 'auditor') && ($user->can('manage-portal') + || app(ScopedPortalAdministration::class)->administrative($user)); + } + public static function requireRole(User $user, string $role): void { abort_unless(in_array($role, self::ROLES, true), 500, 'Função de governação desconhecida.'); - abort_unless(in_array(strtolower($user->email), config('simulation-tax-governance.'.$role, []), true), + abort_unless(self::maintainer($user) || in_array(strtolower($user->email), config('simulation-tax-governance.'.$role, []), true), 403, 'O utilizador não está designado para esta função de governação fiscal.'); } public static function requireAnyRole(User $user): void { + if (self::maintainer($user)) { + return; + } foreach (self::ROLES as $role) { if (in_array(strtolower($user->email), config('simulation-tax-governance.'.$role, []), true)) { return; diff --git a/app/Services/SimulationTaxParameterResolver.php b/app/Services/SimulationTaxParameterResolver.php index 43edea14..e669c5f5 100644 --- a/app/Services/SimulationTaxParameterResolver.php +++ b/app/Services/SimulationTaxParameterResolver.php @@ -69,7 +69,9 @@ final class SimulationTaxParameterResolver ->where('rule.status', 'published') ->whereDate('rule.valid_from', '<=', $periodEnd) ->where(fn ($query) => $query->whereNull('rule.valid_until')->orWhereDate('rule.valid_until', '>=', $periodStart)) - ->where('source.validation_status', 'validated') + ->where(fn ($query) => $query->where('source.validation_status', 'validated')->orWhere(fn ($fiscal) => $fiscal->where('source.validation_status', 'pending')->where('source.fiscal_acceptance->scope', 'fiscal_parameters') + ->where('source.fiscal_acceptance->basis', 'Tempo-official-sources-20261008') + ->whereRaw("source.fiscal_acceptance->>'sha256'=rule.source_sha256"))) ->whereColumn('source.version_no', 'rule.source_version_no') ->whereDate('source.valid_from', '<=', $periodStart) ->where(fn ($query) => $query->whereNull('source.valid_until')->orWhereDate('source.valid_until', '>=', $periodEnd)) @@ -78,7 +80,7 @@ final class SimulationTaxParameterResolver 'rule.valid_from', 'rule.valid_until', 'rule.parameters', 'rule.rule_definition', 'rule.source_version_no', 'rule.source_sha256', 'source.title as source_title', 'source.diploma as source_diploma', 'source.article as source_article', - 'source.url as source_url', 'source.validation_status as source_validation_status', + 'source.url as source_url', 'source.validation_status as source_validation_status', 'source.fiscal_acceptance', 'source.version_no as source_current_version', 'source.valid_from as source_valid_from', 'source.valid_until as source_valid_until', ]) @@ -111,6 +113,7 @@ final class SimulationTaxParameterResolver 'article' => $row->source_article, 'url' => $row->source_url, 'validation_status' => $row->source_validation_status, + 'fiscal_acceptance' => is_array($row->fiscal_acceptance) ? $row->fiscal_acceptance : json_decode($row->fiscal_acceptance ?? 'null', true), 'version_no' => (int) $row->source_current_version, 'valid_from' => (string) $row->source_valid_from, 'valid_until' => $row->source_valid_until === null ? null : (string) $row->source_valid_until, @@ -151,7 +154,7 @@ final class SimulationTaxParameterResolver foreach ($versions as $version) { if (($version['component'] ?? null) !== $component || ($version['status'] ?? null) !== $requiredStatus - || ($version['source']['validation_status'] ?? null) !== 'validated' + || ! SimulationFiscalSourceAcceptance::eligible($version['source'] ?? [], $version['source_sha256'] ?? null) || (int) ($version['source_version_no'] ?? 0) !== (int) ($version['source']['version_no'] ?? -1) || ! $this->overlaps($version, $start, $end) || ! $this->covers($version['source'], $start) || ! $this->covers($version['source'], $end) @@ -249,7 +252,7 @@ final class SimulationTaxParameterResolver || ($version['component'] ?? null) !== $component || ($version['scope_key'] ?? null) !== $scopeKey || ! $this->covers($version, $date) - || ($version['source']['validation_status'] ?? null) !== 'validated' + || ! SimulationFiscalSourceAcceptance::eligible($version['source'] ?? [], $version['source_sha256'] ?? null) || (int) ($version['source_version_no'] ?? 0) !== (int) ($version['source']['version_no'] ?? -1) || ! $this->covers($version['source'], $date) || ! preg_match('/^[a-f0-9]{64}$/D', (string) ($version['source_sha256'] ?? ''))) { diff --git a/app/Services/SimulationTaxRuleGovernance.php b/app/Services/SimulationTaxRuleGovernance.php index 5bc0f4aa..355b2d60 100644 --- a/app/Services/SimulationTaxRuleGovernance.php +++ b/app/Services/SimulationTaxRuleGovernance.php @@ -80,15 +80,12 @@ final class SimulationTaxRuleGovernance break; case 'review': case 'reject': - abort_unless((int) $rule->author_id !== $actor->id, 403, 'O autor não pode rever a própria regra.'); abort_unless($rule->status === 'submitted', 422, 'A regra tem de estar submetida para revisão.'); $this->sourceStillValid($rule); $next += ['status' => $action === 'review' ? 'reviewed' : 'rejected', 'reviewer_id' => $actor->id, 'review_evidence' => $evidence, 'reviewed_at' => now()]; break; case 'approve': - abort_unless((int) $rule->author_id !== $actor->id && (int) $rule->reviewer_id !== $actor->id, 403, - 'Autor e revisor não podem aprovar a mesma regra.'); abort_unless($rule->status === 'reviewed', 422, 'A regra tem de estar revista antes da aprovação.'); $this->sourceStillValid($rule); $next += ['status' => 'approved', 'approver_id' => $actor->id, @@ -96,9 +93,6 @@ final class SimulationTaxRuleGovernance break; case 'publish': abort_unless($rule->status === 'approved', 422, 'A regra tem de estar aprovada antes da publicação.'); - abort_unless((int) $rule->author_id !== $actor->id && (int) $rule->reviewer_id !== $actor->id - && (int) $rule->approver_id !== $actor->id, 403, - 'Autor, revisor e aprovador não podem publicar a mesma regra.'); $this->sourceStillValid($rule); $this->assertNoPublishedOverlap($rule); $next += ['status' => 'published', 'publisher_id' => $actor->id, @@ -157,7 +151,11 @@ final class SimulationTaxRuleGovernance $source = InvestmentLegalSource::query()->whereKey($data['legal_source_id'])->firstOrFail(); abort_unless(preg_match('/^[a-f0-9]{64}$/D', (string) $data['source_sha256']) === 1, 422, 'Indique o SHA-256 do exemplar oficial efectivamente revisto.'); - abort_unless($source->validation_status === 'validated', 422, 'A fonte jurídica ainda não foi validada.'); + abort_unless(SimulationFiscalSourceAcceptance::eligible($source->attributesToArray(), $data['source_sha256']), 422, + 'A fonte ainda não está aceite para este cadastro fiscal.'); + abort_if(is_array($source->fiscal_acceptance) + && ($source->fiscal_acceptance['sha256'] ?? null) !== $data['source_sha256'], 422, + 'O hash da regra deve corresponder ao exemplar registado na aceitação da fonte fiscal.'); abort_unless($source->valid_from <= $data['valid_from'] && ($source->valid_until === null || ($data['valid_until'] !== null && $source->valid_until >= $data['valid_until'])) && ($data['valid_until'] !== null || $source->valid_until === null), 422, @@ -169,7 +167,8 @@ final class SimulationTaxRuleGovernance private function sourceStillValid(SimulationTaxRuleVersion $rule): void { $source = InvestmentLegalSource::query()->lockForUpdate()->find($rule->legal_source_id); - abort_unless($source !== null && $source->validation_status === 'validated' + abort_unless($source !== null && SimulationFiscalSourceAcceptance::eligible($source->attributesToArray(), $rule->source_sha256) + && (! is_array($source->fiscal_acceptance) || ($source->fiscal_acceptance['sha256'] ?? null) === $rule->source_sha256) && (int) $source->version_no === (int) $rule->source_version_no && $source->valid_from <= $rule->valid_from->format('Y-m-d') && ($source->valid_until === null || ($rule->valid_until !== null && $source->valid_until >= $rule->valid_until->format('Y-m-d'))) diff --git a/config/simulation-tax-governance.php b/config/simulation-tax-governance.php index 9d59f983..7b5d6fa9 100644 --- a/config/simulation-tax-governance.php +++ b/config/simulation-tax-governance.php @@ -1,8 +1,9 @@ array_values(array_filter(array_map('strtolower', array_map('trim', explode(',', (string) env('INVESTMOZ_TAX_RULE_EDITORS', '')))))), 'reviewers' => array_values(array_filter(array_map('strtolower', array_map('trim', explode(',', (string) env('INVESTMOZ_TAX_RULE_REVIEWERS', '')))))), 'approvers' => array_values(array_filter(array_map('strtolower', array_map('trim', explode(',', (string) env('INVESTMOZ_TAX_RULE_APPROVERS', '')))))), diff --git a/database/migrations/2026_10_21_130000_correct_fiscal_maintenance_actor_constraints.php b/database/migrations/2026_10_21_130000_correct_fiscal_maintenance_actor_constraints.php new file mode 100644 index 00000000..78498639 --- /dev/null +++ b/database/migrations/2026_10_21_130000_correct_fiscal_maintenance_actor_constraints.php @@ -0,0 +1,68 @@ +=20)), + ADD CONSTRAINT tax_rule_approval_check CHECK(status NOT IN ('approved','published','suspended','archived') OR (approver_id IS NOT NULL AND approved_at IS NOT NULL AND length(trim(approval_evidence))>=20)), + ADD CONSTRAINT tax_rule_publication_check CHECK(status NOT IN ('published','suspended','archived') OR (publisher_id IS NOT NULL AND published_at IS NOT NULL AND length(trim(publication_evidence))>=20))"); + + DB::statement('ALTER TABLE investment_legal_sources ADD COLUMN fiscal_acceptance jsonb NULL'); + DB::statement("ALTER TABLE investment_legal_sources ADD CONSTRAINT investment_legal_source_fiscal_acceptance_check CHECK ( + fiscal_acceptance IS NULL OR COALESCE(( + fiscal_acceptance->>'scope'='fiscal_parameters' AND + fiscal_acceptance->>'basis'='Tempo-official-sources-20261008' AND + fiscal_acceptance->>'responsible_user_id'=owner_id::text AND + fiscal_acceptance->>'sha256' ~ '^[a-f0-9]{64}$' AND + fiscal_acceptance->>'legal_opinion'='false' AND fiscal_acceptance->>'independent_review'='false' AND + length(trim(fiscal_acceptance->>'citation'))>=10 AND + length(trim(fiscal_acceptance->>'official_origin'))>=10 + ), false))"); + DB::statement('DROP POLICY simulation_tax_rule_versions_public_read ON simulation_tax_rule_versions'); + DB::statement("CREATE POLICY simulation_tax_rule_versions_public_read ON simulation_tax_rule_versions FOR SELECT USING ( + status='published' AND EXISTS (SELECT 1 FROM investment_legal_sources source + WHERE source.id=simulation_tax_rule_versions.legal_source_id + AND (source.validation_status='validated' OR (source.validation_status='pending' + AND source.fiscal_acceptance->>'scope'='fiscal_parameters' + AND source.fiscal_acceptance->>'basis'='Tempo-official-sources-20261008' + AND source.fiscal_acceptance->>'sha256'=simulation_tax_rule_versions.source_sha256)) + AND source.version_no=simulation_tax_rule_versions.source_version_no + AND source.valid_from<=simulation_tax_rule_versions.valid_from + AND (source.valid_until IS NULL OR (simulation_tax_rule_versions.valid_until IS NOT NULL AND source.valid_until>=simulation_tax_rule_versions.valid_until)) + AND (simulation_tax_rule_versions.valid_until IS NOT NULL OR source.valid_until IS NULL)))"); + + } + + public function down(): void + { + if (! IsolatedQaDatabase::allowsSchemaChanges()) { + throw new LogicException('Reversão permitida apenas em QA isolado.'); + } + $used = DB::table('investment_legal_sources')->whereNotNull('fiscal_acceptance')->exists() + || DB::table('simulation_tax_rule_versions')->whereRaw('reviewer_id=author_id OR approver_id=author_id OR approver_id=reviewer_id OR publisher_id=author_id OR publisher_id=reviewer_id OR publisher_id=approver_id')->exists(); + if ($used) { + throw new LogicException('Reversão recusada: conservar os cadastros e históricos legítimos; desactivar novas execuções em vez de apagar registos.'); + } + DB::statement('ALTER TABLE simulation_tax_rule_versions DROP CONSTRAINT tax_rule_stage_evidence_check, DROP CONSTRAINT tax_rule_approval_check, DROP CONSTRAINT tax_rule_publication_check'); + DB::statement("ALTER TABLE simulation_tax_rule_versions + ADD CONSTRAINT tax_rule_stage_evidence_check CHECK(status NOT IN ('reviewed','rejected','approved','published','suspended','archived') OR (reviewer_id IS NOT NULL AND reviewer_id<>author_id AND submitted_at IS NOT NULL AND reviewed_at IS NOT NULL AND length(trim(review_evidence))>=20)), + ADD CONSTRAINT tax_rule_approval_check CHECK(status NOT IN ('approved','published','suspended','archived') OR (approver_id IS NOT NULL AND approver_id<>author_id AND approver_id<>reviewer_id AND approved_at IS NOT NULL AND length(trim(approval_evidence))>=20)), + ADD CONSTRAINT tax_rule_publication_check CHECK(status NOT IN ('published','suspended','archived') OR (publisher_id IS NOT NULL AND publisher_id<>author_id AND publisher_id<>reviewer_id AND publisher_id<>approver_id AND published_at IS NOT NULL AND length(trim(publication_evidence))>=20))"); + DB::statement('DROP POLICY simulation_tax_rule_versions_public_read ON simulation_tax_rule_versions'); + (require __DIR__.'/2026_10_03_190000_allow_public_read_of_published_tax_parameters.php')->up(); + DB::statement('ALTER TABLE investment_legal_sources DROP CONSTRAINT investment_legal_source_fiscal_acceptance_check'); + DB::statement('ALTER TABLE investment_legal_sources DROP COLUMN fiscal_acceptance'); + } +}; 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 6883d437..fb3e389b 100644 --- a/docs/investmoz/simulations/ficha-IVA-GERAL-BASE-LIQUIDA-v1.md +++ b/docs/investmoz/simulations/ficha-IVA-GERAL-BASE-LIQUIDA-v1.md @@ -87,6 +87,8 @@ As15–26h anteriores eram condicionais e incluíam o trabalho de cadastro e int ## Revisão consolidada de83d2f97d +**Controlo de manutenção corrigido emQA,21/10/2026:** por decisão expressa da Tempo, a mesma administração autorizada pode cadastrar a fonte e concluir as etapas da regra, comMFA, responsável, evidência e histórico. A obrigação técnica de quatro pessoas não tinha fundamento aprovado identificado para este cadastro fiscal. Foi retirada apenas deste fluxo; o novo fundamento de aceitação da fonte oficial não é parecer jurídico nem revisão independente. Âmbito, exclusões, pressupostos e identificação do resultado como estimativa sobre base declarada mantêm-se. O manual consolidado contém a origem da condição, as6provas dirigidas/86asserções e o procedimento actualizado; não se reensaiou o cálculo nem se publicou ou activou a regra real nesta correcçãoQA. + **Actualização técnica de21/10/2026 — delta2ced7b79 implementado em QA:** o consumidor por versão publicada está ligado ao simulador normal, com activação desligada por omissão, selecção da regra no servidor e gravação/recuperação/relatório pelo histórico existente. O mesmo manual regista a demonstração pelo Edge, indeterminação sem declaração de âmbito, repetição sem duplicação, suspensão antes do processamento da fila, conclusão com versão congelada e recusa de novas execuções. Oito testes dirigidos/75asserções e dois testes frontend novos passaram, além deTypeScript/build/Pint. Mantêm-se integralmente o âmbito2026, exclusões, fontes e precisão desta ficha; não se repete a fundamentação jurídica nem se lhe atribui validação da Tempo. O procedimento único foi actualizado para integração, publicação da versão real e activação, decisões ainda não executadas na homologação. A fixture publicada exclusivamente no clone QA foi suspensa após o ensaio; os históricos foram preservados. As notas abaixo sobre ausência do consumidor descrevem o estado anterior à implementação deste delta. **Disponibilização no simulador existente:** a proposta única «Consumidor IVA no simulador da homologação», no mesmo manual, delimita o código ainda necessário e as três operações distintas: integração com consumidor desligado, publicação da versão pela governação existente e activação da ligação à versão autorizada. O percurso QA aceite não equivale a esse consumidor público. Mantêm-se âmbito2026, exclusões, precisão exacta e históricos; suspensão impede novas admissões e preserva execuções já admitidas. Esta proposta não executa nenhuma das três operações nem amplia a cobertura fiscal. diff --git a/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json b/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json index e6764dc6..11ebdbf4 100644 --- a/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json +++ b/docs/investmoz/simulations/integrated-review-continuation-20261020-receipt.json @@ -588,5 +588,156 @@ "user_local_file_review": false, "procedure": "Existing manual: Procedimento unico das tres operacoes - agora executavel apos decisao", "proofs_private": true + }, + "homologation_authorised_rollout_20261021": { + "authorisation": "Tempo authorised implementation of candidate 226fd308 and the three operations in the current conversation", + "recorded_utc": "2026-10-21T10:08:57.106309+00:00", + "candidate": "226fd308dd45d2805ef4678b20ca4398d7fef6c6", + "previous_main": "13f68f1bd8676d5ed418876a562af259e3887ba8", + "integration": "fast-forward and push origin/main", + "integration_completed": true, + "deployment_verified": { + "commit": "226fd308dd45d2805ef4678b20ca4398d7fef6c6", + "branch": "main", + "built_at": "2026-10-21T10:07:12.300278+00:00", + "manifest_sha256": "d524c957dc95e92fcb6eb0d802b9df845743466bd51e398a485bd5ba3d7e3d8f" + }, + "consumer_availability_verified": { + "data": { + "enabled": false, + "coverage_start": "2026-01-01", + "coverage_end": "2026-12-31", + "message": "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." + } + }, + "intended_real_rule": { + "rule_code": "IVA-GERAL-BASE-LIQUIDA", + "rule_version": 1, + "documentary_sheet": "v1.2", + "source_sha256": "ea1b0dc6f5cf6e595f6d9a19e7042f398999e0087caef1785f6e8fac571b5f53", + "destination_id": null + }, + "rule_publication_completed": false, + "consumer_activation_completed": false, + "qa_fixture_imported": false, + "data_writes_executed": false, + "remaining_dependency": "SSH authentication resolved and backup/restoration confirmed. Unsupported four-person fiscal control corrected and verified in QA following explicit Tempo instruction. Corrective code/schema not applied to homologation; real rule publication and consumer activation remain unexecuted.", + "accepted_tests_repeated": false, + "credentials_recorded": false, + "juridical_validation_attributed_to_user": false, + "prior_dependency_preserved": "Previous destination SSH session closed; noninteractive authentication failed; user asked to reopen the existing authenticated deployment session. Read-only preflight prepared, not submitted. Backup/restoration and existing application actors must be checked before destination fiscal data writes." + }, + "fiscal_maintenance_actor_correction_20261021": { + "status": "QA control correction completed; not integrated or applied to homologation", + "authority": "Explicit Tempo instruction: no four distinct persons required for fiscal parameter maintenance; reuse existing authorised administration", + "origin": { + "commit": "e65cb269", + "date": "2026-10-06", + "classification": "implementation choice; no express applicable Tempo approval identified", + "files": [ + "app/Services/SimulationTaxRuleGovernance.php", + "database/migrations/2026_10_03_170000_create_simulation_tax_rule_governance.php", + "docs/investmoz/simulator-evolution-progress.md" + ] + }, + "designated_destination_responsible": "nala@tempo.co.mz", + "existing_account_read_only": { + "id": 15, + "verified": true, + "mfa_enrolled": true, + "existing_superuser_profile": true, + "no_account_or_profile_created": true + }, + "reuses": [ + "existing verified administration", + "MFA and existing scoped profiles", + "rule states versions sources and immutable events" + ], + "removed": "Mandatory distinct actor identities in fiscal maintenance only", + "fiscal_official_source_curation": { + "endpoint": "POST /api/v2/investment/admin/tax-rule-sources", + "same_authorised_person": true, + "required": [ + "official_origin", + "citation", + "sha256" + ], + "legal_opinion": false, + "independent_review": false, + "other_source_workflows_unchanged": true, + "general_validation_status": "pending", + "not_automatically_usable_in_other_workflows": true, + "source_eligibility_only": "Existing fiscal governance, resolver and published-rule SELECT policy recognise explicit fiscal acceptance and matching hash/version; formula/evaluator unchanged" + }, + "schema_delta": { + "migration": "2026_10_21_130000_correct_fiscal_maintenance_actor_constraints.php", + "qa_guard": true, + "active_application": false, + "unrelated_constraints_unchanged": true, + "reversal_preserves_dependent_records": true, + "original_source_independent_validation_check_preserved": true + }, + "tests": { + "database": "simulator_fiscal_checks_20261021_test", + "tests": 6, + "assertions": 86, + "result": "passed", + "transactions_rolled_back": true, + "calculator_tests_repeated": false, + "simulation_execution_count_preserved": true, + "exporter_tests_repeated": false, + "non_superuser_published_rule_visibility": true + }, + "destination_backup_restore": { + "utc": "2026-10-21T10:18:00.737890+00:00", + "source_database": "teste_investidor", + "served_commit": "226fd308dd45d2805ef4678b20ca4398d7fef6c6", + "backup_directory": "/var/backups/investmoz/iva-rollout-20261021_101750", + "dump_sha256": "4e972c028fbe70c347c518ba86fe7e293093b7c81278ac4d64a077bf3e5325b7", + "dump_bytes": 4584693, + "environment_backup_sha256": "537eff52f0f28b83ccc56bf9a9b85c2d8fca1da2f678c30b929b46c67c09491b", + "environment_path": "/var/www/html/teste-investidor.tempo.co.mz/.env", + "restore_database": "iva_rollout_20261021_101750_test", + "restore_database_public_connect": false, + "restore_equal": true, + "table_count": 171, + "method": "Exported repeatable-read snapshot, custom pg_dump, isolated restore, all public table counts and canonical row hashes compared", + "active_data_writes": false, + "qa_fixture_imported": false + }, + "served_revision_unchanged": "226fd308dd45d2805ef4678b20ca4398d7fef6c6", + "consumer_enabled": false, + "real_rule_published": false, + "synthetic_destination_actors_created": false, + "user_juridical_validation": false, + "user_local_file_review": false, + "persistent_qa_schema_proof": { + "qa_database": "simulator_fiscal_delta_20261021_test", + "preserved_tables": { + "simulation_tax_rule_versions": { + "rows": 2, + "hash": "5ec81670fd48a9ae4393552b06a37cfb" + }, + "simulation_tax_rule_events": { + "rows": 7, + "hash": "0bd8ab5a124751729c7ff90e7e799086" + }, + "simulation_results": { + "rows": 11, + "hash": "09cdaa0c5acdbc7e17739c98d9b3ba4e" + }, + "simulation_reports": { + "rows": 11, + "hash": "127ee229e4baca978d3ad8c699b59f19" + }, + "investment_legal_sources": { + "rows": 8, + "hash": "473d504fc4e9a03d795fde4be16deb70" + } + }, + "same_rows_and_hashes": true, + "fiscal_acceptance_column": true, + "active_database_changed": false + } } } diff --git a/docs/investmoz/simulations/rule-governance.md b/docs/investmoz/simulations/rule-governance.md index cc05f2db..19687ba8 100644 --- a/docs/investmoz/simulations/rule-governance.md +++ b/docs/investmoz/simulations/rule-governance.md @@ -1,5 +1,15 @@ # Governação de versões de regras fiscais +## Correcção de controlo — decisão expressa da Tempo,21/10/2026 + +A manutenção dos parâmetros fiscais reutiliza a administração já autorizada, com autenticação/MFA, fonte, versão, responsável e histórico. Uma mesma pessoa pode concluir `submit` → `review` → `approve` → `publish`; esses estados preservam o percurso técnico e os eventos, sem afirmar independência pessoal ou parecer jurídico. As quatro listas específicas de permissões permanecem opcionais. A utilização do simulador pelo investidor conserva as permissões e os limites próprios, separados da manutenção. + +A obrigação de actores distintos foi encontrada no serviço fiscal e nosCHECKs da migration de03/10/2026, incorporados em `e65cb269` (06/10/2026), e descrita como implementação em `simulator-evolution-progress.md`. Não foi localizada uma decisão expressa da Tempo ou requisito aplicável que a imponha ao cadastro fiscal. A origem identificada é escolha de implementação; a autoria ou integração do commit não demonstra aprovação dessa regra organizacional. A decisão actual da Tempo manda removê-la neste fluxo, sem alterar a independência acordada nas oportunidades, perfis ou outros processos. + +A correcçãoQA retira apenas as comparações de identidade entre autor/revisor/aprovador/publicador dos controlos fiscais; mantém campos obrigatórios, evidência, datas, etapas, versão optimista, fonte, bloqueios, RLS e eventos imutáveis. Para cadastrar uma fonte oficial neste percurso existe `POST /api/v2/investment/admin/tax-rule-sources`, sob a mesma administração autenticada. Recebe os metadados existentes mais `sha256`, `citation` e `official_origin`; regista a aceitação da fonte oficial segundo a decisão Tempo de08/10/2026, o responsável real e a indicação explícita de que não é parecer jurídico nem revisão independente. O hash da regra tem de coincidir com o exemplar cadastrado. A fonte permanece pending no estado geral: a aceitação fiscal não a valida para outros módulos. A projecção pública e o resolvedor fiscal reconhecem esse fundamento e o hash correspondente, sem alterar a fórmula. A fonte não activa nem publica uma regra automaticamente. A validação normal de fontes para os outros processos mantém o seu controlo; não se alteraram os workflows de oportunidades/zonas/perfis. + +Migration de correcção: `2026_10_21_130000_correct_fiscal_maintenance_actor_constraints.php`, protegida contra execução fora deQA isolado. Acrescenta metadados de aceitação fiscal à fonte e corrige osCHECKs que impediam o actor único. Foi ensaiada apenas emQA, com6testes/86asserções do controlo alterado, sem cálculo, build ou repetição dos testes aritméticos/exportadores. O manual consolidado regista provas e procedimento actualizado das três operações. Os estados históricos abaixo descrevem o incremento original e não reabrem as provas já aceites da candidata226fd308. + ## Âmbito deste incremento Foi criado um fluxo administrativo para registar uma versão declarativa de regra e a sua referência jurídica. O fluxo existe separado do motor de cálculo. Nenhum endpoint de simulação consulta `simulation_tax_rule_versions`; `published` significa apenas publicada no registo interno de governação, não activa no produto nem aprovada para aconselhamento fiscal. diff --git a/docs/investmoz/unification/traceability-and-manual.md b/docs/investmoz/unification/traceability-and-manual.md index 943fa454..0b9d17c3 100644 --- a/docs/investmoz/unification/traceability-and-manual.md +++ b/docs/investmoz/unification/traceability-and-manual.md @@ -1904,3 +1904,42 @@ Provas privadas em `outputs/simulator-continuation-20261020`: `published-consume **Suspender/desactivar:** repor o interruptor emfalse e aplicar a configuração impede novas admissões; para retirar a elegibilidade da própria regra usar a transição existente `suspend`, com versão corrente/evidência e actor autorizado. Pedidos já admitidos concluem pelo snapshot; repetição idempotente recupera-os. Histórico/relatório/exportação continuam acessíveis sem novo cálculo, publicação ou activação. Retoma segue uma versão publicada elegível e activação explícita, nuncaSQL directo sobre o estado suspenso. A janela2026 mantém-se um limite de cobertura, não data de caducidade legal. **Saldo:** ligação funcional do primeiro consumidor fechada em QA, pronta para revisão e decisão sobre as três operações; sem outra preparação genérica identificada. AUD-SIM-INT02 permanece parcial para cobertura fiscal integral. As estimativas anteriores não constituem horas ainda por executar deste delta já concluído. Main local continua em13f68f1b; sem merge/push, publicação real ou activação na homologação. A Tempo não conferiu os ficheiros locais nem validou juridicamente as fontes nesta execução. + +### Disponibilização autorizada pela Tempo —21/10/2026, execução em curso + +A Tempo autorizou a implementação após o fecho da candidata226fd308 e a identificação das três operações. A integração foi executada por avanço directo de13f68f1b para `226fd308dd45d2805ef4678b20ca4398d7fef6c6`, seguida de envio aorigin/main. A disponibilização automática foi conferida através da camada HTTP autenticada: `/build/revision.json` serve essa revisão emmain, build de21/10/2026 às10:07:12.300278UTC, manifest SHA256 `d524c957dc95e92fcb6eb0d802b9df845743466bd51e398a485bd5ba3d7e3d8f`. `/api/v1/fiscal-consumer` responde200 com `enabled:false` e cobertura2026. A primeira leitura sem autenticação da camada HTTP foi recusada; a conferência seguinte utilizou o acesso já disponibilizado, sem divulgar credenciais. Não se repetiram provas aceites. + +**Saldo das operações:** integração e disponibilização do código concluídas; publicação da versão real e activação ainda não executadas. A versão a cadastrar/publicar é `IVA-GERAL-BASE-LIQUIDA`, `rule_version:1`, da ficha documentalv1.2, vinculada ao exemplar da Lei22/2022 com hash `ea1b0dc6f5cf6e595f6d9a19e7042f398999e0087caef1785f6e8fac571b5f53`; oID real do destino ainda não foi obtido. Não usar a fixtureQA deversão2 ou os seus actores. A autorização de disponibilização não é apresentada como validação jurídica do utilizador. + +**Dependência operacional concreta:** a sessão SSH anterior terminou comfalha de ligação; autenticação não interactiva no destino foi recusada. Pediu-se a reabertura do mecanismo existente `outputs/investmoz-git-delivery-20261020/resume-git-session.py`, com autenticação exclusivamente na consola local. Está preparado um inventário remoto somente por leitura para conferir os papéis/actores existentes, fonte, versão e estado do consumidor. Ainda não foi submetido, por falta da sessão autenticada. Backup/restauro e acção autenticada da governação serão confirmados antes de escrever dados fiscais ou alterar a activação. Não há nova pendência de fundamentação, nova ronda QA ou pedido de autorização para as operações já autorizadas. + +Recibo privado da integração em `outputs/investmoz-iva-disponibilizacao-20261021/integration-receipt.json` no checkout principal; o recibo consolidado existente replica o estado. Nenhuma escrita fiscal, importação da fixture ou activação no destino foi executada nesta passagem. A continuação começa na autenticação remota e nas duas operações restantes, sem voltar a integrar ou preparar a candidata. + +### Correcção QA da manutenção fiscal por um responsável — decisão Tempo de21/10/2026 + +**Origem e decisão:** a obrigação de quatro pessoas foi localizada em `SimulationTaxRuleGovernance::transition` e nos trêsCHECKs de `2026_10_03_170000_create_simulation_tax_rule_governance.php`, incorporados por `e65cb269` (06/10/2026, «varias alteracoes»). A nota de `simulator-evolution-progress.md` descreve «autor distinto de revisor, aprovador e publicador» como implementação. Não foi encontrada no acervo consultado uma decisão expressa da Tempo ou um requisito aplicável que imponha essa separação ao cadastro fiscal. Classificação: **escolha de implementação**, não regra organizacional aprovada. A autoria do commit não equivale à aprovação da condição. A Tempo agora instruiu expressamente a sua remoção neste fluxo e indicou `nala@tempo.co.mz` como responsável. Não se criam quatro designações nem uma excepção artificial limitada à primeira regra; a administração autorizada pode manter os parâmetros fiscais, sem extensão à independência acordada noutros processos. + +**O que ficou corrigido em QA:** + +- A administração existente, incluindo o perfil administrativo já autorizado comMFA recente, pode cadastrar/manter parâmetros sem preencher quatro listas específicas. As listas existentes continuam disponíveis para atribuições específicas. Utilizadores comuns e perfis apenas de auditoria não adquirem manutenção fiscal; autenticação, verificação de email eMFA permanecem. O investidor continua a utilizar o simulador pelas permissões próprias de execução/consulta; a manutenção não é requisito de utilização. +- O mesmo responsável pode percorrer `draft` → `submitted` → `reviewed` → `approved` → `published`, conservando osID reais em cada etapa. Retiraram-se as comparações de identidade no serviço fiscal e nos respectivosCHECKs; mantiveram-se evidência, datas, estado, versão, coerência da fonte, unicidade/sobreposição, bloqueios, RLS e eventos imutáveis. Não há alegação de revisão independente quando o actor é o mesmo. +- Como o destino não tem a fonte deste lote cadastrada, o percurso fiscal permite registá-la em `POST /api/v2/investment/admin/tax-rule-sources`: metadados existentes, origem oficial identificada, citação eSHA256. Regista a aceitação oficial já decidida pela Tempo em08/10/2026, responsável e momento, com `legal_opinion:false` e `independent_review:false`. O estado geral da fonte permanece `pending`; o fundamento `fiscal_acceptance` permite a sua utilização apenas na governação/resolução fiscal. Assim, não fica automaticamente validada para zonas ou outros processos, nem certifica juridicamente uma operação ou atribui parecer à Tempo. A regra deve referenciar o mesmo hash e versão. Cadastro de fonte não publica regra nem activa consumidor. +- A mesma pessoa pode concluir essa curadoria fiscal sem inventar outro actor para «validar» a fonte oficial aceite. A condição normal da fonte fora deste percurso, as relações/validações de zonas, oportunidades e perfis mantêm-se. O novo fundamento explícito `fiscal_acceptance` fica na fonte e na auditoria; o endpoint normal de zonas não permite marcar esse fundamento. Não se alteraram os fluxos com independência já acordada. + +**Esquema:** `2026_10_21_130000_correct_fiscal_maintenance_actor_constraints.php` foi ensaiada apenas na base QA isolada. O delta é explícito: retirar desigualdades de identidade dos trêsCHECKs fiscais; acrescentar o fundamento de aceitação fiscal à fonte; registar a responsabilidade única na aceitação fiscal da fonte; ajustar apenas a projecção pública/resolução fiscal para reconhecer esse fundamento com hash e versão correspondentes. A condição normal de validação independente da fonte não foi removida e o seu estado continua pending. A guarda recusa execução da migration foraQA. Nenhuma constraint de oportunidades, zonas, perfis ou outra tabela foi modificada. A reversão só é permitida em QA sem registos dependentes; quando existirem cadastros legítimos, recusa apagá-los ou voltar a impor uma condição que os invalide. Desligar novas execuções preserva os históricos. + +**Verificação estritamente do controlo alterado:**6testes/86asserções passaram em `simulator_fiscal_checks_20261021_test`, em transacções revertidas. Cobrem administração existente por lista e por perfil/MFA recente, sem quatro designações; curadoria da fonte, cadastro e quatro etapas pelo mesmo actor; responsabilidade/hashes dos eventos; consulta e suspensão; consumidor ainda desligado; recusa de investidor, auditor, ausência/expiração deMFA, falta de evidência, hash/versão de fonte incoerente e versão optimista errada; imutabilidade; compatibilidade das constraints não fiscais; reversão vazia e recusa de reversão com dados dependentes; guarda contra alvo nãoQA. Não se executou uma simulação: a contagem de execuções foi preservada. Não houve testes novos de cálculo, decimal, exportadores, PDF, TypeScript ou build, pois esse código não mudou. Pint foi conferido nos ficheirosPHP do delta. Prova privada: `outputs/simulator-continuation-20261020/fiscal-maintenance-actor-tests.txt`. Os testes antigos de governação foram ajustados para retirar a expectativa de uma proibição agora expressamente revogada, preservando os casos de permissões, versões e histórico. + +O esquema foi depois aplicado, apenas pelo caminho desta migration, ao clone persistente `simulator_fiscal_delta_20261021_test`, mantendo a guarda de isolamento. Prova por leitura antes/depois:2versões de regra,7eventos de governação,11resultados,11relatórios e8fontes com as mesmas contagens e hashes de conteúdo. Na comparação da fonte exclui-se apenas a coluna nova, que ficaNULL nos registos anteriores; nenhum conteúdo ou snapshot histórico foi reescrito. Prova privada `fiscal-maintenance-schema-proof.json` na mesma pasta. O clone QA está, assim, com o controlo aplicado para revisão; a base activa e os seus actores não foram alterados. + +**Estado real do destino:** a dificuldade de introduzir o comandoSSH foi resolvida abrindo directamente a consola habitual; a autenticação foi feita pelo utilizador e a sessão ficou pronta. A leitura do vhost identificou o directório servido `/var/www/html/teste-investidor.tempo.co.mz/current`, que aponta para a release226fd308; a cópia anterior conservada na raiz não é o consumidor servido. Foi confirmado por leitura que `nala@tempo.co.mz` existe (ID15), email verificado, MFA configurado e perfil de superutilizador existente; não foi criada conta ou alterado perfil. Não se confunde matrículaMFA com sessão autenticada recente: as acções de cadastro continuam a exigir a autenticação normal do responsável. + +O backup/restauro anterior à escrita ficou comprovado em21/10/2026 às10:18:00UTC: cópia consistente por snapshot de `teste_investidor`, arquivo privado `/var/backups/investmoz/iva-rollout-20261021_101750/database.dump`, SHA256 `4e972c028fbe70c347c518ba86fe7e293093b7c81278ac4d64a077bf3e5325b7`,4.584.693bytes; restauro isolado em `iva_rollout_20261021_101750_test`, acesso público à base revogado. Contagens e hashes canónicos das171tabelas coincidiram. A configuração anterior ficou protegida na mesma pasta. Isto comprova backup/restauro, sem ensaiar ou escrever os parâmetros no activo. A primeira validação local do cabeçalho do arquivo usava texto inadequado e foi corrigida para o formatoPGDMP; a segunda operação completou o restauro e a comparação. Não é falha do simulador. + +#### Procedimento actualizado das três operações, sem quatro responsáveis obrigatórios + +1. **Código e controlo de esquema:** a revisão226fd308 já está integrada e servida. A presente correcção de governação fica na branch QA para revisão; não foi enviada amain. O delta inclui o esquema acima, cuja aplicação no destino deve acompanhar a referência exacta do código, por operação técnica autenticada e combackup/restauro actualizado conforme o estado nesse momento. A migration protegida é o ensaioQA, não um comando a executar contra a base activa. Conservar os históricos e o consumidor desligado durante a aplicação do controlo; não usar o deploy para cadastrar/publicar dados. Não voltar a integrar226fd308 nem repetir as provas de cálculo. +2. **Fonte e versão fundamentada:** o responsável real indicado pela Tempo autentica-se normalmente, comMFA. Reutiliza a fonte aceite se já existir; caso contrário, cadastra-a pelo endpoint fiscal acima, com exemplar/hash/citação reais. Cadastra `IVA-GERAL-BASE-LIQUIDA`, `rule_version:1`, conforme fichav1.2 e âmbito/exclusões2026. A mesma conta autorizada conclui `submit` → `review` → `approve` → `publish`, sempre com `version_no` corrente e evidência fiel: manutenção técnica autorizada, não parecer jurídico nem revisão independente. Registar oID real; não usar a fixtureQA deversão2 nem contas sintéticas. Não é preciso criar quatro designações. +3. **Activação do consumidor:** permanece separada da publicação. Aplicar a ligação aoID e `rule_version` reais autorizados e o interruptor de activação pelo procedimento já definido, conservando cálculo sobre base declarada como estimativa, sem apuramento final do imposto a pagar. Conferir somente a revisão/regra efectivamente servidas e o delta no destino. Desactivar o consumidor ou suspender a regra recusa novas admissões, preservando as já admitidas, históricos e relatórios. + +**Saldo actual:** controlo corrigido e verificado emQA, fontes e funcionamento do cálculo não reabertos; sem nova publicação ou activação na homologação. A referência servida continua226fd308 e o consumidor desligado. A correcção não foi aplicada ao esquema activo. A indicação do responsável e a autorização operacional não são tratadas como conferência de ficheiros locais ou validação jurídica da Tempo. Esta actualização substitui a indicação anterior de que a simples falta de quatro listas configuradas bloqueava obrigatoriamente a disponibilização. diff --git a/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv b/docs/investmoz/v2/auditoria-portal/matriz-requisitos.csv index c30cb7d2..442f82c2 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,Parcial: primeiro consumidor IVA funcional no simulador normal em QA; cobertura fiscal integral e disponibilizacao na homologacao permanecem separadas.,QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P0,,,,,,,,,,,,,,"Ligacao IVA do lote 2026 demonstrada em QA. Decisoes pendentes distintas: integrar candidata, publicar versao real e activar consumidor na homologacao. Demais componentes fiscais fora deste recorte.",,,"Tempo decide integracao do codigo, publicacao da versao real e activacao do consumidor conforme procedimento unico; nenhuma executada nesta passagem.",Ligacao de consumidor publicada concluida em QA; decisoes operacionais separadas.,Sem saldo de implementacao identificado para este delta QA; nao reutilizar estimativas anteriores como horas ainda em falta; nao estima cobertura fiscal integral.," | [Leitura SSH 21/10/2026 08:17 Maputo] Zero regras em simulation_tax_rule_versions na homologação, incluindo todos os estados por leitura administrativa sem ocultação RLS. Seis referências em investment_legal_sources; não são parâmetros executáveis. QA também tinha zero regras. BEGIN READ ONLY/SELECT/ROLLBACK; zero escritas. Prova homologation-fiscal-readonly.json. O git HEAD registado é do checkout consultado, não certifica a revisão do build servido. A ausência de cadastro não é ausência de fontes oficiais nem reabre a aceitação da Tempo de 08/10. Não implementa a ligação funcional ao motor fiscal. | [Saldo consolidado21/10/2026] Parcial. Leitura remota concluída: zero regras de todos os estados na homologação, tal como QA; seis referências jurídicas não são parâmetros executáveis. Falta cadastro fundamentado por componente/âmbito/período e ligação funcional ao motor fiscal. Fontes e análises existentes reutilizáveis; aceitação oficial da Tempo de08/10 preservada. Não activar taxas nem importar/publicar por esta reconciliação. Ver saldo dos oito itens no manual existente; sem fecho global por esta reconciliação. | [Candidata21/10/2026] Diagnóstico fiscal separado no manual existente: fontes/analises já disponíveis; zero parâmetros executáveis na QA e homologação por leitura de todos os estados; governação/resolvedor temporal/DSL existentes; RunSimulation ainda aritmética personalizada sem consumir regras fiscais. Saldo: cadastro fundamentado, orquestração, contrato de resultado/histórico e ligação ao dossier/relatório, com casos por componente/período. Nenhuma regra criada nem activada; implementação fiscal integral aberta e excluída das1–2h de exportações. | [Proposta21/10/2026] Primeiro lote proposto: uma regra IVA geral sobre base liquida/MZN, cobertura demonstrada2026 sem inventar caducidade legal. Reutilizar fontes/analises/governacao/resolvedor/avaliador existentes; arredondamento e ambito aplicavel explicitamente por fundamentar antes de publicar. Cadastro4-8h e codigo/aceitacao12-20h, total16-28h condicionado ao acervo, separado das1-2h de exportacao. Criterios e exclusoes no manual existente. Nenhuma regra cadastrada/activada; nao e implementacao fiscal integral. | [QA IVA v1.2 21/10/2026] Orientacao posterior Tempo: calcular com precisao decimal exacta sem arredondamento automatico, separando apresentacao monetaria; arredondamento generico deixa de ser bloqueio deste lote. Cadastro draft versionado em clone QA; resolvedor/avaliador existentes; proveniencia, historico e relatorio imutavel ligados. 5 feature (51 assercoes), 7 unit (25), 3 frontend; types/build/Pint aprovados. Contexto declarado; nao e saldo a pagar. Zero regras publicadas; sem main/activacao. Recorte QA autorizado verificado (0h engenharia identificada restante); produto fiscal integral continua parcial. Limites e provas no mesmo manual e ficha v1.2; sem atribuicao de validacao juridica ao utilizador. | [Revisao 83d2f97d] Ambito/periodo/exclusoes e procedimentos de integracao, publicacao, activacao e suspensao consolidados no mesmo manual. Lacuna concreta de ecra QA fechada: entradas 1000.03/MZN/2026-10-21; calculo exacto 160.0048; gravacao fa6d17f2-5f47-4cb6-b8cb-40850c12505f; recarga; recuperacao do mesmo ID; relatorio d13e2ad5-3ba8-4d01-9d30-5815496da774 com mesmo valor/proveniencia e hash igual ao resultado. Uma execucao persistida, draft e zero regras publicadas. 1 teste novo de rota/17 assercoes, TypeScript/build e browser aprovados; restantes provas reutilizadas. Interrupcao inicial da ferramenta resolvida, sem atribuicao de falha ao simulador. Revisao do recorte demonstrada; sem nova preparacao generica identificada, main ou activacao; nao fecha aplicabilidade fiscal universal nem simulador integral. | 21/10/2026: proposta do consumidor IVA no simulador existente no manual: falta ligacao a regra publicada e activacao explicita. Integrar codigo desligado, publicar versao e activar consumidor sao tres operacoes distintas. Reutiliza permissoes, governanca, historico e provas aceites; sem ampliar cobertura ou executar estas operacoes. | 21/10/2026: implementado delta de 2ced7b79 no simulador normal QA, activacao desligada por omissao. 8 testes/75 assercoes, 2 testes frontend novos, TypeScript/build/Pint. Edge: indeterminacao, calculo, repeticao sem duplicar, fila admitida antes de suspensao concluida depois, nova execucao recusada e historicos/relatorios preservados. Mesmo manual e recibo consolidam provas e tres operacoes. Fixture isolada suspensa; sem merge ou publicacao/activacao na homologacao.",,20/10/2026,,,,,,,,,,,,,, +AUD-SIM-INT02,Review actual fiscal parameters sources periods and eligibility,Tempo request 20/10/2026; integrated simulator review; same manual,simulator and shared language,,,QA only,Verification pending; no parameter activation,"Parcial: consumidor IVA integrado e servido em 226fd308, desligado. Controlo de manutencao por mesma administracao autorizada corrigido em QA; publicacao real e activacao pendentes.",QA continuation verified; precise open inputs and cashflow visual check in same manual; 2-4 h remaining,,,P0,,,,,,,,,,,,,,Rever/aplicar delta de controlo fiscal e esquema ja ensaiado em QA; cadastrar/publicar versao real fundamentada e activar consumidor. Sem obrigacao de quatro pessoas. Demais componentes fiscais fora deste recorte.,,,Tempo indicou nala@tempo.co.mz e removeu expressamente a condicao de pessoas distintas na manutencao fiscal. Preservar as tres operacoes e as independencias aprovadas dos outros processos.,Ligacao de consumidor publicada concluida em QA; decisoes operacionais separadas.,Sem saldo de implementacao identificado para este delta QA; nao reutilizar estimativas anteriores como horas ainda em falta; nao estima cobertura fiscal integral.," | [Leitura SSH 21/10/2026 08:17 Maputo] Zero regras em simulation_tax_rule_versions na homologação, incluindo todos os estados por leitura administrativa sem ocultação RLS. Seis referências em investment_legal_sources; não são parâmetros executáveis. QA também tinha zero regras. BEGIN READ ONLY/SELECT/ROLLBACK; zero escritas. Prova homologation-fiscal-readonly.json. O git HEAD registado é do checkout consultado, não certifica a revisão do build servido. A ausência de cadastro não é ausência de fontes oficiais nem reabre a aceitação da Tempo de 08/10. Não implementa a ligação funcional ao motor fiscal. | [Saldo consolidado21/10/2026] Parcial. Leitura remota concluída: zero regras de todos os estados na homologação, tal como QA; seis referências jurídicas não são parâmetros executáveis. Falta cadastro fundamentado por componente/âmbito/período e ligação funcional ao motor fiscal. Fontes e análises existentes reutilizáveis; aceitação oficial da Tempo de08/10 preservada. Não activar taxas nem importar/publicar por esta reconciliação. Ver saldo dos oito itens no manual existente; sem fecho global por esta reconciliação. | [Candidata21/10/2026] Diagnóstico fiscal separado no manual existente: fontes/analises já disponíveis; zero parâmetros executáveis na QA e homologação por leitura de todos os estados; governação/resolvedor temporal/DSL existentes; RunSimulation ainda aritmética personalizada sem consumir regras fiscais. Saldo: cadastro fundamentado, orquestração, contrato de resultado/histórico e ligação ao dossier/relatório, com casos por componente/período. Nenhuma regra criada nem activada; implementação fiscal integral aberta e excluída das1–2h de exportações. | [Proposta21/10/2026] Primeiro lote proposto: uma regra IVA geral sobre base liquida/MZN, cobertura demonstrada2026 sem inventar caducidade legal. Reutilizar fontes/analises/governacao/resolvedor/avaliador existentes; arredondamento e ambito aplicavel explicitamente por fundamentar antes de publicar. Cadastro4-8h e codigo/aceitacao12-20h, total16-28h condicionado ao acervo, separado das1-2h de exportacao. Criterios e exclusoes no manual existente. Nenhuma regra cadastrada/activada; nao e implementacao fiscal integral. | [QA IVA v1.2 21/10/2026] Orientacao posterior Tempo: calcular com precisao decimal exacta sem arredondamento automatico, separando apresentacao monetaria; arredondamento generico deixa de ser bloqueio deste lote. Cadastro draft versionado em clone QA; resolvedor/avaliador existentes; proveniencia, historico e relatorio imutavel ligados. 5 feature (51 assercoes), 7 unit (25), 3 frontend; types/build/Pint aprovados. Contexto declarado; nao e saldo a pagar. Zero regras publicadas; sem main/activacao. Recorte QA autorizado verificado (0h engenharia identificada restante); produto fiscal integral continua parcial. Limites e provas no mesmo manual e ficha v1.2; sem atribuicao de validacao juridica ao utilizador. | [Revisao 83d2f97d] Ambito/periodo/exclusoes e procedimentos de integracao, publicacao, activacao e suspensao consolidados no mesmo manual. Lacuna concreta de ecra QA fechada: entradas 1000.03/MZN/2026-10-21; calculo exacto 160.0048; gravacao fa6d17f2-5f47-4cb6-b8cb-40850c12505f; recarga; recuperacao do mesmo ID; relatorio d13e2ad5-3ba8-4d01-9d30-5815496da774 com mesmo valor/proveniencia e hash igual ao resultado. Uma execucao persistida, draft e zero regras publicadas. 1 teste novo de rota/17 assercoes, TypeScript/build e browser aprovados; restantes provas reutilizadas. Interrupcao inicial da ferramenta resolvida, sem atribuicao de falha ao simulador. Revisao do recorte demonstrada; sem nova preparacao generica identificada, main ou activacao; nao fecha aplicabilidade fiscal universal nem simulador integral. | 21/10/2026: proposta do consumidor IVA no simulador existente no manual: falta ligacao a regra publicada e activacao explicita. Integrar codigo desligado, publicar versao e activar consumidor sao tres operacoes distintas. Reutiliza permissoes, governanca, historico e provas aceites; sem ampliar cobertura ou executar estas operacoes. | 21/10/2026: implementado delta de 2ced7b79 no simulador normal QA, activacao desligada por omissao. 8 testes/75 assercoes, 2 testes frontend novos, TypeScript/build/Pint. Edge: indeterminacao, calculo, repeticao sem duplicar, fila admitida antes de suspensao concluida depois, nova execucao recusada e historicos/relatorios preservados. Mesmo manual e recibo consolidam provas e tres operacoes. Fixture isolada suspensa; sem merge ou publicacao/activacao na homologacao. | 21/10/2026: origem da condicao fiscal localizada em e65cb269/servico/CHECKs de 03/10 e nota de implementacao; sem decisao expressa aplicavel encontrada. Correccao QA: administracao existente, mesma pessoa, fonte oficial identificada e eventos preservados. 6 testes/86 assercoes apenas do controlo, sem calculos/exportadores; constraints de outras tabelas iguais. Esquema activo nao alterado; backup/restauro de 171 tabelas confirmado; sem fixture ou publicacao real.",,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/routes/investment-zones.php b/routes/investment-zones.php index 78b3752e..7186b37e 100644 --- a/routes/investment-zones.php +++ b/routes/investment-zones.php @@ -20,6 +20,7 @@ Route::get('/api/v2/investment/zones/export.csv', [InvestmentZoneController::cla Route::get('/api/v2/investment/zones/{id}', [InvestmentZoneController::class, 'show'])->whereUuid('id')->middleware('throttle:api'); Route::get('/zonas/{zone}/anexos/{attachment}', [InvestmentZoneAttachmentController::class, 'downloadPublic'])->whereUuid(['zone', 'attachment'])->middleware('throttle:api'); Route::middleware([CorrelationId::class, 'auth', 'verified', \App\Http\Middleware\EnsureScopedPortalAdministration::class, RequireZoneMfa::class, PrivateInvestmentResponse::class, 'throttle:30,1'])->group(function () { + Route::post('/api/v2/investment/admin/tax-rule-sources', [\App\Http\Controllers\SimulationFiscalSourceController::class, 'store']); Route::get('/api/v2/investment/admin/legal-sources', [InvestmentLegalSourceController::class, 'index']); Route::post('/api/v2/investment/admin/legal-sources', [InvestmentLegalSourceController::class, 'store']); Route::post('/api/v2/investment/admin/legal-sources/{id}/{action}', [InvestmentLegalSourceController::class, 'transition'])->whereUuid('id')->whereIn('action', ['validate', 'revoke']); diff --git a/tests/Feature/SimulationFiscalMaintenanceActorTest.php b/tests/Feature/SimulationFiscalMaintenanceActorTest.php new file mode 100644 index 00000000..269b370a --- /dev/null +++ b/tests/Feature/SimulationFiscalMaintenanceActorTest.php @@ -0,0 +1,217 @@ +otherConstraints = $this->unrelatedConstraints(); + if (! Schema::hasColumn('investment_legal_sources', 'fiscal_acceptance')) { + (require database_path('migrations/2026_10_21_130000_correct_fiscal_maintenance_actor_constraints.php'))->up(); + } + $this->maintainer = User::factory()->withTwoFactor()->create(); + config(['portal.managers' => [$this->maintainer->email], + 'simulation-tax-governance.editors' => [], 'simulation-tax-governance.reviewers' => [], + 'simulation-tax-governance.approvers' => [], 'simulation-tax-governance.publishers' => [], + 'simulation-fiscal-consumer.enabled' => false]); + } + + private function unrelatedConstraints(): array + { + return array_map(fn ($row) => (array) $row, DB::select("SELECT conrelid::regclass::text AS relation, conname, pg_get_constraintdef(oid) AS definition FROM pg_constraint + WHERE connamespace='public'::regnamespace AND conrelid NOT IN ('public.investment_legal_sources'::regclass,'public.simulation_tax_rule_versions'::regclass) + ORDER BY relation,conname")); + } + + private function sourceInput(): array + { + return ['title' => 'Synthetic QA official source curation; not a destination record', + '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, + 'sha256' => str_repeat('a', 64), 'citation' => 'QA fixture: PDF 3, art.17(1); no legal opinion.', + 'official_origin' => 'QA fixture of an identified official publication; not a new legal validation.']; + } + + private function source(): string + { + return $this->actingAs($this->maintainer)->postJson('/api/v2/investment/admin/tax-rule-sources', $this->sourceInput()) + ->assertCreated()->assertJsonPath('fiscal_acceptance.legal_opinion', false) + ->assertJsonPath('fiscal_acceptance.independent_review', false) + ->assertJsonPath('owner_id', $this->maintainer->id)->assertJsonPath('validator_id', null) + ->assertJsonPath('validation_status', 'pending')->assertJsonPath('fiscal_acceptance.responsible_user_id', $this->maintainer->id)->json('id'); + } + + private function rule(string $source): string + { + $payload = json_decode(file_get_contents(database_path('qa/iva-general-net-v1.json')), true, flags: JSON_THROW_ON_ERROR); + $payload['legal_source_id'] = $source; + $payload['source_sha256'] = str_repeat('a', 64); + + return $this->postJson('/api/v2/investment/admin/tax-rules', $payload)->assertCreated()->json('id'); + } + + public function test_one_existing_administrator_completes_fiscal_curation_with_responsibility_and_history(): void + { + $simulationCount = DB::table('simulations')->count(); + $source = $this->source(); + $id = $this->rule($source); + $evidence = 'QA technical parameter maintenance by the authorised same actor; not independent review or legal validation.'; + $this->postJson('/api/v2/investment/admin/tax-rules/'.$id.'/publish', ['version_no' => 1, 'evidence' => $evidence])->assertUnprocessable(); + foreach (['submit', 'review', 'approve', 'publish'] as $offset => $action) { + $this->postJson('/api/v2/investment/admin/tax-rules/'.$id.'/'.$action, + ['version_no' => $offset + 1, 'evidence' => $evidence])->assertOk()->assertJsonPath('version_no', $offset + 2); + } + $rule = SimulationTaxRuleVersion::findOrFail($id); + foreach (['author_id', 'reviewer_id', 'approver_id', 'publisher_id'] as $column) { + $this->assertSame($this->maintainer->id, (int) $rule->$column); + } + $this->assertSame('published', $rule->status); + $this->assertFalse(InvestmentLegalSource::whereKey($source)->where('validation_status', 'validated')->exists()); + // Source eligibility is checked through the existing resolver, without executing a formula. + DB::statement('CREATE ROLE qa_fiscal_maintenance_read_20261021 NOLOGIN NOSUPERUSER NOBYPASSRLS'); + DB::statement('GRANT USAGE ON SCHEMA public TO qa_fiscal_maintenance_read_20261021'); + DB::statement('GRANT SELECT ON simulation_tax_rule_versions, investment_legal_sources TO qa_fiscal_maintenance_read_20261021'); + DB::statement("SELECT set_config('app.simulation_tax_governance', 'off', true)"); + DB::statement('SET LOCAL ROLE qa_fiscal_maintenance_read_20261021'); + try { + $this->assertSame(1, SimulationTaxRuleVersion::whereKey($id)->count()); + $parameters = app(SimulationTaxParameterResolver::class)->resolvePublishedForPeriod('IVA', '2026-10-21', '2026-10-21', + ['currency' => 'MZN', 'operation' => 'internal_general_net']); + $this->assertSame('resolved', $parameters[0]['status']); + } finally { + DB::statement('RESET ROLE'); + } + $this->assertSame(1, (int) $rule->source_version_no); + $this->assertSame(str_repeat('a', 64), $rule->source_sha256); + $events = DB::table('simulation_tax_rule_events')->where('rule_version_id', $id)->orderBy('version_no')->get(); + $this->assertSame(['created', 'submit', 'review', 'approve', 'publish'], $events->pluck('action')->all()); + foreach ($events as $event) { + $this->assertSame($this->maintainer->id, (int) $event->actor_user_id); + $canonical = DB::selectOne('select CAST(? AS jsonb)::text as canonical', [$event->snapshot])->canonical; + $this->assertSame(hash('sha256', $canonical), $event->sha256); + } + $this->assertDatabaseHas('audit_events', ['entity_id' => $source, 'actor_user_id' => $this->maintainer->id, 'action' => 'official_source_acceptance']); + $this->getJson('/api/v2/investment/admin/tax-rules/'.$id)->assertOk()->assertJsonPath('id', $id); + $this->getJson('/api/v1/fiscal-consumer')->assertOk()->assertJsonPath('data.enabled', false); + $this->assertSame($simulationCount, DB::table('simulations')->count()); + $this->assertSame($this->otherConstraints, $this->unrelatedConstraints()); + $this->postJson('/api/v2/investment/admin/tax-rules/'.$id.'/suspend', ['version_no' => 5, 'evidence' => $evidence])->assertOk(); + $this->assertSame(6, DB::table('simulation_tax_rule_events')->where('rule_version_id', $id)->count()); + } + + public function test_investor_and_unconfirmed_mfa_cannot_maintain_fiscal_parameters(): void + { + $path = '/api/v2/investment/admin/tax-rule-sources'; + $this->postJson($path, $this->sourceInput())->assertUnauthorized(); + $this->actingAs(User::factory()->withTwoFactor()->create())->postJson($path, $this->sourceInput())->assertForbidden(); + $withoutMfa = User::factory()->create(); + config(['portal.managers' => [$withoutMfa->email]]); + $this->actingAs($withoutMfa)->postJson($path, $this->sourceInput())->assertForbidden(); + config(['portal.managers' => [$this->maintainer->email], 'portal.profile_access_enabled' => true]); + app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), User::factory()->withTwoFactor()->create(), + ['user_id' => $this->maintainer->id, 'role' => 'auditor', 'organization_id' => null, + 'opportunity_types' => ['investment_project'], 'source' => 'synthetic-qa-fiscal-control']); + $this->actingAs($this->maintainer)->postJson($path, $this->sourceInput())->assertForbidden(); + + } + + public function test_evidence_source_version_locking_and_immutable_events_remain_required(): void + { + $this->actingAs($this->maintainer)->postJson('/api/v2/investment/admin/tax-rule-sources', + [...$this->sourceInput(), 'sha256' => ''])->assertUnprocessable(); + $source = $this->source(); + $wrong = json_decode(file_get_contents(database_path('qa/iva-general-net-v1.json')), true, flags: JSON_THROW_ON_ERROR); + $wrong['legal_source_id'] = $source; + $wrong['source_sha256'] = str_repeat('b', 64); + $this->postJson('/api/v2/investment/admin/tax-rules', $wrong)->assertUnprocessable(); + $id = $this->rule($source); + $path = '/api/v2/investment/admin/tax-rules/'.$id.'/submit'; + $this->postJson($path, ['version_no' => 1, 'evidence' => 'short'])->assertUnprocessable(); + $this->postJson($path, ['version_no' => 2, 'evidence' => 'QA evidence and correct actor; stale version.'])->assertConflict(); + InvestmentLegalSource::whereKey($source)->update(['version_no' => 2]); + $this->postJson($path, ['version_no' => 1, 'evidence' => 'QA evidence and correct actor; source changed.'])->assertUnprocessable(); + $this->assertSame('draft', SimulationTaxRuleVersion::findOrFail($id)->status); + try { + DB::transaction(fn () => DB::table('simulation_tax_rule_events')->where('rule_version_id', $id)->update(['action' => 'tampered'])); + $this->fail('Audit must stay immutable.'); + } catch (QueryException) { + $this->assertDatabaseHas('simulation_tax_rule_events', ['rule_version_id' => $id, 'action' => 'created']); + } + try { + (require database_path('migrations/2026_10_21_130000_correct_fiscal_maintenance_actor_constraints.php'))->down(); + $this->fail('Rollback must preserve legitimate fiscal curation.'); + } catch (\LogicException $exception) { + $this->assertStringContainsString('conservar', $exception->getMessage()); + } + } + + public function test_other_source_validation_keeps_its_independence_and_active_migration_is_refused(): void + { + $data = $this->sourceInput(); + unset($data['sha256'], $data['citation'], $data['official_origin']); + $source = InvestmentLegalSource::create([...$data, 'owner_id' => $this->maintainer->id, 'validation_status' => 'pending']); + config(['investment-zones.legal_reviewers' => [$this->maintainer->email]]); + $this->actingAs($this->maintainer)->postJson('/api/v2/investment/admin/legal-sources/'.$source->id.'/validate', + ['version_no' => 1])->assertForbidden(); + try { + DB::transaction(fn () => $source->update(['validator_id' => $this->maintainer->id, 'validated_at' => now(), 'validation_status' => 'validated'])); + $this->fail('Other source curation must retain its check.'); + } catch (QueryException) { + $this->assertSame('pending', $source->fresh()->validation_status); + } + config(['database.connections.pgsql.url' => 'not-an-isolated-qa-target']); + $this->expectException(\LogicException::class); + (require database_path('migrations/2026_10_21_130000_correct_fiscal_maintenance_actor_constraints.php'))->up(); + } + + public function test_scoped_administration_is_reused_without_four_email_designations(): void + { + config(['portal.managers' => []]); + config(['portal.profile_access_enabled' => true]); + $second = User::factory()->withTwoFactor()->create(); + // Pre-existing-authority fixture, exclusively inside the reverted QA transaction. + DB::table('portal_superuser_pair')->updateOrInsert(['id' => 1], [ + 'first_user_id' => $this->maintainer->id, 'second_user_id' => $second->id, + 'approved_by' => $second->id, 'version' => 1, 'created_at' => now(), 'updated_at' => now(), + ]); + $this->actingAs($this->maintainer)->withSession(['portal.mfa_actor' => $this->maintainer->id, 'portal.mfa_at' => time()]); + $source = $this->source(); + $id = $this->rule($source); + foreach (['submit', 'review', 'approve', 'publish'] as $offset => $action) { + $this->postJson('/api/v2/investment/admin/tax-rules/'.$id.'/'.$action, + ['version_no' => $offset + 1, 'evidence' => 'QA existing scoped administrator completes fiscal maintenance; not independent legal validation.'])->assertOk(); + } + $this->withSession(['portal.mfa_at' => time() - 901])->postJson('/api/v2/investment/admin/tax-rule-sources', $this->sourceInput())->assertForbidden(); + } + + public function test_unused_correction_can_be_rolled_back_and_reapplied_only_in_qa(): void + { + $migration = require database_path('migrations/2026_10_21_130000_correct_fiscal_maintenance_actor_constraints.php'); + $migration->down(); + $this->assertFalse(Schema::hasColumn('investment_legal_sources', 'fiscal_acceptance')); + $migration->up(); + $this->assertTrue(Schema::hasColumn('investment_legal_sources', 'fiscal_acceptance')); + $this->assertSame($this->otherConstraints, $this->unrelatedConstraints()); + } +} diff --git a/tests/Feature/SimulationTaxRuleGovernanceTest.php b/tests/Feature/SimulationTaxRuleGovernanceTest.php index 041741d9..b0411a1a 100644 --- a/tests/Feature/SimulationTaxRuleGovernanceTest.php +++ b/tests/Feature/SimulationTaxRuleGovernanceTest.php @@ -56,7 +56,7 @@ class SimulationTaxRuleGovernanceTest extends TestCase ]); } - public function test_rule_requires_independent_review_approval_publication_and_records_hash_chained_versions(): void + public function test_rule_allows_separate_actors_and_records_hash_chained_versions(): void { $this->actingAs($this->editor); $created = $this->postJson('/api/v2/investment/admin/tax-rules', $this->rulePayload()) @@ -66,9 +66,6 @@ class SimulationTaxRuleGovernanceTest extends TestCase $this->postJson("/api/v2/investment/admin/tax-rules/{$id}/submit", $this->transitionPayload(1)) ->assertOk()->assertJsonPath('status', 'submitted')->assertJsonPath('version_no', 2); - $this->postJson("/api/v2/investment/admin/tax-rules/{$id}/review", $this->transitionPayload(2)) - ->assertForbidden(); - $this->actingAs($this->reviewer) ->postJson("/api/v2/investment/admin/tax-rules/{$id}/review", $this->transitionPayload(2)) ->assertOk()->assertJsonPath('status', 'reviewed')->assertJsonPath('reviewer_id', $this->reviewer->id); @@ -135,9 +132,9 @@ class SimulationTaxRuleGovernanceTest extends TestCase ->assertOk()->assertJsonPath('status', 'suspended'); } - public function test_draft_edit_uses_optimistic_locking_and_other_roles_cannot_create_rules(): void + public function test_draft_edit_uses_optimistic_locking_and_unauthorised_users_cannot_create_rules(): void { - $this->actingAs($this->reviewer)->postJson('/api/v2/investment/admin/tax-rules', $this->rulePayload())->assertForbidden(); + $this->actingAs(User::factory()->withTwoFactor()->create())->postJson('/api/v2/investment/admin/tax-rules', $this->rulePayload())->assertForbidden(); $this->actingAs($this->editor); $created = $this->postJson('/api/v2/investment/admin/tax-rules', $this->rulePayload())->assertCreated();