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()); }); function supplierReportReference(bool $otherProvince = false): array { $record = collect(app(AdministrativeGeography::class)->catalogue()['records'])->first(fn ($record) => $record['selectable'] && $record['level'] === 4 && ($otherProvince ? $record['codes'][0] !== '01' : $record['codes'][0] === '01')); return ['country' => 'MZ', 'catalogue' => config('geography.source_sha256'), 'record_id' => $record['id']]; } test('supplier territory report shares directory classification and preserves approved evaluation snapshots', function () { $this->withoutVite(); config(['geography.enabled' => true]); $supplier = Supplier::query()->findOrFail($this->evaluation->supplier_id); $supplier->update(['geography_reference' => supplierReportReference()]); $measure = reportMeasure($this->evaluation, $this->f['issuer']); $before = []; foreach (['suppliers', 'supplier_evaluations', 'supplier_improvements', 'audit_logs'] as $table) { $before[$table] = DB::table($table)->orderBy('id')->get()->toArray(); } $this->get($this->base.'?location=2')->assertInertia(fn (Assert $page) => $page->where('records.total', 1)->where('records.data.0.id', $measure->id)->where('records.data.0.supplier', 'Fornecedor preservado QA')->where('counts.open', 1)->where('overdue', 1)->where('geographyFilter.path.0.name', 'Niassa')); $this->get('/fornecedores?location=2')->assertInertia(fn (Assert $page) => $page->has('suppliers.data', 1)->where('suppliers.data.0.id', $supplier->id)); expect($this->get($this->base.'.csv?location=2')->assertOk()->streamedContent())->toContain('Niassa', 'localizacao_actual_da_ficha', 'Fornecedor preservado QA'); foreach ($before as $table => $rows) { expect(DB::table($table)->orderBy('id')->get()->toArray())->toEqual($rows); } }); test('supplier territory report uses current registration without rewriting the evaluation when geography changes', function () { $this->withoutVite(); config(['geography.enabled' => true]); $supplier = Supplier::query()->findOrFail($this->evaluation->supplier_id); $supplier->update(['geography_reference' => supplierReportReference()]); reportMeasure($this->evaluation, $this->f['issuer']); $original = $this->evaluation->fresh()->getAttributes(); $this->get($this->base.'?location=2')->assertInertia(fn (Assert $page) => $page->where('records.total', 1)); $supplier->update(['geography_reference' => supplierReportReference(true)]); $this->get($this->base.'?location=2')->assertInertia(fn (Assert $page) => $page->where('records.total', 0)); $this->get($this->base)->assertInertia(fn (Assert $page) => $page->where('records.total', 1)); expect($this->evaluation->refresh()->getAttributes())->toBe($original); }); test('supplier territory report keeps historical revisions suspended suppliers and complete CSV across pages', function () { $this->withoutVite(); config(['geography.enabled' => true]); Supplier::query()->findOrFail($this->evaluation->supplier_id)->update(['geography_reference' => supplierReportReference(), 'status' => 'suspended']); $revision = reportEvaluation($this->f, null, 2, $this->evaluation->id); foreach (range(1, 26) as $index) { reportMeasure($index === 26 ? $revision : $this->evaluation, $this->f['issuer'], ['title' => 'Território '.$index, 'status' => $index === 1 ? 'closed' : 'open']); } $this->get($this->base.'?location=2')->assertInertia(fn (Assert $page) => $page->where('records.total', 26)->has('records.data', 25)->where('counts.open', 25)->where('counts.closed', 1)->where('overdue', 25)); $response = $this->get($this->base.'?location=2&page=2')->assertInertia(fn (Assert $page) => $page->has('records.data', 1)->where('records.data.0.revision', 2)); expect($response->inertiaProps('records.prev_page_url'))->toContain('location=2'); $csv = $this->get($this->base.'.csv?location=2&page=2')->assertOk()->streamedContent(); $rows = array_map(fn ($line) => str_getcsv($line, ';', '"', ''), explode("\n", trim($csv))); expect($rows)->toHaveCount(29)->and($rows[2])->toHaveCount(17)->and($rows[3][4])->toBe('Território 1')->and($rows[28][4])->toBe('Território 26'); }); test('supplier territory report does not infer location from address or obsolete and foreign country references', function () { $this->withoutVite(); config(['geography.enabled' => true]); $supplier = Supplier::query()->findOrFail($this->evaluation->supplier_id); $supplier->update(['address' => 'Niassa / CIDADE DE LICHINGA / Urbana nº 1']); reportMeasure($this->evaluation, $this->f['issuer']); foreach ([null, [...supplierReportReference(), 'catalogue' => str_repeat('0', 64)], [...supplierReportReference(), 'country' => 'ZA']] as $reference) { $supplier->update(['geography_reference' => $reference]); $this->get($this->base.'?location=2')->assertInertia(fn (Assert $page) => $page->where('records.total', 0)); $this->get('/fornecedores?location=2')->assertInertia(fn (Assert $page) => $page->has('suppliers.data', 0)); } $this->get($this->base)->assertInertia(fn (Assert $page) => $page->where('records.total', 1)); }); test('supplier territory report combines native filters and retains department and tenant boundaries', function () { $this->withoutVite(); config(['geography.enabled' => true]); Supplier::query()->findOrFail($this->evaluation->supplier_id)->update(['geography_reference' => supplierReportReference()]); reportMeasure($this->evaluation, $this->f['issuer'], ['title' => 'Visível territorial', 'status' => 'returned']); $department = Department::query()->create(['tenant_id' => $this->f['tenant']->id, 'name' => 'Outra área']); reportMeasure(reportEvaluation($this->f, $department->id), $this->f['issuer'], ['title' => 'OCULTA-AREA']); $foreign = receiptFixture(); Supplier::query()->findOrFail($foreign['order']->supplier_id)->update(['geography_reference' => supplierReportReference()]); reportMeasure(reportEvaluation($foreign), $foreign['issuer'], ['title' => 'OCULTA-EMPRESA']); $head = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'department_id' => $this->f['department']->id, 'purchasing_role' => PurchasingRole::DepartmentHead]); $this->actingAs($head); $query = '?location=2&status=returned&supplier_id='.$this->evaluation->supplier_id.'&responsible_id='.$this->f['issuer']->id.'&search=territorial&overdue=1'; $this->get($this->base.$query)->assertInertia(fn (Assert $page) => $page->where('records.total', 1)->where('counts.returned', 1)); $csv = $this->get($this->base.'.csv'.$query)->assertOk()->streamedContent(); expect($csv)->toContain('Visível territorial')->not->toContain('OCULTA-AREA', 'OCULTA-EMPRESA'); }); test('supplier territory report validates territory before export and preserves legacy reports when disabled', function () { $this->withoutVite(); config(['geography.enabled' => true]); reportMeasure($this->evaluation, $this->f['issuer']); $this->getJson($this->base.'?location=999999')->assertUnprocessable(); $this->getJson($this->base.'.csv?location=999999')->assertUnprocessable(); $this->getJson($this->base.'?location[]=2')->assertUnprocessable(); config(['geography.enabled' => false]); $this->getJson($this->base.'?location=2')->assertServiceUnavailable(); $this->getJson($this->base.'.csv?location=2')->assertServiceUnavailable(); $this->get($this->base)->assertInertia(fn (Assert $page) => $page->where('geographyEnabled', false)->where('records.total', 1)); }); test('supplier territory report revalidates a revoked role before reading geographic results', function () { config(['geography.enabled' => true]); $this->f['issuer']->forceFill(['purchasing_role' => PurchasingRole::Requester])->save(); $this->getJson($this->base.'?location=2')->assertForbidden(); $this->getJson($this->base.'.csv?location=2')->assertForbidden(); }); 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); });