f = receiptFixture(); $this->actor = $this->f['issuer']; $this->actor->forceFill(['purchasing_role' => PurchasingRole::Admin])->save(); $this->f['tenant']->update(['rules_provisional' => false, 'work_context_policy' => ['revision' => 1, 'enabled' => array_keys(WorkContext::OPTIONS)]]); config(['work_context.entitlements.'.$this->actor->tenant_id => array_keys(WorkContext::OPTIONS)]); $this->actingAs($this->actor); }); test('selection intersects product configuration and permission without granting functions', function () { $before = app(OrganizationAccess::class)->permissions($this->actor); config(['work_context.entitlements.'.$this->actor->tenant_id => ['procurement.internal']]); $this->post('/contexto', ['area' => 'people', 'option' => 'people.internal'])->assertForbidden(); $this->post('/contexto', ['area' => 'procurement'])->assertRedirect('/dashboard'); expect(session('work_context.key'))->toBe('procurement.internal') ->and(app(OrganizationAccess::class)->permissions($this->actor))->toBe($before); $this->get('/requisicoes')->assertOk(); $this->get('/pessoas')->assertForbidden(); $this->get('/pessoas/relatorios')->assertForbidden(); $this->get('/pessoas/relatorios.csv')->assertForbidden(); $this->post('/pessoas', [])->assertForbidden(); }); test('two choices require explicit option and none exposes no business data on dashboard', function () { $this->post('/contexto', ['area' => 'procurement'])->assertForbidden(); $this->get('/dashboard')->assertOk()->assertInertia(fn (Assert $page) => $page->component('work-context')->missing('stats')->missing('recent')->where('context.active', null)); config(['work_context.entitlements.'.$this->actor->tenant_id => []]); $this->get('/dashboard')->assertInertia(fn (Assert $page) => $page->where('context.available', [])); $this->get('/requisicoes')->assertForbidden(); }); test('context changes invalidate old area operations and do not change submitted records', function () { $before = $this->f['r']->fresh()->toArray(); $this->post('/contexto', ['area' => 'procurement', 'option' => 'procurement.internal'])->assertRedirect(); $this->post('/contexto', ['area' => 'people', 'option' => 'people.internal'])->assertRedirect(); $this->get('/requisicoes/'.$this->f['r']->id)->assertNotFound(); $this->get('/requisicoes/'.$this->f['r']->id.'/relatorio')->assertNotFound(); $this->post('/requisicoes/'.$this->f['r']->id.'/submeter')->assertNotFound(); expect(Requisition::withoutGlobalScope('work_context')->findOrFail($this->f['r']->id)->toArray())->toBe($before); }); test('commercial withdrawal invalidates active context and pending options cannot reuse internal workflows', function () { $this->post('/contexto', ['area' => 'procurement', 'option' => 'procurement.internal'])->assertRedirect(); config(['work_context.entitlements.'.$this->actor->tenant_id => ['procurement.sales']]); $this->get('/requisicoes')->assertForbidden(); $this->post('/contexto', ['area' => 'procurement', 'option' => 'procurement.sales'])->assertRedirect(); $this->get('/requisicoes')->assertRedirect('/dashboard'); $this->post('/requisicoes', [])->assertStatus(409); }); test('people reports in a pending modality return to the context panel without exposing internal data', function () { $this->post('/contexto', ['area' => 'people', 'option' => 'people.third_party'])->assertRedirect('/dashboard'); $this->get('/pessoas/relatorios')->assertRedirect('/dashboard'); $this->get('/pessoas/relatorios.csv')->assertRedirect('/dashboard'); $this->get('/dashboard')->assertOk()->assertInertia(fn (Assert $page) => $page ->component('work-context')->where('context.active', 'people.third_party')->missing('processes')); $this->post('/pessoas', [])->assertStatus(409); $this->post('/contexto', ['area' => 'people', 'option' => 'people.internal'])->assertRedirect('/dashboard'); $this->get('/pessoas/relatorios')->assertOk()->assertInertia(fn (Assert $page) => $page->component('people/reports')); }); test('context is bound to account and company and rejects area mismatches', function () { $this->post('/contexto', ['area' => 'procurement', 'option' => 'people.internal'])->assertForbidden(); $this->post('/contexto', ['area' => 'procurement', 'option' => 'procurement.internal'])->assertRedirect(); $other = User::factory()->create(['tenant_id' => $this->actor->tenant_id, 'department_id' => $this->actor->department_id, 'purchasing_role' => PurchasingRole::Admin]); $this->actingAs($other)->get('/requisicoes')->assertForbidden(); expect(AuditLog::query()->where('action', 'work_context.selected')->count())->toBe(1); }); test('area profiles do not combine permissions from other contexts and expired membership grants nothing', function () { $tenant = $this->actor->tenant_id; DB::table('organization_access')->insert(['tenant_id' => $tenant, 'active' => true, 'version' => 1, 'created_at' => now(), 'updated_at' => now()]); foreach ([['procurement.internal', ['requester']], ['people.internal', ['admin']]] as [$key, $permissions]) { $profile = DB::table('organization_profiles')->insertGetId(['tenant_id' => $tenant, 'name' => $key, 'permissions' => json_encode($permissions), 'work_contexts' => json_encode([$key]), 'version' => 1, 'created_at' => now(), 'updated_at' => now()]); DB::table('organization_memberships')->insert(['tenant_id' => $tenant, 'user_id' => $this->actor->id, 'profile_id' => $profile, 'maintenance' => false, 'created_at' => now(), 'updated_at' => now()]); } $this->post('/contexto', ['area' => 'procurement'])->assertRedirect(); $this->get('/dashboard')->assertInertia(fn (Assert $page) => $page->where('context.permissions', ['requester'])); $this->get('/requisicoes')->assertInertia(fn (Assert $page) => $page->where('organizationAccess.permissions', ['requester'])); DB::table('organization_memberships')->where('tenant_id', $tenant)->update(['expires_at' => now()->subMinute()]); $this->get('/requisicoes')->assertForbidden(); }); test('product configuration cannot enable unpurchased options and changes are versioned audited', function () { config(['work_context.entitlements.'.$this->actor->tenant_id => ['procurement.internal']]); $body = ['version' => 1, 'enabled' => ['people.internal'], 'confirm' => true, 'reason' => 'Definir as opções adquiridas pela empresa.']; $this->postJson('/administracao/parametrizacao/work_contexts', $body)->assertUnprocessable(); $body['enabled'] = ['procurement.internal']; $this->postJson('/administracao/parametrizacao/work_contexts', $body)->assertRedirect(); $this->postJson('/administracao/parametrizacao/work_contexts', $body)->assertUnprocessable(); expect($this->f['tenant']->fresh()->work_context_policy)->toBe(['revision' => 2, 'enabled' => ['procurement.internal']]) ->and(AuditLog::query()->where('action', 'organization.work_contexts')->count())->toBe(1); }); test('SGF composition excludes third party hiring even if installation requests it', function () { config(['work_context.composition' => 'sgf']); expect(app(WorkContext::class)->purchased($this->actor))->not->toContain('people.third_party'); $this->post('/contexto', ['area' => 'people', 'option' => 'people.third_party'])->assertForbidden(); }); test('origin classification is internal and SQL cannot transfer a process to another context', function () { expect($this->f['r']->fresh()->work_context_key)->toBe('procurement.internal'); expect(fn () => DB::table('requisitions')->where('id', $this->f['r']->id)->update(['work_context_key' => 'procurement.sales']))->toThrow(QueryException::class); }); test('RH boundary refuses selection and draft contracts and performs no external request', function () { Http::fake(); foreach (['selected', 'selection_review', 'contract_draft'] as $status) { $record = new PeopleProcess(['tenant_id' => $this->actor->tenant_id, 'status' => $status, 'work_context_key' => 'people.internal', 'contract' => ['status' => 'draft']]); expect(fn () => app(PeopleContractHandoff::class)->envelope($record))->toThrow(HttpException::class); } Http::assertNothingSent(); }); test('delivery check refuses the development fixture without deleting its records', function () { $before = DB::table('requisitions')->count(); $this->artisan('sgc:delivery-check', ['tenant' => $this->actor->tenant_id])->assertFailed(); expect(DB::table('requisitions')->count())->toBe($before); }); test('temporary maintenance may select its authorised context but cannot execute business commands', function () { $tenant = $this->actor->tenant_id; DB::table('organization_access')->insert(['tenant_id' => $tenant, 'active' => true, 'version' => 1, 'created_at' => now(), 'updated_at' => now()]); $profile = DB::table('organization_profiles')->insertGetId(['tenant_id' => $tenant, 'name' => 'Consulta temporária', 'permissions' => json_encode(['procurement']), 'work_contexts' => json_encode(['procurement.internal']), 'version' => 1, 'created_at' => now(), 'updated_at' => now()]); DB::table('organization_memberships')->insert(['tenant_id' => $tenant, 'user_id' => $this->actor->id, 'profile_id' => $profile, 'maintenance' => true, 'expires_at' => now()->addHour(), 'created_at' => now(), 'updated_at' => now()]); $this->post('/contexto', ['area' => 'procurement'])->assertRedirect('/dashboard'); $this->get('/requisicoes')->assertOk(); $this->post('/requisicoes', [])->assertForbidden(); $this->post('/administracao/parametrizacao/work_contexts', [])->assertForbidden(); });