f = receiptFixture(); $this->admins = []; for ($index = 0; $index < 3; $index++) { $this->admins[] = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'purchasing_role' => PurchasingRole::Admin]); } $this->actingAs($this->admins[0]); $this->data = ['previous_id' => 0, 'mode' => 'ceiling', 'scope' => 'process_total', 'currency' => 'MZN', 'ceiling' => '100.00', 'includes_tax' => true, 'valid_from' => today()->format('Y-m-d'), 'valid_until' => today()->addMonth()->format('Y-m-d'), 'reason' => 'Decisão explícita fictícia']; }); function proposeInvestmentTest(object $test): InvestmentPolicy { $test->actingAs($test->admins[0]); return app(InvestmentWorkflow::class)->propose($test->admins[0], $test->data); } function reviewInvestmentTest(object $test, InvestmentPolicy $policy, int $actor, string $decision = 'approved'): void { $test->actingAs($test->admins[$actor]); app(InvestmentWorkflow::class)->review($test->admins[$actor], $policy, $decision, 'Conferência administrativa de teste'); } test('investment policy requires two independent administrators without counting repeats', function () { $policy = proposeInvestmentTest($this); reviewInvestmentTest($this, $policy, 1); reviewInvestmentTest($this, $policy, 1); expect($policy->refresh()->status)->toBe('pending')->and(InvestmentPolicyReview::query()->count())->toBe(1); reviewInvestmentTest($this, $policy, 2); expect($policy->refresh()->status)->toBe('approved')->and(app(InvestmentWorkflow::class)->requireCurrent()->id)->toBe($policy->id); }); test('policy author cannot approve own proposal', function () { $policy = proposeInvestmentTest($this); $this->post('/regras-investimento/'.$policy->id.'/decidir', ['decision' => 'approved', 'reason' => 'Teste'])->assertForbidden(); }); test('policy parameters and decisions cannot be rewritten', function () { $policy = proposeInvestmentTest($this); expect(fn () => $policy->update(['ceiling_minor' => 1]))->toThrow(LogicException::class); reviewInvestmentTest($this, $policy, 1, 'rejected'); expect(fn () => reviewInvestmentTest($this, $policy, 2))->toThrow(ValidationException::class); expect(fn () => InvestmentPolicyReview::query()->firstOrFail()->delete())->toThrow(LogicException::class); }); test('an unapproved draft does not replace an approved company rule', function () { $approved = publishInvestmentFixture($this->f['tenant']->id); $this->data['previous_id'] = $approved->id; $draft = proposeInvestmentTest($this); expect($draft->status)->toBe('pending')->and(app(InvestmentWorkflow::class)->requireCurrent()->id)->toBe($approved->id); }); test('new rule starts on its explicit date and expiry cannot restore an older more permissive rule', function () { $old = publishInvestmentFixture($this->f['tenant']->id, '500.00'); $this->data['previous_id'] = $old->id; $this->data['valid_from'] = today()->addDay()->format('Y-m-d'); $this->data['valid_until'] = today()->addDays(2)->format('Y-m-d'); $new = proposeInvestmentTest($this); reviewInvestmentTest($this, $new, 1); reviewInvestmentTest($this, $new, 2); expect(app(InvestmentWorkflow::class)->requireCurrent()->id)->toBe($old->id); $this->travel(1)->days(); expect(app(InvestmentWorkflow::class)->requireCurrent()->id)->toBe($new->id); $this->travel(3)->days(); expect(fn () => app(InvestmentWorkflow::class)->requireCurrent())->toThrow(ValidationException::class); $this->travelBack(); }); test('stale proposals and decisions on superseded drafts are refused', function () { $first = proposeInvestmentTest($this); expect(fn () => proposeInvestmentTest($this))->toThrow(ValidationException::class); $this->data['previous_id'] = $first->id; proposeInvestmentTest($this); expect(fn () => reviewInvestmentTest($this, $first, 1))->toThrow(ValidationException::class); }); test('unsupported limit semantics cannot be configured by the browser', function (string $field, string $value) { $this->post('/regras-investimento', [...$this->data, $field => $value])->assertSessionHasErrors($field); expect(InvestmentPolicy::query()->count())->toBe(0); })->with([['mode', 'additional_approval'], ['scope', 'project'], ['currency', 'USD']]); test('finance and procurement may read company rules but cannot change them', function (PurchasingRole $role) { $user = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'purchasing_role' => $role]); $this->actingAs($user)->get('/regras-investimento')->assertOk(); $this->post('/regras-investimento', $this->data)->assertForbidden(); })->with([PurchasingRole::Finance, PurchasingRole::Procurement]); test('foreign administrators cannot read or approve another company policy', function () { $policy = proposeInvestmentTest($this); $other = receiptFixture(); $user = User::factory()->create(['tenant_id' => $other['tenant']->id, 'purchasing_role' => PurchasingRole::Admin]); $this->actingAs($user)->post('/regras-investimento/'.$policy->id.'/decidir', ['decision' => 'approved', 'reason' => 'Teste'])->assertNotFound(); $this->get('/regras-investimento')->assertOk()->assertInertia(fn ($page) => $page->has('policies', 0)); }); test('audit failure rolls back policy proposal and publication decision', function () { $policy = proposeInvestmentTest($this); $this->mock(RequisitionWorkflow::class)->shouldReceive('audit')->andThrow(new RuntimeException('audit failed')); expect(fn () => reviewInvestmentTest($this, $policy, 1))->toThrow(RuntimeException::class); expect(InvestmentPolicyReview::query()->count())->toBe(0); $this->data['previous_id'] = $policy->id; expect(fn () => proposeInvestmentTest($this))->toThrow(RuntimeException::class); expect(InvestmentPolicy::query()->count())->toBe(1); }); test('an administrator losing eligibility before publication blocks the second signature', function () { $policy = proposeInvestmentTest($this); reviewInvestmentTest($this, $policy, 1); $this->admins[1]->forceFill(['purchasing_role' => PurchasingRole::Finance])->save(); expect(fn () => reviewInvestmentTest($this, $policy, 2))->toThrow(ValidationException::class); expect($policy->refresh()->status)->toBe('pending'); }); test('public investment example contains no company policies', function () { auth()->forgetGuards(); $this->get('/consulta/investimento')->assertOk()->assertInertia(fn ($page) => $page->where('initialScreen', 'investment')->where('auth.user', null)->missing('policies')->missing('reviews')); });