consumer = User::factory()->create(['email_verified_at' => now()]); $actors = []; foreach (['editors', 'reviewers', 'approvers', 'publishers'] as $role) { $actors[$role] = User::factory()->withTwoFactor()->create(); config(['simulation-tax-governance.'.$role => [$actors[$role]->email]]); } $this->publisher = $actors['publishers']; $this->source = InvestmentLegalSource::create([ 'title' => 'Synthetic QA fixture, not a homologation publication', 'url' => 'https://www.inm.gov.mz/', 'diploma' => 'Lei 22/2022', 'article' => 'Artigo 17(1)', 'competent_entity' => 'Assembleia da República', 'valid_from' => '2023-01-01', 'valid_until' => null, 'validation_status' => 'validated', 'owner_id' => $actors['editors']->id, 'version_no' => 1, 'validator_id' => $actors['reviewers']->id, 'validated_at' => now(), ]); $data = json_decode(file_get_contents(database_path('qa/iva-general-net-v1.json')), true, flags: JSON_THROW_ON_ERROR); $data['legal_source_id'] = $this->source->id; $gov = app(SimulationTaxRuleGovernance::class); $this->rule = $gov->create($actors['editors'], $data); foreach (['submit' => 'editors', 'review' => 'reviewers', 'approve' => 'approvers', 'publish' => 'publishers'] as $action => $role) { $this->rule = $gov->transition($actors[$role], $this->rule->id, $this->rule->version_no, $action, 'Synthetic isolated QA fixture for consumer integration only.'); } config(['simulation-fiscal-consumer.enabled' => true, 'simulation-fiscal-consumer.rule_id' => $this->rule->id, 'simulation-fiscal-consumer.rule_version' => 1]); } private function input(array $overrides = []): array { return ['scenario_kind' => 'fiscal_rule_published', 'base' => '1000.03', 'currency' => 'MZN', 'reference_date' => '2026-10-21', 'operation' => 'internal_general_net', 'scope_declared' => true, ...$overrides]; } private function create(array $overrides = [], ?string $key = null) { return $this->actingAs($this->consumer)->postJson('/api/v1/simulations', $this->input($overrides), ['Idempotency-Key' => $key ?? (string) Str::uuid()]); } private function finish(string $id): Simulation { (new RunSimulation($id, $this->consumer->id, null))->handle(app(SimulationExecution::class)); return Simulation::with('reports')->findOrFail($id); } public function test_normal_simulator_admits_published_rule_and_reuses_history_and_reports(): void { $key = (string) Str::uuid(); $this->getJson('/api/v1/fiscal-consumer')->assertOk()->assertJsonPath('data.enabled', true); $id = $this->create([], $key)->assertAccepted()->json('data.id'); $this->create([], $key)->assertOk()->assertJsonPath('replayed', true)->assertJsonPath('data.id', $id); $this->create(['base' => '1001'], $key)->assertConflict(); $simulation = $this->finish($id); $snapshot = $simulation->result_snapshot; $this->assertSame('fiscal_rule_published', $snapshot['scenario_kind']); $this->assertSame('calculated', $snapshot['result']['status']); $this->assertSame($this->rule->id, $snapshot['fiscal_provenance']['rule']['id']); $this->assertSame('published', $snapshot['fiscal_provenance']['rule']['status']); $this->assertArrayNotHasKey('review_evidence', $snapshot['fiscal_provenance']['rule']); $this->assertArrayNotHasKey('author_id', $snapshot['fiscal_provenance']['rule']); $this->getJson('/api/v1/simulations/history')->assertOk()->assertJsonPath('meta.total', 1); $this->getJson('/api/v1/simulations/'.$id)->assertOk()->assertJsonPath('data.result_snapshot', $snapshot); $report = $simulation->reports->first(); $this->getJson('/api/v1/simulations/'.$id.'/reports/'.$report->id)->assertOk()->assertJsonPath('data.report_snapshot', $snapshot); $this->assertSame($report->report_sha256, DB::table('simulation_results')->where('simulation_id', $id)->value('result_sha256')); $this->assertSame(1, Simulation::where('owner_user_id', $this->consumer->id)->count()); $this->assertDatabaseHas('simulation_result_sources', ['source_id' => $this->rule->id, 'source_type' => 'tax_rule_published']); $this->actingAs(User::factory()->create(['email_verified_at' => now()]))->getJson('/api/v1/simulations/'.$id)->assertNotFound(); $this->getJson('/api/v1/simulations/'.$id.'/reports/'.$report->id)->assertNotFound(); } public function test_missing_scope_or_changed_source_is_indeterminate_not_zero(): void { $id = $this->create(['scope_declared' => false])->assertAccepted()->json('data.id'); $snapshot = $this->finish($id)->result_snapshot; $this->assertSame('indeterminate', $snapshot['result']['status']); $this->assertNull($snapshot['result']['value']); $this->assertContains('operation_scope', $snapshot['result']['missing_inputs']); $this->assertSame('undetermined', $snapshot['fiscal_provenance']['scope_status']); $this->source->update(['version_no' => 2]); $id = $this->create()->assertAccepted()->json('data.id'); $snapshot = $this->finish($id)->result_snapshot; $this->assertSame('indeterminate', $snapshot['result']['status']); $this->assertNull($snapshot['result']['value']); $this->assertContains('eligible_parameter', $snapshot['result']['missing_inputs']); } public function test_suspension_refuses_new_admissions_but_frozen_queue_and_replays_survive(): void { $key = (string) Str::uuid(); $id = $this->create([], $key)->assertAccepted()->json('data.id'); $this->assertDatabaseHas('simulations', ['id' => $id, 'status' => 'queued']); app(SimulationTaxRuleGovernance::class)->transition($this->publisher, $this->rule->id, $this->rule->version_no, 'suspend', 'QA: suspend after admission and before worker execution.'); $this->getJson('/api/v1/fiscal-consumer')->assertOk()->assertJsonPath('data.enabled', false); $this->create()->assertStatus(503); $this->create([], $key)->assertOk()->assertJsonPath('data.id', $id)->assertJsonPath('replayed', true); config(['simulation-fiscal-consumer.enabled' => false]); $this->create()->assertStatus(503); $simulation = $this->finish($id); $this->assertSame('completed', $simulation->status); $this->assertSame('published', $simulation->result_snapshot['fiscal_provenance']['rule']['status']); $this->assertSame($this->rule->id, $simulation->result_snapshot['fiscal_provenance']['rule']['id']); $this->assertSame('suspended', $this->rule->fresh()->status); $this->create([], $key)->assertOk()->assertJsonPath('data.status', 'completed'); $this->getJson('/api/v1/simulations/'.$id.'/reports/'.$simulation->reports->first()->id)->assertOk() ->assertJsonPath('data.report_snapshot', $simulation->result_snapshot); $this->getJson('/api/v1/simulations/history')->assertOk()->assertJsonPath('meta.total', 1); } public function test_server_selection_cannot_be_overridden_and_coverage_is_bounded(): void { $this->create(['rate' => '0'])->assertUnprocessable(); $this->create(['rule_id' => (string) Str::uuid()])->assertUnprocessable(); $this->create(['reference_date' => '2027-01-01'])->assertUnprocessable(); $this->create(['currency' => 'USD'])->assertUnprocessable(); config(['simulation-fiscal-consumer.rule_version' => 2]); $this->create()->assertStatus(503); config(['simulation-fiscal-consumer.rule_id' => 'invalid-configuration']); $this->getJson('/api/v1/fiscal-consumer')->assertOk()->assertJsonPath('data.enabled', false); $this->assertSame(0, Simulation::where('owner_user_id', $this->consumer->id)->count()); } public function test_anonymous_cookie_token_ownership_is_reused(): void { $key = (string) Str::uuid(); $response = $this->postJson('/api/v1/simulations', $this->input(), ['Idempotency-Key' => $key]); $id = $response->assertAccepted()->json('data.id'); $token = $response->json('access_token'); $this->getJson('/api/v1/simulations/'.$id, ['X-Simulation-Token' => $token])->assertOk(); $this->getJson('/api/v1/simulations/'.$id, ['X-Simulation-Token' => str_repeat('x', 40)])->assertNotFound(); $this->getJson('/api/v1/simulations/history')->assertUnauthorized(); } public function test_disabled_consumer_does_not_disable_custom_planning(): void { config(['simulation-fiscal-consumer.enabled' => false]); $this->create()->assertStatus(503); $this->actingAs($this->consumer)->postJson('/api/v1/simulations', ['base' => '100', 'rate' => '5', 'reduction' => '0'], ['Idempotency-Key' => (string) Str::uuid()])->assertAccepted()->assertJsonPath('data.input_snapshot.scenario_kind', 'custom_planning'); } public function test_public_consumer_works_under_existing_rls_without_governance_context(): void { // Role and grants roll back with the isolated test transaction. No application permission is added. DB::statement('CREATE ROLE qa_fiscal_consumer_rls_20261021 NOLOGIN NOSUPERUSER NOBYPASSRLS'); DB::statement('GRANT USAGE ON SCHEMA public TO qa_fiscal_consumer_rls_20261021'); DB::statement('GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO qa_fiscal_consumer_rls_20261021'); DB::statement('GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO qa_fiscal_consumer_rls_20261021'); DB::statement("SELECT set_config('app.simulation_tax_governance', 'off', true)"); DB::statement('SET LOCAL ROLE qa_fiscal_consumer_rls_20261021'); try { $this->assertSame(1, SimulationTaxRuleVersion::whereKey($this->rule->id)->count()); $id = $this->create()->assertAccepted()->json('data.id'); $this->assertNotEmpty($id); $this->assertSame(0, DB::table('simulation_tax_rule_events')->where('rule_version_id', $this->rule->id)->count()); // An UPDATE scope is not granted by the public SELECT policy. $this->assertSame(0, SimulationTaxRuleVersion::whereKey($this->rule->id)->update(['status' => 'suspended'])); } finally { DB::statement('RESET ROLE'); } } public function test_admission_holds_shared_locks_until_transaction_end(): void { $this->create()->assertAccepted(); config(['database.connections.fiscal_lock_probe' => config('database.connections.pgsql')]); $probe = DB::connection('fiscal_lock_probe'); $probe->beginTransaction(); try { // A concurrent suspension/source transition cannot cross an admitted transaction. $rule = $probe->selectOne('select pg_try_advisory_xact_lock(261021, hashtext(?)) as acquired', [$this->rule->id]); $source = $probe->selectOne('select pg_try_advisory_xact_lock(261022, hashtext(?)) as acquired', [$this->source->id]); $this->assertFalse($rule->acquired); $this->assertFalse($source->acquired); } finally { $probe->rollBack(); DB::purge('fiscal_lock_probe'); } } }