f = paymentFixture(); $this->actingAs($this->f['author']); $this->payment = app(ExternalPaymentWorkflow::class)->register($this->f['author'], $this->f['invoice'], $this->f['payment_data']); }); test('payment task routes the independent reviewer to the exact record without exposing evidence in the task', function () { $this->get('/requisicoes/tarefas?module=finance')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); $this->actingAs($this->f['validator']); $before = [DB::table('external_payments')->get()->toArray(), DB::table('audit_logs')->count(), BudgetConsumption::query()->sum('amount_minor')]; $this->get('/requisicoes/tarefas?module=finance')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1) ->where('tasks.data.0.href', '/pagamentos?payment_id='.$this->payment->id.'#payment-'.$this->payment->id) ->missing('tasks.data.0.external_reference')->missing('tasks.data.0.evidence_reference')->missing('tasks.data.0.bank_snapshot')); expect([DB::table('external_payments')->get()->toArray(), DB::table('audit_logs')->count(), BudgetConsumption::query()->sum('amount_minor')])->toEqual($before); $this->get('/pagamentos?payment_id='.$this->payment->id)->assertHeader('Cache-Control', 'max-age=0, no-store, private') ->assertInertia(fn (Assert $page) => $page->where('focusPaymentId', $this->payment->id)->where('payments.total', 1)->where('payments.data.0.canReview', true)); $this->post('/pagamentos/'.$this->payment->id.'/conferir', ['decision' => 'confirmed', 'reason' => 'Conferência independente sintética', 'independently_verified' => true])->assertRedirect(); $this->get('/requisicoes/tarefas?module=finance')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); expect(app(ExternalPaymentWorkflow::class)->outstanding($this->f['invoice']))->toBe(2000) ->and(BudgetConsumption::query()->sum('amount_minor'))->toEqual($before[2]); }); test('payment focus stays scoped to its invoice and process documents link to its exact record', function () { $this->get('/pagamentos?invoice_id='.$this->f['invoice']->id)->assertInertia(fn (Assert $page) => $page->where('focusInvoiceId', $this->f['invoice']->id)->has('invoices', 1)->where('payments.total', 1)); $this->get('/requisicoes/'.$this->f['r']->id.'/processo')->assertInertia(fn (Assert $page) => $page ->where('stages.7.href', '/facturas?purchase_order_id='.$this->f['order']->id) ->where('documents', fn ($documents) => collect($documents)->contains('href', '/pagamentos?payment_id='.$this->payment->id.'#payment-'.$this->payment->id))); }); test('payment task and review form respect conflicts and revoked finance authority', function () { $this->actingAs($this->f['validator']); $guard = Mockery::mock(ConflictGuard::class)->makePartial(); $guard->shouldReceive('processBlocked')->andReturn(true); app()->instance(ConflictGuard::class, $guard); $this->get('/requisicoes/tarefas?module=finance')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); $this->get('/pagamentos')->assertInertia(fn (Assert $page) => $page->where('payments.data.0.canReview', false)); app()->forgetInstance(ConflictGuard::class); $this->f['validator']->forceFill(['purchasing_role' => PurchasingRole::Admin])->save(); $this->actingAs($this->f['validator']->fresh()); $this->get('/pagamentos')->assertInertia(fn (Assert $page) => $page->where('payments.data.0.canReview', false)->where('payments.data.0.external_reference', null)); $this->post('/pagamentos/'.$this->payment->id.'/conferir', ['decision' => 'confirmed', 'reason' => 'Teste', 'independently_verified' => true])->assertForbidden(); }); test('bank revision does not hide a pending rejection but confirmation still rechecks the bank', function () { $this->actingAs($this->f['issuer']); app(SupplierBankWorkflow::class)->propose($this->f['issuer'], $this->f['supplier'], [...$this->f['bank_data'], 'previous_id' => $this->f['bank']->id, 'account_identifier' => 'TEST-NEW-VERSION']); $this->actingAs($this->f['validator']); $this->get('/requisicoes/tarefas?module=finance')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)); $data = ['decision' => 'confirmed', 'reason' => 'Conferência sintética', 'independently_verified' => true]; $this->postJson('/pagamentos/'.$this->payment->id.'/conferir', $data)->assertUnprocessable(); expect($this->payment->fresh()->status)->toBe('pending'); $this->post('/pagamentos/'.$this->payment->id.'/conferir', [...$data, 'decision' => 'rejected'])->assertRedirect(); $this->get('/requisicoes/tarefas?module=finance')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); }); test('payment and invoice focus cannot cross companies', function () { $other = invoiceFixture(); $foreign = User::factory()->create(['tenant_id' => $other['tenant']->id, 'purchasing_role' => PurchasingRole::Finance]); $this->actingAs($foreign)->getJson('/pagamentos?payment_id='.$this->payment->id)->assertUnprocessable(); $this->getJson('/pagamentos?invoice_id='.$this->f['invoice']->id)->assertUnprocessable(); $this->get('/requisicoes/tarefas?module=finance')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 1)->where('tasks.data.0.id', 'invoice:'.$other['invoice']->id)); }); test('maintenance finance can consult payment without receiving evidence or decision authority', function () { $actor = $this->f['validator']; supplyGrant($actor, ['finance']); DB::table('organization_memberships')->where('user_id', $actor->id)->update(['maintenance' => true, 'expires_at' => now()->addHour()]); $this->actingAs($actor); $this->get('/requisicoes/tarefas?module=finance')->assertInertia(fn (Assert $page) => $page->where('tasks.total', 0)); $this->get('/pagamentos')->assertInertia(fn (Assert $page) => $page->where('canManage', false)->where('payments.data.0.canReview', false)->where('payments.data.0.evidence_reference', null)); $this->post('/pagamentos/'.$this->payment->id.'/conferir', ['decision' => 'rejected', 'reason' => 'Teste', 'independently_verified' => true])->assertForbidden(); expect($this->payment->fresh()->status)->toBe('pending'); });