f = proposalOpeningFixture(); $this->url = '/requisicoes/'.$this->f['r']->id.'/abertura'; $this->actingAs($this->f['issuer']); }); function recordOpening(object $test, array $changes = []): ProposalOpening { $test->post($test->url, [...$test->f['data'], ...$changes])->assertSessionHasNoErrors(); return ProposalOpening::query()->latest('revision')->firstOrFail(); } test('opening is an optional documentary snapshot without approval or financial effects', function () { $opening = recordOpening($this); expect($opening->revision)->toBe(1)->and($opening->snapshot['proposals'][0]['total_minor'])->toBe(10000) ->and($opening->snapshot['proposals'][0])->not->toHaveKey('evaluations')->and(BudgetEntry::query()->count())->toBe(0) ->and($this->f['r']->refresh()->status)->toBe('approved')->and(PurchaseAward::query()->count())->toBe(0); $this->get($this->url)->assertOk()->assertHeader('Cache-Control', 'max-age=0, no-store, private')->assertInertia(fn (Assert $page) => $page->component('purchases/opening')->where('sourceChanged', false)->where('latestId', $opening->id)); $this->get('/requisicoes/'.$this->f['r']->id.'/processo')->assertInertia(fn (Assert $page) => $page->where('documents', fn ($documents) => collect($documents)->contains('kind', 'Abertura'))); }); test('opening corrections append a new version and refuse stale predecessor ids', function () { $first = recordOpening($this); $second = recordOpening($this, ['previous_id' => $first->id, 'notes' => 'Nova observação sem apagar o passado']); expect($second->revision)->toBe(2)->and($first->refresh()->notes)->toBe('Observações fictícias'); $this->post($this->url, [...$this->f['data'], 'previous_id' => $first->id])->assertSessionHasErrors('previous_id'); expect(ProposalOpening::query()->count())->toBe(2); }); test('opening refuses a proposal basis changed after the form was loaded', function () { $this->f['quote']->update(['reference' => 'Referência actualizada']); $this->post($this->url, $this->f['data'])->assertSessionHasErrors('basis_hash'); expect(ProposalOpening::query()->count())->toBe(0); }); test('printed acta preserves supplier and price snapshots after source changes', function () { $opening = recordOpening($this); $name = $opening->snapshot['proposals'][0]['supplier']['name']; Supplier::query()->whereKey($this->f['quote']->supplier_id)->update(['name' => 'Nome posterior fictício']); $this->f['quote']->update(['total_minor' => 20000]); $this->get($this->url)->assertInertia(fn (Assert $page) => $page->where('sourceChanged', true)); $this->get('/actas/'.$opening->id.'/imprimir')->assertOk()->assertHeader('Cache-Control', 'max-age=0, no-store, private')->assertSee($name)->assertDontSee('Nome posterior fictício')->assertSee('100,00')->assertSee('não valida assinaturas'); }); test('opening stores local session time as UTC and prints the declared timezone', function () { $opening = recordOpening($this); expect($opening->opened_at)->toBe($this->f['service_data']['opened_at'])->and($opening->timezone)->toBe('Africa/Johannesburg'); $this->get('/actas/'.$opening->id.'/imprimir')->assertSee('Africa/Johannesburg'); $this->post($this->url, [...$this->f['data'], 'previous_id' => $opening->id, 'opened_at' => now()->addHour()->timezone('Africa/Johannesburg')->format('Y-m-d\TH:i')])->assertSessionHasErrors('opened_at'); }); test('opening validates deadline confirmation and declared participants', function () { $this->post($this->url, [...$this->f['data'], 'confirmed' => false])->assertSessionHasErrors('confirmed'); $this->post($this->url, [...$this->f['data'], 'submission_deadline' => now()->addDay()->timezone('Africa/Johannesburg')->format('Y-m-d\TH:i')])->assertSessionHasErrors('submission_deadline'); $this->post($this->url, [...$this->f['data'], 'participants' => []])->assertSessionHasErrors('participants'); $this->post($this->url, [...$this->f['data'], 'snapshot' => ['signed' => true]])->assertSessionHasErrors('snapshot'); }); test('opening cannot rewrite a process after an award or record a draft request', function () { $this->f['r']->update(['status' => 'draft']); $this->post($this->url, $this->f['data'])->assertSessionHasErrors('opening'); $this->f['r']->update(['status' => 'approved']); PurchaseAward::query()->create($this->f['award_attributes']); $this->post($this->url, $this->f['data'])->assertSessionHasErrors('opening'); expect(ProposalOpening::query()->count())->toBe(0); }); test('opening is restricted by role department company and authentication including print', function () { $opening = recordOpening($this); $print = '/actas/'.$opening->id.'/imprimir'; $this->actingAs($this->f['receiver'])->get($this->url)->assertForbidden(); $this->get($print)->assertForbidden(); $finance = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'purchasing_role' => PurchasingRole::Finance]); $this->actingAs($finance)->get($this->url)->assertOk(); $this->get($print)->assertOk(); $this->post($this->url, [...$this->f['data'], 'previous_id' => $opening->id])->assertForbidden(); $department = Department::query()->create(['tenant_id' => $this->f['tenant']->id, 'name' => 'Outro departamento']); $head = User::factory()->create(['tenant_id' => $this->f['tenant']->id, 'department_id' => $department->id, 'purchasing_role' => PurchasingRole::DepartmentHead]); $this->actingAs($head)->get($print)->assertNotFound(); $other = Tenant::query()->create(['name' => 'Outra empresa fictícia']); $this->actingAs(User::factory()->create(['tenant_id' => $other->id, 'purchasing_role' => PurchasingRole::Admin]))->get($print)->assertNotFound(); auth()->logout(); $this->get($print)->assertRedirect('/login'); }); test('opening rejects revoked procurement roles and never accepts foreign proposals', function () { $original = $this->f; $foreign = proposalOpeningFixture(); $this->actingAs($original['issuer']); $this->post($this->url, [...$original['data'], 'quotation_ids' => [$foreign['quote']->id]])->assertSessionHasErrors('quotation_ids'); DB::table('users')->where('id', $original['issuer']->id)->update(['purchasing_role' => 'finance']); $this->post($this->url, $original['data'])->assertForbidden(); expect(ProposalOpening::query()->count())->toBe(0); }); test('opening audit failure rolls back the entire immutable document', function () { AuditLog::creating(function (AuditLog $event) { if ($event->action === 'opening.recorded') { throw new RuntimeException('Falha fictícia de auditoria'); } }); try { expect(fn () => app(ProposalOpeningWorkflow::class)->record($this->f['issuer'], $this->f['r'], $this->f['service_data']))->toThrow(RuntimeException::class); } finally { AuditLog::flushEventListeners(); } expect(ProposalOpening::query()->count())->toBe(0); }); test('opening documents are immutable and user text is escaped for printing', function () { $opening = recordOpening($this, ['notes' => '']); $this->get('/actas/'.$opening->id.'/imprimir')->assertDontSee('