assertSame('simulator_fiscal_delta_20261021_test', $identity->database); $this->assertSame(55441, (int) $identity->port); $this->withoutVite(); $this->editor = User::factory()->create(); config(['procurement.enabled' => true, 'portal.managers' => [$this->editor->email]]); $this->organization = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $this->organization, 'name' => 'SINTÉTICO QA — Entidade de ensaio', 'organization_type' => 'private']); $this->actingAs($this->editor); } private function payload(string $regime = 'state'): array { return ['title' => 'SINTÉTICO QA — '.$regime, 'reference' => 'QA-'.Str::uuid(), 'organization_id' => $this->organization, 'regime' => $regime, 'modality' => 'Modalidade documentada de ensaio', 'object' => 'Objecto sintético de concurso para ensaio QA; não corresponde a contratação real.', 'instrument' => $regime === 'state' ? 'Decreto n.º 79/2022 — referência do âmbito estatal' : 'Regras documentadas do procedimento sintético', 'edition' => $regime === 'external' ? 'Banco Mundial, Setembro/2025 — exemplar de referência' : 'QA-2026', 'rule_source' => $regime === 'state' ? 'https://www.ufsa.gov.mz/divnovoreg.php' : 'https://example.invalid/qa-rules', 'origin_published_at' => '2026-10-20T09:00', 'deadline' => '2026-11-20T15:00', 'timezone' => 'Africa/Maputo', 'delivery_channel' => 'Canal externo documentado', 'delivery_address' => 'Endereço sintético para ensaio; não enviar propostas.', 'public_contact' => 'contacto-interno-que-nao-deve-sair@example.test', 'contact_authorized' => false, 'internal_notes' => 'SEGREDO-INTERNO-QA', 'public_reason' => 'Divulgação de anúncio sintético para ensaio QA, sem contratação real.', 'lots' => [['code' => 'L1', 'description' => 'Lote de bens sintéticos', 'quantities' => '10 unidades de ensaio']], 'attachments' => []]; } private function create(array $payload): string { $response = $this->post('/management/concursos', ['version' => 0, 'reason' => 'Preparação sintética documentada para ensaio QA.', 'draft' => $payload])->assertSessionHasNoErrors()->assertRedirect(); return basename(parse_url($response->headers->get('Location'), PHP_URL_PATH)); } private function save(string $id, array $payload): void { $this->put('/management/concursos/'.$id, ['version' => DB::table('portal_tenders')->where('id', $id)->value('version'), 'reason' => 'Actualização sintética de ensaio com histórico conservado.', 'draft' => $payload])->assertRedirect()->assertSessionHasNoErrors(); } private function publish(string $id, string $act = 'initial'): array { $plan = $this->postJson('/management/concursos/'.$id.'/previa', ['act' => $act])->assertOk()->json(); $this->post('/management/concursos/'.$id.'/publicar', ['act' => $act, 'hash' => $plan['hash'], 'reason' => $plan['payload']['public_reason']])->assertRedirect()->assertSessionHasNoErrors(); return $plan; } public function test_incomplete_draft_recovers_without_becoming_public_and_rejects_stale_save(): void { $id = $this->create(['title' => 'Preparação incompleta']); $this->get('/management/concursos/'.$id)->assertOk()->assertHeader('Cache-Control', 'no-store, private')->assertInertia(fn (Assert $p) => $p->component('procurement/edit')->where('record.draft.title', 'Preparação incompleta')); $this->get('/concursos/'.$id)->assertNotFound(); $this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'initial'])->assertUnprocessable()->assertJsonValidationErrors(['organization_id', 'deadline', 'regime']); $this->putJson('/management/concursos/'.$id, ['version' => 0, 'reason' => 'Pedido concorrente obsoleto, sem sobrescrever o rascunho.', 'draft' => ['title' => 'Não guardar']])->assertConflict(); $this->assertSame(1, DB::table('portal_tender_versions')->where('tender_id', $id)->count()); } public function test_state_external_private_regimes_and_filters_are_public_without_ug_ea_or_guarantee_defaults(): void { $ids = []; foreach (['state', 'external', 'private'] as $regime) { $p = $this->payload($regime); $ids[$regime] = $this->create($p); $plan = $this->publish($ids[$regime]); $this->assertArrayNotHasKey('responsible_unit', $plan['payload']); $this->assertArrayNotHasKey('internal_notes', $plan['payload']); $this->assertArrayNotHasKey('public_contact', $plan['payload']); } Auth::logout(); $this->get('/concursos?regime=external&entity='.$this->organization.'&q=SINT')->assertOk()->assertInertia(fn (Assert $p) => $p->component('procurement/index')->where('filters.regime', 'external')->has('items.data', 1)->where('items.data.0.id', $ids['external'])); $this->get('/concursos?q=&entity=§or=&state=open')->assertOk()->assertInertia(fn (Assert $p) => $p->where('filters', ['state' => 'open'])); $this->get('/concursos/'.$ids['private'].'?regime=private&q=SINT&page=2')->assertOk()->assertInertia(fn (Assert $p) => $p->component('procurement/show')->where('backUrl', '/concursos?q=SINT®ime=private&page=2')->where('publication.payload.regime', 'private')->missing('publication.payload.internal_notes')->missing('publication.payload.public_contact')->has('history', 1)); $this->get('/concursos/'.$ids['state'].'/imprimir')->assertOk()->assertInertia(fn (Assert $p) => $p->where('printMode', true)); } public function test_extension_preserves_publication_origin_and_history_and_expiry_is_not_an_award(): void { $p = $this->payload(); $id = $this->create($p); $this->publish($id); $first = DB::table('portal_tender_versions')->where('tender_id', $id)->where('act', 'initial')->first(); $p['deadline'] = '2026-11-25T16:00'; $this->save($id, $p); $this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'correction'])->assertUnprocessable()->assertJsonValidationErrors('act'); $plan = $this->publish($id, 'extension'); $this->assertSame('2026-11-20T15:00', $plan['payload']['deadline_previous']); $this->assertSame('2026-10-20T09:00', $plan['payload']['origin_published_at']); $this->assertSame($first->payload, DB::table('portal_tender_versions')->where('id', $first->id)->value('payload')); $this->travelTo(now()->setDate(2026, 11, 26)); $this->get('/concursos/'.$id)->assertInertia(fn (Assert $p) => $p->where('state', 'deadline_ended')->where('publication.act', 'extension')->has('history', 2)); $this->travelBack(); $p['result'] = 'Resultado sintético de ensaio, não adjudicação real.'; $this->save($id, $p); $this->publish($id, 'award'); $this->get('/concursos/'.$id)->assertInertia(fn (Assert $p) => $p->where('state', 'results')->has('history', 3)); } public function test_cancellation_and_later_clarification_do_not_reopen_the_procedure(): void { $p = $this->payload('private'); $id = $this->create($p); $this->publish($id); $p['result'] = 'Cancelamento sintético fundamentado para ensaio.'; $this->save($id, $p); $this->publish($id, 'cancellation'); $p['object'] .= ' Esclarecimento posterior ao cancelamento.'; $this->save($id, $p); $this->publish($id, 'clarification'); $this->get('/concursos/'.$id)->assertInertia(fn (Assert $p) => $p->where('state', 'cancelled')->has('history', 3)); } public function test_stale_preview_cannot_publish_new_draft_and_public_snapshot_stays_unchanged_until_explicit_publication(): void { $p = $this->payload(); $id = $this->create($p); $plan = $this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'initial'])->assertOk()->json(); $p['title'] = 'Nova edição ainda privada'; $this->save($id, $p); $this->postJson('/management/concursos/'.$id.'/publicar', ['act' => 'initial', 'hash' => $plan['hash'], 'reason' => $p['public_reason']])->assertConflict(); $this->publish($id); $p['title'] = 'Rascunho futuro privado'; $this->save($id, $p); $this->get('/concursos/'.$id)->assertInertia(fn (Assert $p) => $p->where('publication.payload.title', 'Nova edição ainda privada')); } public function test_documented_minimum_dates_and_business_calendar_are_specific_to_the_procedure(): void { $p = $this->payload(); $p['deadline'] = '2026-10-19T15:00'; $id = $this->create($p); $this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'initial'])->assertUnprocessable()->assertJsonValidationErrors('deadline'); $p['origin_published_at'] = '2026-10-23T09:00'; $p['deadline'] = '2026-10-27T09:00'; $p += ['minimum_days' => 2, 'minimum_source' => 'Regra sintética de contagem, cláusula QA-1', 'minimum_edition' => 'QA-2026', 'minimum_modality' => $p['modality'], 'minimum_day_basis' => 'business', 'minimum_working_days' => '1,2,3,4,5', 'minimum_holidays' => '2026-10-26', 'minimum_calendar_source' => 'Calendário sintético QA: segunda a sexta, exclui 26/10. Não é calendário oficial.']; $this->save($id, $p); $this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'initial'])->assertUnprocessable()->assertJsonValidationErrors('deadline'); $p['deadline'] = '2026-10-28T09:00'; $this->save($id, $p); $this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'initial'])->assertOk(); $p['minimum_modality'] = 'Outra modalidade'; $this->save($id, $p); $this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'initial'])->assertUnprocessable()->assertJsonValidationErrors('minimum_modality'); } public function test_simple_account_can_follow_but_cannot_edit_or_publish_and_tracking_is_not_a_bid(): void { $id = $this->create($this->payload()); $this->publish($id); $reader = User::factory()->unverified()->create(); $before = DB::table('portal_role_assignments')->count(); $this->actingAs($reader)->put('/concursos/'.$id.'/acompanhar')->assertRedirect(); $this->get('/concursos/acompanhar')->assertOk()->assertInertia(fn (Assert $p) => $p->where('watched', true)->has('items', 1)->where('items.0.changed', false)); $this->actingAs($this->editor); $p = $this->payload(); $this->save($id, $p); $this->publish($id, 'clarification'); $reader->forceFill(['email_verified_at' => now()])->save(); $this->actingAs($reader); $this->get('/concursos/acompanhar')->assertInertia(fn (Assert $p) => $p->where('items.0.changed', true)); $this->get('/management/concursos')->assertForbidden(); $this->postJson('/management/concursos/'.$id.'/publicar', [])->assertForbidden(); $this->delete('/concursos/'.$id.'/acompanhar')->assertRedirect(); $this->assertSame($before, DB::table('portal_role_assignments')->count()); } public function test_real_scanned_library_attachment_private_redaction_and_withdrawal_are_reused(): void { Storage::fake('local'); config(['queue.default' => 'sync', 'services.malware.scanner' => 'clamd', 'services.malware.socket' => null, 'services.malware.host' => '127.0.0.1', 'services.malware.port' => 3310]); $this->assertTrue(app(MalwareScanner::class)->health()['available'], 'Current real scanner required'); $name = 'synthetic-tender-'.Str::uuid().'.txt'; $this->post('/management/portal-services/upload', ['file' => UploadedFile::fake()->createWithContent($name, 'Exemplo sintético de anúncio QA, sem valor jurídico.')])->assertSessionHasNoErrors(); $doc = DB::table('documents')->where('file_name', $name)->first(); $this->assertSame('clean', $doc->malware_scan_status); $metadata = ['document_id' => $doc->id, 'title' => $name, 'category' => 'Synthetic QA', 'topic' => 'Tender QA', 'document_type' => 'unknown', 'issuer' => 'Synthetic QA only', 'source_url' => 'https://example.invalid/qa', 'language' => 'pt', 'version_label' => 'QA-1', 'territorial_scope' => 'national', 'source_confirmation' => 'identified', 'legal_status' => 'unknown', 'rights_status' => 'authorized', 'data_classification' => 'public', 'source_reference' => 'Synthetic QA only, not an official notice.', 'legislation' => false, 'essential' => false, 'independent_review' => false, 'associations' => []]; $this->post('/management/portal-services/resources', $metadata)->assertSessionHasNoErrors(); $resource = DB::table('portal_resources')->where('document_id', $doc->id)->first(); $this->assertNotNull($resource); $p = $this->payload('external'); $p['attachments'] = [['resource_id' => $resource->id, 'public' => true, 'purpose' => 'Anúncio de ensaio']]; $id = $this->create($p); $this->postJson('/management/concursos/'.$id.'/previa', ['act' => 'initial'])->assertUnprocessable()->assertJsonValidationErrors('attachments'); $this->post('/management/portal-services/resources/'.$resource->id, ['published' => true])->assertSessionHasNoErrors(); $this->publish($id); Auth::logout(); $this->get('/documentos/'.$resource->id.'/download')->assertOk()->assertDownload($name); $this->get('/concursos/'.$id)->assertInertia(fn (Assert $p) => $p->where('publication.payload.attachments.0.available', true)); $this->actingAs($this->editor)->post('/management/portal-services/resources/'.$resource->id, ['published' => false])->assertSessionHasNoErrors(); Auth::logout(); $this->get('/documentos/'.$resource->id.'/download')->assertNotFound(); $this->get('/concursos/'.$id)->assertInertia(fn (Assert $p) => $p->where('publication.payload.attachments.0.available', false)); } }