Files
compras/tests/Feature/SupplierBankTest.php
T
nala 70163c0184
tests / ci (push) Canceled after 0s
commit2
2026-10-28 14:35:06 +02:00

154 lines
8.8 KiB
PHP

<?php
use App\Enums\PurchasingRole;
use App\Models\AuditLog;
use App\Models\Supplier;
use App\Models\SupplierBankDetail;
use App\Models\SupplierBankReview;
use App\Models\User;
use App\Services\RequisitionWorkflow;
use App\Services\SupplierBankWorkflow;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
require_once __DIR__.'/../Support/ReceiptFixture.php';
beforeEach(function () {
$this->f = receiptFixture();
$this->supplier = Supplier::query()->findOrFail($this->f['order']->supplier_id);
$this->buyer = $this->f['issuer'];
$this->first = User::factory()->create(['tenant_id' => $this->supplier->tenant_id, 'purchasing_role' => PurchasingRole::Finance]);
$this->second = User::factory()->create(['tenant_id' => $this->supplier->tenant_id, 'purchasing_role' => PurchasingRole::Finance]);
$this->actingAs($this->buyer);
$this->data = ['previous_id' => 0, 'bank_name' => 'Banco fictício', 'country' => 'MZ', 'currency' => 'MZN', 'beneficiary' => 'Beneficiário fictício de teste', 'account_identifier' => 'TEST-ACCOUNT-0001', 'evidence_reference' => 'Referência fictícia de conferência', 'reason' => 'Cadastro fictício'];
$this->url = '/fornecedores/'.$this->supplier->id.'/dados-bancarios';
});
function bankProposal(object $test): SupplierBankDetail
{
$test->actingAs($test->buyer);
return app(SupplierBankWorkflow::class)->propose($test->buyer, $test->supplier, $test->data);
}
function bankApprove(object $test, User $user, SupplierBankDetail $detail, string $decision = 'approved'): void
{
$test->actingAs($user);
app(SupplierBankWorkflow::class)->review($user, $detail, $decision, 'Conferência independente de teste');
}
test('bank data needs two distinct independent finance reviewers', function () {
$detail = bankProposal($this);
bankApprove($this, $this->first, $detail);
expect(app(SupplierBankWorkflow::class)->state($this->supplier))->toBe('pending');
bankApprove($this, $this->first, $detail);
expect(SupplierBankReview::query()->count())->toBe(1);
bankApprove($this, $this->second, $detail);
expect(app(SupplierBankWorkflow::class)->state($this->supplier))->toBe('approved');
});
test('bank identifiers are encrypted hidden and absent from audit payloads', function () {
$detail = bankProposal($this);
expect(DB::table('supplier_bank_details')->where('id', $detail->id)->value('account_identifier'))->not->toBe($this->data['account_identifier']);
expect($detail->refresh()->account_identifier)->toBe($this->data['account_identifier']);
expect($detail->toArray())->not->toHaveKey('account_identifier')->not->toHaveKey('beneficiary');
expect(json_encode(AuditLog::query()->get()->toArray()))->not->toContain($this->data['account_identifier'])->not->toContain($this->data['beneficiary']);
});
test('new bank revision invalidates prior availability and refuses stale proposals and reviews', function () {
$old = bankProposal($this);
bankApprove($this, $this->first, $old);
bankApprove($this, $this->second, $old);
$this->data['previous_id'] = $old->id;
$this->data['account_identifier'] = 'TEST-ACCOUNT-0002';
$new = bankProposal($this);
expect($new->revision)->toBe(2)->and(app(SupplierBankWorkflow::class)->state($this->supplier))->toBe('pending');
expect(fn () => bankProposal($this))->toThrow(ValidationException::class);
expect(fn () => bankApprove($this, $this->first, $old))->toThrow(ValidationException::class);
expect(SupplierBankDetail::query()->count())->toBe(2);
});
test('author cannot validate after changing to finance role', function () {
$detail = bankProposal($this);
$this->buyer->forceFill(['purchasing_role' => PurchasingRole::Finance])->save();
$this->post($this->url.'/'.$detail->id.'/validar', ['decision' => 'approved', 'reason' => 'Teste', 'independently_verified' => true])->assertForbidden();
});
test('rejection blocks bank readiness and cannot be erased', function () {
$detail = bankProposal($this);
bankApprove($this, $this->first, $detail, 'rejected');
expect(app(SupplierBankWorkflow::class)->state($this->supplier))->toBe('rejected');
expect(fn () => bankApprove($this, $this->second, $detail))->toThrow(ValidationException::class);
expect(fn () => $detail->update(['bank_name' => 'Alterado']))->toThrow(LogicException::class);
expect(fn () => SupplierBankReview::query()->firstOrFail()->delete())->toThrow(LogicException::class);
});
test('bank eligibility changes with reviewer role or supplier suspension', function () {
$detail = bankProposal($this);
bankApprove($this, $this->first, $detail);
bankApprove($this, $this->second, $detail);
$this->first->forceFill(['purchasing_role' => PurchasingRole::Warehouse])->save();
expect(app(SupplierBankWorkflow::class)->state($this->supplier))->toBe('blocked');
$this->first->forceFill(['purchasing_role' => PurchasingRole::Finance])->save();
$this->supplier->update(['status' => 'suspended']);
expect(app(SupplierBankWorkflow::class)->state($this->supplier))->toBe('blocked');
});
test('foreign users cannot propose read or validate bank information', function () {
$detail = bankProposal($this);
$other = receiptFixture();
$user = User::factory()->create(['tenant_id' => $other['tenant']->id, 'purchasing_role' => PurchasingRole::Finance]);
$this->actingAs($user)->get($this->url)->assertNotFound();
$this->post($this->url.'/'.$detail->id.'/validar', ['decision' => 'approved', 'reason' => 'Teste', 'independently_verified' => true])->assertNotFound();
});
test('warehouse and department heads cannot read bank details', function (PurchasingRole $role) {
bankProposal($this);
$user = User::factory()->create(['tenant_id' => $this->supplier->tenant_id, 'purchasing_role' => $role]);
$this->actingAs($user)->get($this->url)->assertForbidden();
})->with([PurchasingRole::Warehouse, PurchasingRole::DepartmentHead, PurchasingRole::Requester]);
test('administration gets metadata only and finance accesses are audited without account numbers', function () {
$detail = bankProposal($this);
$admin = User::factory()->create(['tenant_id' => $this->supplier->tenant_id, 'purchasing_role' => PurchasingRole::Admin]);
$this->actingAs($admin)->get($this->url)->assertOk()->assertInertia(fn ($page) => $page->where('current', null));
$this->actingAs($this->first)->get($this->url)->assertOk()->assertHeader('Cache-Control', 'max-age=0, no-store, private')->assertInertia(fn ($page) => $page->where('current.account_identifier', $this->data['account_identifier']));
expect(AuditLog::query()->where('action', 'supplier.bank_viewed')->count())->toBe(1);
});
test('bank audit failure rolls back proposal and review', function () {
$detail = bankProposal($this);
$this->mock(RequisitionWorkflow::class)->shouldReceive('audit')->andThrow(new RuntimeException('audit failed'));
expect(fn () => bankApprove($this, $this->first, $detail))->toThrow(RuntimeException::class);
expect(SupplierBankReview::query()->count())->toBe(0);
$this->data['previous_id'] = $detail->id;
expect(fn () => bankProposal($this))->toThrow(RuntimeException::class);
expect(SupplierBankDetail::query()->count())->toBe(1);
});
test('invalid bank submission does not flash sensitive input to the session', function () {
$this->post($this->url, [...$this->data, 'country' => 'INVALID'])->assertSessionHasErrors('country');
expect(session()->getOldInput('account_identifier'))->toBeNull()
->and(session()->getOldInput('beneficiary'))->toBeNull()
->and(session()->getOldInput('reason'))->toBeNull();
});
test('bank review requires the independent verification declaration', function () {
$detail = bankProposal($this);
$this->actingAs($this->first)->post($this->url.'/'.$detail->id.'/validar', ['decision' => 'approved', 'reason' => 'Conferência'])->assertSessionHasErrors('independently_verified');
expect(SupplierBankReview::query()->count())->toBe(0);
});
test('supplier conflict blocks a previously approved banking revision', function () {
$detail = bankProposal($this);
bankApprove($this, $this->first, $detail);
bankApprove($this, $this->second, $detail);
$this->actingAs($this->first)->post('/conflitos', ['scope' => 'supplier', 'supplier_id' => $this->supplier->id, 'has_conflict' => true, 'known_since' => today()->format('Y-m-d'), 'reason' => 'Conflito fictício declarado depois da conferência.'])->assertSessionHasNoErrors();
expect(app(SupplierBankWorkflow::class)->state($this->supplier))->toBe('blocked');
});
test('public bank example exposes no company banking details', function () {
auth()->forgetGuards();
$this->get('/consulta/dados-bancarios')->assertOk()->assertInertia(fn ($page) => $page->where('initialScreen', 'bank')->where('auth.user', null)->missing('current')->missing('history'));
});