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')); });