create(['tenant_id' => $fixture['tenant']->id, 'supplier_id' => $fixture['order']->supplier_id, 'department_id' => $department ?? $fixture['r']->department_id, 'created_by' => $fixture['issuer']->id, 'operation_key' => (string) Str::uuid(), 'creation_hash' => str_repeat('b', 64), 'status' => 'approved', 'contributors' => [$fixture['issuer']->id], 'revision' => $revision, 'previous_id' => $previous, 'supplier_snapshot' => ['id' => $fixture['order']->supplier_id, 'name' => 'Fornecedor preservado QA', 'nuit' => '123456789'], 'payload' => ['title' => 'Avaliação QA', 'order_ids' => [], 'criteria' => [], 'period_start' => null, 'period_end' => null]]); } function reportMeasure(SupplierEvaluation $evaluation, User $responsible, array $extra = []): SupplierImprovement { return SupplierImprovement::query()->create([...['tenant_id' => $evaluation->tenant_id, 'supplier_evaluation_id' => $evaluation->id, 'created_by' => $responsible->id, 'responsible_id' => $responsible->id, 'operation_key' => (string) Str::uuid(), 'creation_hash' => str_repeat('c', 64), 'title' => 'Medida QA', 'description' => 'Objectivo fictício para o relatório.', 'due_on' => '2026-10-29', 'contributors' => [$responsible->id]], ...$extra]); } beforeEach(function () { $this->f = receiptFixture(); $this->actingAs($this->f['issuer']); $this->evaluation = reportEvaluation($this->f); $this->base = '/relatorios/medidas-fornecedores'; $this->travelTo(now()->setDate(2026, 10, 30)->startOfDay()); }); test('report paginates all authorized revisions with counts over the entire filtered set', function () { $next = reportEvaluation($this->f, null, 2, $this->evaluation->id); foreach (range(1, 27) as $i) { reportMeasure($i === 27 ? $next : $this->evaluation, $this->f['issuer'], ['title' => 'Medida '.$i, 'status' => $i === 1 ? 'closed' : 'open']); } $this->get($this->base)->assertInertia(fn (Assert $p) => $p->component('reports/supplier-improvements')->where('records.total', 27)->has('records.data', 25)->where('counts.open', 26)->where('counts.closed', 1)->where('overdue', 26)->has('suppliers', 1)->has('responsibles', 1)->missing('records.data.0.operation_key')->missing('records.data.0.creation_hash')); $this->get($this->base.'?page=2')->assertInertia(fn (Assert $p) => $p->has('records.data', 2)->where('records.data.1.revision', 2)->where('records.total', 27)); }); test('deadline boundaries exclude closed and today from overdue and include the full end date', function () { reportMeasure($this->evaluation, $this->f['issuer'], ['title' => 'Prazo ontem']); reportMeasure($this->evaluation, $this->f['issuer'], ['title' => 'Prazo hoje', 'due_on' => '2026-10-30', 'status' => 'review']); reportMeasure($this->evaluation, $this->f['issuer'], ['title' => 'Encerrada', 'due_on' => '2026-10-01', 'status' => 'closed', 'reviewed_at' => '2026-10-20 10:30:00']); $this->get($this->base.'?overdue=1')->assertInertia(fn (Assert $p) => $p->where('records.total', 1)->where('overdue', 1)->where('records.data.0.title', 'Prazo ontem')->where('records.data.0.overdue', true)); $this->get($this->base.'?from=2026-10-30&to=2026-10-30')->assertInertia(fn (Assert $p) => $p->where('records.total', 1)->where('records.data.0.title', 'Prazo hoje')->where('records.data.0.overdue', false)); $this->get($this->base.'?status=closed&overdue=1')->assertInertia(fn (Assert $p) => $p->where('records.total', 0)->where('overdue', 0)); expect($this->get($this->base.'.csv?status=closed')->assertOk()->streamedContent())->toContain('2026-10-20T10:30:00+00:00'); }); test('supplier assignee state and title filters are combined and invalid ranges refused', function () { reportMeasure($this->evaluation, $this->f['issuer'], ['title' => 'Referência ESPECIAL', 'status' => 'returned']); $url = $this->base.'?supplier_id='.$this->evaluation->supplier_id.'&responsible_id='.$this->f['issuer']->id.'&status=returned&search='.urlencode('referência'); $this->get($url)->assertInertia(fn (Assert $p) => $p->where('records.total', 1)->where('counts.returned', 1)); $this->get($url.'&from=2026-11-01')->assertInertia(fn (Assert $p) => $p->where('records.total', 0)); $this->getJson($this->base.'?from=2026-11-01&to=2026-10-01')->assertUnprocessable()->assertJsonValidationErrors('to'); $this->getJson($this->base.'?status=forged')->assertUnprocessable(); $this->getJson($this->base.'.csv?overdue=0')->assertUnprocessable(); }); test('department and company restrictions apply to records counts choices and CSV', function () { reportMeasure($this->evaluation, $this->f['issuer'], ['title' => 'Visível QA']); $department = Department::query()->create(['tenant_id' => $this->f['tenant']->id, 'name' => 'Outro departamento']); $otherEvaluation = reportEvaluation($this->f, $department->id); $otherResponsible = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'department_id' => $department->id, 'purchasing_role' => PurchasingRole::Finance, 'name' => 'Responsável não exposto']); reportMeasure($otherEvaluation, $otherResponsible, ['title' => 'Oculta pelo departamento']); $foreign = receiptFixture(); reportMeasure(reportEvaluation($foreign), $foreign['issuer'], ['title' => 'Oculta pela empresa']); $head = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'department_id' => $this->f['r']->department_id, 'purchasing_role' => PurchasingRole::DepartmentHead]); $this->actingAs($head)->get($this->base)->assertInertia(fn (Assert $p) => $p->where('records.total', 1)->has('responsibles', 1)->where('responsibles.0.id', $this->f['issuer']->id)); $csv = $this->get($this->base.'.csv')->assertOk()->streamedContent(); expect($csv)->toContain('Visível QA')->not->toContain('Oculta', 'Responsável não exposto'); $this->get($this->base.'?responsible_id='.$otherResponsible->id)->assertInertia(fn (Assert $p) => $p->where('records.total', 0)); }); test('guest forbidden roles and a stale actor role cannot access either representation', function () { foreach ([$this->base, $this->base.'.csv'] as $url) { $this->actingAs($this->f['receiver'])->get($url)->assertForbidden(); } $this->actingAs($this->f['issuer']); DB::table('users')->where('id', $this->f['issuer']->id)->update(['purchasing_role' => 'requester']); $this->get($this->base)->assertForbidden(); $this->get($this->base.'.csv')->assertForbidden(); auth()->logout(); $this->get($this->base)->assertRedirect('/login'); }); test('CSV exports beyond the current page in batches and protects formula-like text without writes', function () { foreach (range(1, 207) as $i) { reportMeasure($this->evaluation, $this->f['issuer'], ['title' => $i === 1 ? '=HYPERLINK("bad")' : 'Linha '.$i, 'result' => $i === 207 ? '+resultado final' : null, 'evidence' => $i === 207 ? '@referencia' : null]); } $supplier = Supplier::query()->findOrFail($this->evaluation->supplier_id)->getAttributes(); $evaluation = $this->evaluation->refresh()->getAttributes(); $audits = AuditLog::query()->count(); $response = $this->get($this->base.'.csv?page=2&status=open')->assertOk(); $csv = $response->streamedContent(); $rows = array_map(fn ($line) => str_getcsv($line, ';', '"', ''), explode("\n", trim($csv))); expect($rows)->toHaveCount(210)->and($rows[3][4])->toBe('\'=HYPERLINK("bad")')->and($rows[209][4])->toBe('Linha 207')->and($rows[209][11])->toBe('\'+resultado final')->and($rows[209][12])->toBe('\'@referencia'); expect($rows[209][16])->toEndWith('/fornecedores/avaliacoes/'.$this->evaluation->id.'/relatorio#medida-207'); expect(Supplier::query()->findOrFail($this->evaluation->supplier_id)->getAttributes())->toBe($supplier)->and($this->evaluation->refresh()->getAttributes())->toBe($evaluation)->and(AuditLog::query()->count())->toBe($audits); });