Files
compras/tests/Feature/InvestmentPolicyTest.php

131 lines
6.9 KiB
PHP

<?php
use App\Enums\PurchasingRole;
use App\Models\InvestmentPolicy;
use App\Models\InvestmentPolicyReview;
use App\Models\User;
use App\Services\InvestmentWorkflow;
use App\Services\RequisitionWorkflow;
use Illuminate\Validation\ValidationException;
require_once __DIR__.'/../Support/ReceiptFixture.php';
require_once __DIR__.'/../Support/InvestmentFixture.php';
beforeEach(function () {
$this->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'));
});