'admin']); Role::firstOrCreate(['name' => 'contractor-admin']); Role::firstOrCreate(['name' => 'Super Admin']); Role::firstOrCreate(['name' => 'Site Technical']); } 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_users_page_data_is_visible_to_platform_admin_but_isolated_for_contractor_admins(): void { $contractorA = Contractor::create([ 'company_name' => 'Users Contractor A', 'contact_person' => 'Person A', 'email' => 'users-a@contractor.com', 'status' => 'active', 'payment_terms' => 'net_30', ]); $contractorB = Contractor::create([ 'company_name' => 'Users Contractor B', 'contact_person' => 'Person B', 'email' => 'users-b@contractor.com', 'status' => 'active', 'payment_terms' => 'net_30', ]); $userA = User::factory()->create(['contractor_id' => $contractorA->id]); $userB = User::factory()->create(['contractor_id' => $contractorB->id]); $platformAdmin = User::factory()->create(['contractor_id' => null, 'user_type' => 'admin']); $this->actingAs($platformAdmin); $this->assertTrue(User::all()->contains($userA)); $this->assertTrue(User::all()->contains($userB)); $this->actingAs($userA); $visibleUsers = User::all(); $this->assertTrue($visibleUsers->contains($userA)); $this->assertFalse($visibleUsers->contains($userB)); } 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_platform_super_admin_with_legacy_contractor_link_sees_all_users(): void { $contractorA = Contractor::create([ 'company_name' => 'Legacy Link Contractor A', 'contact_person' => 'Person A', 'email' => 'legacy-a@contractor.com', 'status' => 'active', 'payment_terms' => 'net_30', ]); $contractorB = Contractor::create([ 'company_name' => 'Legacy Link Contractor B', 'contact_person' => 'Person B', 'email' => 'legacy-b@contractor.com', 'status' => 'active', 'payment_terms' => 'net_30', ]); $userA = User::factory()->create(['contractor_id' => $contractorA->id]); $userB = User::factory()->create(['contractor_id' => $contractorB->id]); $platformAdmin = User::factory()->create([ 'contractor_id' => $contractorA->id, 'user_type' => 'admin', ]); $platformAdmin->assignRole('Super Admin'); $this->actingAs($platformAdmin); $visibleUsers = User::all(); $this->assertTrue($visibleUsers->contains($userA)); $this->assertTrue($visibleUsers->contains($userB)); } 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 = Material::create([ 'name' => 'Custom Cement', 'unit' => 'bag', 'contractor_id' => $contractor->id, ]); $globalMaterial = 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 = 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 = Material::create([ 'name' => 'Private Steel', 'unit' => 'kg', 'contractor_id' => $contractor->id, ]); $globalMaterial = 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 = Material::all(); $this->assertTrue($visibleMaterials->contains($customMaterial)); $this->assertFalse($visibleMaterials->contains($globalMaterial)); } public function test_project_code_generation_is_globally_unique_across_tenants(): 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 admin user for Contractor A $userA = User::factory()->create([ 'contractor_id' => $contractorA->id, 'user_type' => 'admin', 'status' => 'active', ]); $userA->assignRole('contractor-admin'); // 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. Act as Contractor A admin and create a project without code $this->actingAs($userA); $projectA = Project::create([ 'name' => 'Project A for Contractor A', ]); // Verify Project A got code PRJ-{year}-001 $year = now()->year; $this->assertEquals(sprintf('PRJ-%d-001', $year), $projectA->code); // 5. Act as Contractor B admin and create a project without code $this->actingAs($userB); $projectB = Project::create([ 'name' => 'Project B for Contractor B', ]); // Verify Project B got code PRJ-{year}-002, bypassing the TenantScope of Contractor B // and avoiding duplicate key violation PRJ-{year}-001 $this->assertEquals(sprintf('PRJ-%d-002', $year), $projectB->code); } public function test_contractor_admin_sees_projects_assigned_through_project_contractor(): void { $contractor = Contractor::create([ 'company_name' => 'Assigned Contractor', 'contact_person' => 'Person A', 'email' => 'assigned@contractor.com', 'status' => 'active', 'payment_terms' => 'net_30', ]); $assignedProject = Project::create([ 'name' => 'Assigned Project', 'code' => 'PRJ-2026-101', 'contractor_id' => null, ]); $unrelatedProject = Project::create([ 'name' => 'Unrelated Project', 'code' => 'PRJ-2026-102', 'contractor_id' => null, ]); $contractor->projects()->attach($assignedProject->id, ['role' => 'subcontractor']); $user = User::factory()->create([ 'contractor_id' => $contractor->id, 'user_type' => 'admin', 'status' => 'active', ]); $user->assignRole('contractor-admin'); $this->actingAs($user); $visibleProjects = Project::all(); $this->assertTrue($visibleProjects->contains($assignedProject)); $this->assertFalse($visibleProjects->contains($unrelatedProject)); } public function test_site_operations_user_sees_only_projects_assigned_to_their_personnel_record(): void { $siteUser = User::factory()->create([ 'user_type' => 'employee', 'status' => 'active', ]); $siteUser->assignRole('Site Technical'); $assignedProject = Project::create([ 'name' => 'Site Assigned Project', 'code' => 'PRJ-2026-111', ]); $unrelatedProject = Project::create([ 'name' => 'Site Unrelated Project', 'code' => 'PRJ-2026-112', ]); $assignedProject->personnel()->attach($siteUser->id, ['role' => 'site_technical']); $this->actingAs($siteUser); $visibleProjects = Project::all(); $this->assertTrue($visibleProjects->contains($assignedProject)); $this->assertFalse($visibleProjects->contains($unrelatedProject)); } public function test_site_operations_user_with_contractor_link_sees_contractor_projects(): void { $contractor = Contractor::create([ 'company_name' => 'Site Operations Contractor', 'contact_person' => 'Person S', 'email' => 'site-ops@contractor.com', 'status' => 'active', 'payment_terms' => 'net_30', ]); $otherContractor = Contractor::create([ 'company_name' => 'Other Contractor', 'contact_person' => 'Person O', 'email' => 'other@contractor.com', 'status' => 'active', 'payment_terms' => 'net_30', ]); $contractorProject = Project::create([ 'name' => 'Contractor Site Project', 'code' => 'PRJ-2026-121', 'contractor_id' => $contractor->id, ]); $otherProject = Project::create([ 'name' => 'Other Site Project', 'code' => 'PRJ-2026-122', 'contractor_id' => $otherContractor->id, ]); $siteUser = User::factory()->create([ 'contractor_id' => $contractor->id, 'user_type' => 'employee', 'status' => 'active', ]); $siteUser->assignRole('Site Technical'); $this->actingAs($siteUser); $visibleProjects = Project::all(); $this->assertTrue($visibleProjects->contains($contractorProject)); $this->assertFalse($visibleProjects->contains($otherProject)); } }