fix: allow authorised fiscal maintenance by the same responsible person
tests / ci (push) Canceled after 0s
tests / ci (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\InvestmentLegalSource;
|
||||
use App\Services\SimulationTaxGovernanceAccess;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/** Explicit fiscal curation of an identified official source; not an independent legal opinion. */
|
||||
final class SimulationFiscalSourceController extends Controller
|
||||
{
|
||||
public function store(Request $request): JsonResponse
|
||||
{
|
||||
SimulationTaxGovernanceAccess::requireRole($request->user(), 'editors');
|
||||
$data = $request->validate([
|
||||
'title' => ['required', 'string', 'max:255'], 'url' => ['required', 'url:https', 'max:2000'],
|
||||
'diploma' => ['required', 'string', 'max:255'], 'article' => ['required', 'string', 'max:255'],
|
||||
'competent_entity' => ['required', 'string', 'max:255'], 'valid_from' => ['required', 'date_format:Y-m-d'],
|
||||
'valid_until' => ['nullable', 'date_format:Y-m-d', 'after_or_equal:valid_from'],
|
||||
'sha256' => ['required', 'regex:/^[a-f0-9]{64}$/D'],
|
||||
'citation' => ['required', 'string', 'min:10', 'max:2000'],
|
||||
'official_origin' => ['required', 'string', 'min:10', 'max:2000'],
|
||||
]);
|
||||
|
||||
return DB::transaction(function () use ($request, $data): JsonResponse {
|
||||
$actor = $request->user();
|
||||
$acceptance = ['scope' => 'fiscal_parameters', 'basis' => 'Tempo-official-sources-20261008',
|
||||
'responsible_user_id' => $actor->id, 'sha256' => $data['sha256'], 'citation' => $data['citation'],
|
||||
'official_origin' => $data['official_origin'], 'accepted_at' => now()->toISOString(),
|
||||
'legal_opinion' => false, 'independent_review' => false];
|
||||
unset($data['sha256'], $data['citation'], $data['official_origin']);
|
||||
$source = InvestmentLegalSource::create([...$data, 'owner_id' => $actor->id, 'validator_id' => null,
|
||||
'validation_status' => 'pending', 'validated_at' => null, 'version_no' => 1, 'fiscal_acceptance' => $acceptance]);
|
||||
DB::table('audit_events')->insert(['source_system' => 'simulation-fiscal-maintenance',
|
||||
'entity_type' => 'investment_legal_sources', 'entity_id' => $source->id, 'actor_user_id' => $actor->id,
|
||||
'action' => 'official_source_acceptance', 'after_data' => json_encode([
|
||||
'source' => $source->attributesToArray(), 'basis' => $acceptance,
|
||||
'sha256' => hash('sha256', $source->toJson()),
|
||||
], JSON_THROW_ON_ERROR)]);
|
||||
|
||||
return response()->json($source->fresh(), 201);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Services\PortalProfileAccess;
|
||||
use App\Services\ScopedPortalAdministration;
|
||||
use App\Services\SimulationTaxGovernanceAccess;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use App\Services\ScopedPortalAdministration;
|
||||
|
||||
final class EnsureScopedPortalAdministration
|
||||
{
|
||||
public function handle(Request $request, Closure $next): Response
|
||||
{
|
||||
$actor=$request->user();abort_unless($actor,403);
|
||||
$scope=app(ScopedPortalAdministration::class);
|
||||
$path=$request->path();
|
||||
$interests=$request->is('management/interests','management/interests/*','api/v1/investment/admin/interests/*');
|
||||
// Exact areas of P1; unrelated management, fiscal rules and private records keep their gate.
|
||||
$catalogue=$request->is('management/economic-references/lot-01','management/economic-references/lot-01/*',
|
||||
'management/education','management/education/*','management/investment/references','management/investment/references/*','api/v1/investment/admin/references','api/v1/investment/admin/references/*',
|
||||
'management/investment/zones','management/investment/zones/*','api/v2/investment/admin/zones','api/v2/investment/admin/zones/*',
|
||||
'management/balcao/atribuicoes','management/balcao/atribuicoes/*','management/balcao/servicos',
|
||||
'management/balcao/fontes') || ($request->isMethod('GET') && $path==='management/balcao-install');
|
||||
$allowed=$interests ? $scope->interestOperator($actor) : ($catalogue && $scope->administrative($actor));
|
||||
abort_unless($allowed || ($actor->can('manage-portal') && !app(\App\Services\PortalProfileAccess::class)->has($actor,'auditor')),403,'Acesso reservado ao âmbito já designado.');
|
||||
$response=$next($request);
|
||||
if (!$request->isMethod('GET') && $response->getStatusCode()<400) {
|
||||
app(\App\Services\PortalProfileAccess::class)->audit($actor,'scoped_administration',[
|
||||
'method'=>$request->method(),'route'=>$request->route()?->uri(),'scope'=>$interests?'assigned-interest':'catalogue-administration']);
|
||||
$actor = $request->user();
|
||||
abort_unless($actor, 403);
|
||||
$scope = app(ScopedPortalAdministration::class);
|
||||
$path = $request->path();
|
||||
$interests = $request->is('management/interests', 'management/interests/*', 'api/v1/investment/admin/interests/*');
|
||||
// Fiscal maintenance reuses authorised administration; unrelated gates remain unchanged.
|
||||
$catalogue = $request->is('management/economic-references/lot-01', 'management/economic-references/lot-01/*',
|
||||
'management/education', 'management/education/*', 'management/investment/references', 'management/investment/references/*', 'api/v1/investment/admin/references', 'api/v1/investment/admin/references/*',
|
||||
'management/investment/zones', 'management/investment/zones/*', 'api/v2/investment/admin/zones', 'api/v2/investment/admin/zones/*',
|
||||
'management/balcao/atribuicoes', 'management/balcao/atribuicoes/*', 'management/balcao/servicos',
|
||||
'management/balcao/fontes') || ($request->isMethod('GET') && $path === 'management/balcao-install');
|
||||
$fiscal = $request->is('api/v2/investment/admin/tax-rules', 'api/v2/investment/admin/tax-rules/*',
|
||||
'api/v2/investment/admin/tax-rule-sources', 'management/investment/tax-rules/*');
|
||||
$allowed = ($fiscal && SimulationTaxGovernanceAccess::maintainer($actor)) || ($interests ? $scope->interestOperator($actor) : ($catalogue && $scope->administrative($actor)));
|
||||
abort_unless($allowed || ($actor->can('manage-portal') && ! app(PortalProfileAccess::class)->has($actor, 'auditor')), 403, 'Acesso reservado ao âmbito já designado.');
|
||||
$response = $next($request);
|
||||
if (! $request->isMethod('GET') && $response->getStatusCode() < 400) {
|
||||
app(PortalProfileAccess::class)->audit($actor, 'scoped_administration', [
|
||||
'method' => $request->method(), 'route' => $request->route()?->uri(), 'scope' => $fiscal ? 'fiscal-maintenance' : ($interests ? 'assigned-interest' : 'catalogue-administration')]);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,4 +16,9 @@ class InvestmentLegalSource extends Model
|
||||
use HasUuids;
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return ['fiscal_acceptance' => 'array'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
final class SimulationFiscalSourceAcceptance
|
||||
{
|
||||
public static function eligible(array $source, ?string $hash = null): bool
|
||||
{
|
||||
if (($source['validation_status'] ?? null) === 'validated') {
|
||||
return true;
|
||||
}
|
||||
$basis = $source['fiscal_acceptance'] ?? null;
|
||||
|
||||
return ($source['validation_status'] ?? null) === 'pending' && is_array($basis)
|
||||
&& ($basis['scope'] ?? null) === 'fiscal_parameters'
|
||||
&& ($basis['basis'] ?? null) === 'Tempo-official-sources-20261008'
|
||||
&& preg_match('/^[a-f0-9]{64}$/D', (string) ($basis['sha256'] ?? '')) === 1
|
||||
&& ($hash === null || $basis['sha256'] === $hash);
|
||||
}
|
||||
}
|
||||
@@ -8,15 +8,24 @@ final class SimulationTaxGovernanceAccess
|
||||
{
|
||||
private const ROLES = ['editors', 'reviewers', 'approvers', 'publishers'];
|
||||
|
||||
public static function maintainer(User $user): bool
|
||||
{
|
||||
return $user->hasVerifiedEmail() && ! app(PortalProfileAccess::class)->has($user, 'auditor') && ($user->can('manage-portal')
|
||||
|| app(ScopedPortalAdministration::class)->administrative($user));
|
||||
}
|
||||
|
||||
public static function requireRole(User $user, string $role): void
|
||||
{
|
||||
abort_unless(in_array($role, self::ROLES, true), 500, 'Função de governação desconhecida.');
|
||||
abort_unless(in_array(strtolower($user->email), config('simulation-tax-governance.'.$role, []), true),
|
||||
abort_unless(self::maintainer($user) || in_array(strtolower($user->email), config('simulation-tax-governance.'.$role, []), true),
|
||||
403, 'O utilizador não está designado para esta função de governação fiscal.');
|
||||
}
|
||||
|
||||
public static function requireAnyRole(User $user): void
|
||||
{
|
||||
if (self::maintainer($user)) {
|
||||
return;
|
||||
}
|
||||
foreach (self::ROLES as $role) {
|
||||
if (in_array(strtolower($user->email), config('simulation-tax-governance.'.$role, []), true)) {
|
||||
return;
|
||||
|
||||
@@ -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'] ?? ''))) {
|
||||
|
||||
@@ -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')))
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
// These lists are deliberately empty by default. A portal manager and MFA are
|
||||
// also required; governance roles never activate rules in the calculation engine.
|
||||
// Optional specific assignments. Existing authorised administration can maintain
|
||||
// fiscal parameters with MFA, without four people or four email designations.
|
||||
// Rule publication never activates the calculation consumer by itself.
|
||||
'editors' => array_values(array_filter(array_map('strtolower', array_map('trim', explode(',', (string) env('INVESTMOZ_TAX_RULE_EDITORS', '')))))),
|
||||
'reviewers' => array_values(array_filter(array_map('strtolower', array_map('trim', explode(',', (string) env('INVESTMOZ_TAX_RULE_REVIEWERS', '')))))),
|
||||
'approvers' => array_values(array_filter(array_map('strtolower', array_map('trim', explode(',', (string) env('INVESTMOZ_TAX_RULE_APPROVERS', '')))))),
|
||||
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
use App\Support\IsolatedQaDatabase;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if (! IsolatedQaDatabase::allowsSchemaChanges()) {
|
||||
throw new LogicException('Ensaie esta correcção apenas numa base QA isolada. A aplicação no destino exige decisão, backup/restauro e acção autenticada.');
|
||||
}
|
||||
|
||||
// Only fiscal rule maintenance changes. Opportunity/zone workflow checks remain intact.
|
||||
DB::statement('ALTER TABLE simulation_tax_rule_versions DROP CONSTRAINT tax_rule_stage_evidence_check, DROP CONSTRAINT tax_rule_approval_check, DROP CONSTRAINT tax_rule_publication_check');
|
||||
DB::statement("ALTER TABLE simulation_tax_rule_versions
|
||||
ADD CONSTRAINT tax_rule_stage_evidence_check CHECK(status NOT IN ('reviewed','rejected','approved','published','suspended','archived') OR (reviewer_id IS NOT NULL AND submitted_at IS NOT NULL AND reviewed_at IS NOT NULL AND length(trim(review_evidence))>=20)),
|
||||
ADD CONSTRAINT tax_rule_approval_check CHECK(status NOT IN ('approved','published','suspended','archived') OR (approver_id IS NOT NULL AND approved_at IS NOT NULL AND length(trim(approval_evidence))>=20)),
|
||||
ADD CONSTRAINT tax_rule_publication_check CHECK(status NOT IN ('published','suspended','archived') OR (publisher_id IS NOT NULL AND published_at IS NOT NULL AND length(trim(publication_evidence))>=20))");
|
||||
|
||||
DB::statement('ALTER TABLE investment_legal_sources ADD COLUMN fiscal_acceptance jsonb NULL');
|
||||
DB::statement("ALTER TABLE investment_legal_sources ADD CONSTRAINT investment_legal_source_fiscal_acceptance_check CHECK (
|
||||
fiscal_acceptance IS NULL OR COALESCE((
|
||||
fiscal_acceptance->>'scope'='fiscal_parameters' AND
|
||||
fiscal_acceptance->>'basis'='Tempo-official-sources-20261008' AND
|
||||
fiscal_acceptance->>'responsible_user_id'=owner_id::text AND
|
||||
fiscal_acceptance->>'sha256' ~ '^[a-f0-9]{64}$' AND
|
||||
fiscal_acceptance->>'legal_opinion'='false' AND fiscal_acceptance->>'independent_review'='false' AND
|
||||
length(trim(fiscal_acceptance->>'citation'))>=10 AND
|
||||
length(trim(fiscal_acceptance->>'official_origin'))>=10
|
||||
), false))");
|
||||
DB::statement('DROP POLICY simulation_tax_rule_versions_public_read ON simulation_tax_rule_versions');
|
||||
DB::statement("CREATE POLICY simulation_tax_rule_versions_public_read ON simulation_tax_rule_versions FOR SELECT USING (
|
||||
status='published' AND EXISTS (SELECT 1 FROM investment_legal_sources source
|
||||
WHERE source.id=simulation_tax_rule_versions.legal_source_id
|
||||
AND (source.validation_status='validated' OR (source.validation_status='pending'
|
||||
AND source.fiscal_acceptance->>'scope'='fiscal_parameters'
|
||||
AND source.fiscal_acceptance->>'basis'='Tempo-official-sources-20261008'
|
||||
AND source.fiscal_acceptance->>'sha256'=simulation_tax_rule_versions.source_sha256))
|
||||
AND source.version_no=simulation_tax_rule_versions.source_version_no
|
||||
AND source.valid_from<=simulation_tax_rule_versions.valid_from
|
||||
AND (source.valid_until IS NULL OR (simulation_tax_rule_versions.valid_until IS NOT NULL AND source.valid_until>=simulation_tax_rule_versions.valid_until))
|
||||
AND (simulation_tax_rule_versions.valid_until IS NOT NULL OR source.valid_until IS NULL)))");
|
||||
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if (! IsolatedQaDatabase::allowsSchemaChanges()) {
|
||||
throw new LogicException('Reversão permitida apenas em QA isolado.');
|
||||
}
|
||||
$used = DB::table('investment_legal_sources')->whereNotNull('fiscal_acceptance')->exists()
|
||||
|| DB::table('simulation_tax_rule_versions')->whereRaw('reviewer_id=author_id OR approver_id=author_id OR approver_id=reviewer_id OR publisher_id=author_id OR publisher_id=reviewer_id OR publisher_id=approver_id')->exists();
|
||||
if ($used) {
|
||||
throw new LogicException('Reversão recusada: conservar os cadastros e históricos legítimos; desactivar novas execuções em vez de apagar registos.');
|
||||
}
|
||||
DB::statement('ALTER TABLE simulation_tax_rule_versions DROP CONSTRAINT tax_rule_stage_evidence_check, DROP CONSTRAINT tax_rule_approval_check, DROP CONSTRAINT tax_rule_publication_check');
|
||||
DB::statement("ALTER TABLE simulation_tax_rule_versions
|
||||
ADD CONSTRAINT tax_rule_stage_evidence_check CHECK(status NOT IN ('reviewed','rejected','approved','published','suspended','archived') OR (reviewer_id IS NOT NULL AND reviewer_id<>author_id AND submitted_at IS NOT NULL AND reviewed_at IS NOT NULL AND length(trim(review_evidence))>=20)),
|
||||
ADD CONSTRAINT tax_rule_approval_check CHECK(status NOT IN ('approved','published','suspended','archived') OR (approver_id IS NOT NULL AND approver_id<>author_id AND approver_id<>reviewer_id AND approved_at IS NOT NULL AND length(trim(approval_evidence))>=20)),
|
||||
ADD CONSTRAINT tax_rule_publication_check CHECK(status NOT IN ('published','suspended','archived') OR (publisher_id IS NOT NULL AND publisher_id<>author_id AND publisher_id<>reviewer_id AND publisher_id<>approver_id AND published_at IS NOT NULL AND length(trim(publication_evidence))>=20))");
|
||||
DB::statement('DROP POLICY simulation_tax_rule_versions_public_read ON simulation_tax_rule_versions');
|
||||
(require __DIR__.'/2026_10_03_190000_allow_public_read_of_published_tax_parameters.php')->up();
|
||||
DB::statement('ALTER TABLE investment_legal_sources DROP CONSTRAINT investment_legal_source_fiscal_acceptance_check');
|
||||
DB::statement('ALTER TABLE investment_legal_sources DROP COLUMN fiscal_acceptance');
|
||||
}
|
||||
};
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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']);
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
use App\Models\InvestmentLegalSource;
|
||||
use App\Models\SimulationTaxRuleVersion;
|
||||
use App\Models\User;
|
||||
use App\Services\PortalProfileAdministration;
|
||||
use App\Services\SimulationTaxParameterResolver;
|
||||
use Illuminate\Database\QueryException;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
/** Only the maintenance control is exercised; no simulation or arithmetic runs here. */
|
||||
class SimulationFiscalMaintenanceActorTest extends TestCase
|
||||
{
|
||||
use DatabaseTransactions;
|
||||
|
||||
private User $maintainer;
|
||||
|
||||
private array $otherConstraints;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user