'admin']); Role::firstOrCreate(['name' => 'contractor-admin']); } public function test_tenant_isolation_is_enforced_between_independent_contractors(): void { // 1. Create two independent contractors $contractorA = Contractor::create([ 'company_name' => 'Contractor A', 'contact_person' => 'Person A', 'email' => 'a@contractor.com', 'status' => 'active', 'payment_terms' => 'net_30', ]); $contractorB = Contractor::create([ 'company_name' => 'Contractor B', 'contact_person' => 'Person B', 'email' => 'b@contractor.com', 'status' => 'active', 'payment_terms' => 'net_30', ]); // 2. Create projects for each $projectA = Project::create([ 'name' => 'Project A', 'contractor_id' => $contractorA->id, 'code' => 'PRJ-2026-001', ]); $projectB = Project::create([ 'name' => 'Project B', 'contractor_id' => $contractorB->id, 'code' => 'PRJ-2026-002', ]); // 3. Create admin user for Contractor B $userB = User::factory()->create([ 'contractor_id' => $contractorB->id, 'user_type' => 'admin', 'status' => 'active', ]); $userB->assignRole('contractor-admin'); // 4. Authenticate as Contractor B user $this->actingAs($userB); // 5. Query projects and verify Contractor B only sees Project B $visibleProjects = Project::all(); $this->assertTrue($visibleProjects->contains($projectB)); $this->assertFalse($visibleProjects->contains($projectA)); } public function test_platform_owners_bypass_tenant_filtering(): void { // 1. Create independent contractors and projects $contractorA = Contractor::create([ 'company_name' => 'Contractor A', 'contact_person' => 'Person A', 'email' => 'a@contractor.com', 'status' => 'active', 'payment_terms' => 'net_30', ]); $contractorB = Contractor::create([ 'company_name' => 'Contractor B', 'contact_person' => 'Person B', 'email' => 'b@contractor.com', 'status' => 'active', 'payment_terms' => 'net_30', ]); $projectA = Project::create([ 'name' => 'Project A', 'contractor_id' => $contractorA->id, 'code' => 'PRJ-2026-001', ]); $projectB = Project::create([ 'name' => 'Project B', 'contractor_id' => $contractorB->id, 'code' => 'PRJ-2026-002', ]); // 2. Create Platform Owner (contractor_id === null) $platformAdmin = User::factory()->create([ 'contractor_id' => null, 'user_type' => 'admin', 'status' => 'active', ]); $platformAdmin->assignRole('admin'); // 3. Authenticate as Platform Owner $this->actingAs($platformAdmin); // 4. Query projects and verify both projects are visible $visibleProjects = Project::all(); $this->assertTrue($visibleProjects->contains($projectA)); $this->assertTrue($visibleProjects->contains($projectB)); } public function test_parent_contractor_can_traverse_subcontractor_hierarchy(): void { // 1. Create Parent Contractor A $parentContractor = Contractor::create([ 'company_name' => 'Parent Contractor', 'contact_person' => 'Parent Person', 'email' => 'parent@contractor.com', 'status' => 'active', 'type' => 'main', 'payment_terms' => 'net_30', ]); // 2. Create Subcontractor B (parent_id = Parent Contractor A) $subContractor = Contractor::create([ 'company_name' => 'Sub Contractor', 'contact_person' => 'Sub Person', 'email' => 'sub@contractor.com', 'status' => 'active', 'type' => 'sub', 'parent_id' => $parentContractor->id, 'payment_terms' => 'net_30', ]); // 3. Create projects $parentProject = Project::create([ 'name' => 'Parent Project', 'contractor_id' => $parentContractor->id, 'code' => 'PRJ-2026-001', ]); $subProject = Project::create([ 'name' => 'Sub Project', 'contractor_id' => $subContractor->id, 'code' => 'PRJ-2026-002', ]); // 4. Create Parent Admin User $parentAdmin = User::factory()->create([ 'contractor_id' => $parentContractor->id, 'user_type' => 'admin', 'status' => 'active', ]); $parentAdmin->assignRole('contractor-admin'); // 5. Authenticate as Parent Admin $this->actingAs($parentAdmin); // 6. Query projects and verify parent admin sees BOTH parent and sub projects $visibleProjects = Project::all(); $this->assertTrue($visibleProjects->contains($parentProject)); $this->assertTrue($visibleProjects->contains($subProject)); // 7. Create Sub Admin User $subAdmin = User::factory()->create([ 'contractor_id' => $subContractor->id, 'user_type' => 'admin', 'status' => 'active', ]); $subAdmin->assignRole('contractor-admin'); // 8. Authenticate as Sub Admin $this->actingAs($subAdmin); // 9. Query projects and verify sub admin ONLY sees sub project $visibleProjectsForSub = Project::all(); $this->assertTrue($visibleProjectsForSub->contains($subProject)); $this->assertFalse($visibleProjectsForSub->contains($parentProject)); } public function test_contractor_sees_global_materials_if_shares_materials_catalog_is_true(): void { // 1. Create a contractor with shares_materials_catalog = true $contractor = Contractor::create([ 'company_name' => 'Sharing Contractor', 'contact_person' => 'Person S', 'email' => 's@contractor.com', 'status' => 'active', 'shares_materials_catalog' => true, 'payment_terms' => 'net_30', ]); // 2. Create custom material and global material $customMaterial = \Modules\MasterData\Models\Material::create([ 'name' => 'Custom Cement', 'unit' => 'bag', 'contractor_id' => $contractor->id, ]); $globalMaterial = \Modules\MasterData\Models\Material::create([ 'name' => 'Global Sand', 'unit' => 'cubic_meter', 'contractor_id' => null, ]); // 3. Authenticate as contractor user $user = User::factory()->create([ 'contractor_id' => $contractor->id, 'user_type' => 'admin', 'status' => 'active', ]); $this->actingAs($user); // 4. Query materials and verify both are visible $visibleMaterials = \Modules\MasterData\Models\Material::all(); $this->assertTrue($visibleMaterials->contains($customMaterial)); $this->assertTrue($visibleMaterials->contains($globalMaterial)); } public function test_contractor_does_not_see_global_materials_if_shares_materials_catalog_is_false(): void { // 1. Create a contractor with shares_materials_catalog = false $contractor = Contractor::create([ 'company_name' => 'Isolated Contractor', 'contact_person' => 'Person I', 'email' => 'i@contractor.com', 'status' => 'active', 'shares_materials_catalog' => false, 'payment_terms' => 'net_30', ]); // 2. Create custom material and global material $customMaterial = \Modules\MasterData\Models\Material::create([ 'name' => 'Private Steel', 'unit' => 'kg', 'contractor_id' => $contractor->id, ]); $globalMaterial = \Modules\MasterData\Models\Material::create([ 'name' => 'Global Sand', 'unit' => 'cubic_meter', 'contractor_id' => null, ]); // 3. Authenticate as contractor user $user = User::factory()->create([ 'contractor_id' => $contractor->id, 'user_type' => 'admin', 'status' => 'active', ]); $this->actingAs($user); // 4. Query materials and verify ONLY custom material is visible $visibleMaterials = \Modules\MasterData\Models\Material::all(); $this->assertTrue($visibleMaterials->contains($customMaterial)); $this->assertFalse($visibleMaterials->contains($globalMaterial)); } }