f = IdentityFixture::create('-module-'.bin2hex(random_bytes(6))); foreach (['clients.view','services.view'] as $p) IdentityFixture::grant($this->f['reader']->id,$this->f['tenant'],$this->f['company'],$p); IdentityFixture::grant($this->f['reader']->id,$this->f['tenant'],$this->f['company'],'workshop.view',$this->f['unit']); $this->actingAs($this->f['reader'])->withSession(['auth_version'=>$this->f['reader']->auth_version]); } private function select(?string $module, ?int $company=null) { return $this->postJson('/api/context',['tenant_id'=>$this->f['tenant'],'company_id'=>$company??$this->f['company'],'unit_id'=>$this->f['unit'],'module_id'=>$module]); } public function test_explicit_selection_direct_access_and_old_generation(): void { $this->select(null)->assertOk(); $this->getJson('/api/a/clients')->assertStatus(409); $token=$this->select('commercial')->assertOk()->json('context.token'); $this->withHeader('X-SGF-Context',$token)->getJson('/api/a/clients')->assertOk(); $this->getJson('/api/workshop')->assertStatus(409); $next=$this->select('workshop')->assertOk()->json('context.token'); $this->assertNotSame($token,$next); $this->withHeader('X-SGF-Context',$token)->getJson('/api/workshop')->assertStatus(409); $this->withHeader('X-SGF-Context',$next)->getJson('/api/a/clients')->assertOk()->assertJsonPath('records',[]); $this->postJson('/api/a/clients',['name'=>'Must not be inserted'])->assertForbidden(); $this->assertFalse(DB::table('a_clients')->where('name','Must not be inserted')->exists()); } public function test_company_configuration_and_revocation_are_checked_again(): void { $token=$this->select('commercial')->assertOk()->json('context.token'); DB::table('permission_grants')->where('user_id',$this->f['reader']->id)->whereIn('permission',['clients.view','services.view'])->delete(); $this->withHeader('X-SGF-Context',$token)->getJson('/api/a/clients')->assertForbidden(); $this->getJson('/api/session')->assertOk()->assertJsonPath('context.module_id',null); $this->select('commercial')->assertForbidden(); $this->select('workshop',$this->f['sibling'])->assertForbidden(); config(['modules.companies.'.$this->f['tenant'].':'.$this->f['company']=>['contracted'=>['workshop'],'enabled'=>[]]]); $this->select('workshop')->assertForbidden(); } public function test_development_discovery_never_grants_operations_or_applies_in_production(): void { config(['modules.contracted'=>[], 'modules.development_profiles'=>[$this->f['reader']->id.':'.$this->f['tenant'].':'.$this->f['company']]]); $selected=$this->select('purchasing')->assertOk(); $this->assertCount(6,$selected->json('context.modules')); $this->withHeader('X-SGF-Context',$selected->json('context.token'))->getJson('/api/supplier-payables')->assertForbidden(); app()->detectEnvironment(fn()=>'production'); $this->getJson('/api/session')->assertOk()->assertJsonPath('context.modules',[])->assertJsonPath('context.module_id',null); } public function test_failed_selection_preserves_context_and_audit_contains_module_without_token(): void { $selected=$this->select('commercial')->assertOk(); $this->select('invented')->assertForbidden(); $session=$this->getJson('/api/session')->assertOk(); $session->assertJsonPath('context.module_id','commercial')->assertJsonPath('context.token',$selected->json('context.token')); $audit=DB::table('identity_audit')->where('actor_id',$this->f['reader']->id)->where('action','context.selected')->orderByDesc('id')->first(); $this->assertSame('commercial',json_decode($audit->details,true)['module_id']); $this->assertStringNotContainsString($selected->json('context.token'),$audit->details); } public function test_local_environment_and_copied_profile_cannot_enable_discovery_for_client_database_configuration(): void { $this->select(null)->assertOk(); config(['modules.contracted'=>[], 'modules.development_profiles'=>[$this->f['reader']->id.':'.$this->f['tenant'].':'.$this->f['company']]]); $connection=config('database.default');$original=config("database.connections.$connection.database"); config(["database.connections.$connection.database"=>'sgf_tempo_dev']); app()->detectEnvironment(fn()=>'local'); try { $this->getJson('/api/session')->assertOk()->assertJsonPath('context.modules',[]); } finally { config(["database.connections.$connection.database"=>$original]);app()->detectEnvironment(fn()=>'testing'); } } public function test_shared_records_require_explicit_association_and_company_scope(): void { $actor=$this->f['manager']; foreach(['clients.view','clients.create','clients.edit'] as $p)IdentityFixture::grant($actor->id,$this->f['tenant'],$this->f['company'],$p); IdentityFixture::grant($actor->id,$this->f['tenant'],$this->f['company'],'workshop.view',$this->f['unit']); $this->actingAs($actor)->withSession(['auth_version'=>$actor->auth_version]); $token=$this->select('commercial')->assertOk()->json('context.token'); $this->withHeaders(['X-SGF-Context'=>$token,'Idempotency-Key'=>'module-client-proof-001']); $id=$this->postJson('/api/a/clients',['name'=>'Module proof client'])->assertCreated()->json('id'); $this->getJson('/api/a/clients')->assertOk()->assertJsonPath('records.0.id',$id); $token=$this->select('workshop')->assertOk()->json('context.token'); $this->withHeader('X-SGF-Context',$token)->getJson('/api/a/clients')->assertOk()->assertJsonPath('records',[]); $this->getJson('/api/a/clients/'.$id)->assertForbidden(); $this->getJson('/api/module-records')->assertOk()->assertJsonPath('records.clients.0.id',$id); $this->postJson('/api/module-records',['area'=>'clients','id'=>$id,'version'=>1,'module_id'=>'workshop','reason'=>'Explicit synthetic association'])->assertOk(); $this->getJson('/api/a/clients/'.$id)->assertOk()->assertJsonPath('name','Module proof client'); $this->assertSame(1,DB::table('a_clients')->where('id',$id)->value('version')); $this->assertSame(2,DB::table('identity_audit')->where('action','module.record.associated')->where('company_id',$this->f['company'])->count()); $this->postJson('/api/module-records',['area'=>'clients','id'=>$id,'version'=>1,'module_id'=>'workshop','reason'=>'Correct association without rewriting original','operation'=>'remove'])->assertOk(); $this->getJson('/api/a/clients/'.$id)->assertForbidden(); $this->getJson('/api/module-records')->assertOk()->assertJsonPath('records.clients.0.modules',['commercial']); $this->assertSame(1,DB::table('a_clients')->where('id',$id)->value('version')); $this->postJson('/api/module-records',['area'=>'clients','id'=>$id,'version'=>1,'module_id'=>'workshop','reason'=>'Re-association explicitly reviewed'])->assertOk(); $this->getJson('/api/a/clients/'.$id)->assertOk(); IdentityFixture::grant($actor->id,$this->f['tenant'],$this->f['sibling'],'clients.view'); $token=$this->postJson('/api/context',['tenant_id'=>$this->f['tenant'],'company_id'=>$this->f['sibling'],'unit_id'=>$this->f['siblingUnit'],'module_id'=>'commercial'])->assertOk()->json('context.token'); $this->withHeader('X-SGF-Context',$token)->getJson('/api/a/clients/'.$id)->assertForbidden(); $this->getJson('/api/a/clients')->assertOk()->assertJsonPath('records',[]); } public function test_unassociated_legacy_records_remain_in_explicit_administrative_inventory(): void { $id=DB::table('a_clients')->insertGetId(['tenant_id'=>$this->f['tenant'],'company_id'=>$this->f['company'],'unit_id'=>null,'name'=>'Existing unassociated record','version'=>1,'data'=>'{}','created_at'=>now(),'updated_at'=>now()]); $token=$this->select('commercial')->assertOk()->json('context.token'); $this->withHeader('X-SGF-Context',$token)->getJson('/api/a/clients')->assertOk()->assertJsonPath('records',[])->assertJsonStructure(['scope_notice']); $this->getJson('/api/a/clients/'.$id)->assertForbidden(); $this->getJson('/api/module-records')->assertForbidden(); IdentityFixture::grant($this->f['reader']->id,$this->f['tenant'],$this->f['company'],'access.manage'); $this->getJson('/api/module-records')->assertOk()->assertJsonPath('records.clients.0.id',$id)->assertJsonPath('records.clients.0.modules',[]); $this->postJson('/api/module-records',['area'=>'clients','id'=>$id,'version'=>1,'module_id'=>'commercial','reason'=>'Cannot assign without edit permission'])->assertForbidden(); $this->assertSame('Existing unassociated record',DB::table('a_clients')->where('id',$id)->value('name')); } public function test_draft_creation_preserves_source_history_and_does_not_share_dependencies_implicitly(): void { $actor=$this->f['manager']; foreach(['clients','services','service_drafts','billing_drafts'] as $area){ foreach(['view','create','edit'] as $action)IdentityFixture::grant($actor->id,$this->f['tenant'],$this->f['company'],"$area.$action",str_ends_with($area,'_drafts')?$this->f['unit']:null); } IdentityFixture::grant($actor->id,$this->f['tenant'],$this->f['company'],'workshop.view',$this->f['unit']); $this->actingAs($actor)->withSession(['auth_version'=>$actor->auth_version]); $token=$this->select('commercial')->assertOk()->json('context.token'); $write=function($area,$body)use($token){return $this->withHeaders(['X-SGF-Context'=>$token,'Idempotency-Key'=>(string)\Illuminate\Support\Str::uuid()])->postJson('/api/a/'.$area,$body)->assertCreated()->json('id');}; $client=$write('clients',['name'=>'Synthetic modular client']); $service=$write('services',['name'=>'Synthetic modular service']); $draft=$write('service_drafts',['client_id'=>$client,'currency'=>'MZN','lines'=>[['service_id'=>$service,'quantity'=>'2','unit_price'=>'75']]]); $bill=$write('billing_drafts',['source_id'=>$draft]); $original=$this->getJson('/api/a/billing_drafts/'.$bill)->assertOk()->json(); $this->assertSame($draft,$original['source_id']); $token=$this->select('workshop')->assertOk()->json('context.token'); $this->withHeader('X-SGF-Context',$token)->postJson('/api/module-records',['area'=>'billing_drafts','id'=>$bill,'version'=>99,'module_id'=>'workshop','reason'=>'Reject stale review'])->assertStatus(409); $this->postJson('/api/module-records',['area'=>'billing_drafts','id'=>$bill,'version'=>1,'module_id'=>'workshop','reason'=>'Explicitly share this preserved draft'])->assertOk(); $record=$this->getJson('/api/a/billing_drafts/'.$bill)->assertOk()->json(); $this->assertSame($original['data'],$record['data']); $this->assertSame($original['history'],$record['history']); $this->assertSame($draft,$record['source_id']); $this->getJson('/api/a/service_drafts/'.$draft)->assertForbidden(); $this->getJson('/api/a/clients/'.$client)->assertForbidden(); $this->getJson('/api/a/services/'.$service)->assertForbidden(); $this->assertSame(1,DB::table('a_billing_drafts')->where('id',$bill)->value('version')); } }