calculator = new VarianceCalculator; } // 2.1: Unit test - row variance uses selected month planned value public function test_row_variance_uses_planned_month_value(): void { $project = Project::factory()->create(); // Create month plan for Jan 2026 ProjectMonthPlan::create([ 'project_id' => $project->id, 'month' => '2026-01-01', 'planned_hours' => 100, ]); // Create allocation totaling 120 hours using factory for proper date handling $allocation = Allocation::factory()->create([ 'project_id' => $project->id, 'team_member_id' => null, // untracked 'month' => '2026-01-01', 'allocated_hours' => 120, ]); $result = $this->calculator->calculateRowVariance($project->id, '2026-01'); $this->assertEquals(120, $result['allocated_total']); $this->assertEquals(100, $result['planned_month']); $this->assertEquals(20, $result['variance']); $this->assertEquals('OVER', $result['status']); } // 2.2: Unit test - blank month plan treated as zero for row variance public function test_blank_month_plan_treated_as_zero(): void { $project = Project::factory()->create(); // No month plan created (blank) // Create allocation of 50 hours Allocation::factory()->create([ 'project_id' => $project->id, 'team_member_id' => null, 'month' => '2026-01-01', 'allocated_hours' => 50, ]); $result = $this->calculator->calculateRowVariance($project->id, '2026-01'); // Blank plan = 0, so variance = 50 - 0 = 50 $this->assertEquals(50, $result['allocated_total']); $this->assertEquals(0, $result['planned_month']); $this->assertEquals(50, $result['variance']); $this->assertEquals('OVER', $result['status']); } // 2.3: Unit test - column variance uses member month capacity public function test_column_variance_uses_member_capacity(): void { $role = Role::factory()->create(); $teamMember = TeamMember::factory()->create(['role_id' => $role->id]); // Create allocation for member Allocation::factory()->create([ 'project_id' => Project::factory()->create()->id, 'team_member_id' => $teamMember->id, 'month' => '2026-01-01', 'allocated_hours' => 120, ]); // Mock capacity service - we'll test basic logic here $mockCapacityService = $this->createMock(\App\Services\CapacityService::class); $mockCapacityService->method('calculateIndividualCapacity') ->willReturn(['hours' => 160]); $result = $this->calculator->calculateColumnVariance($teamMember->id, '2026-01', $mockCapacityService); $this->assertEquals(120, $result['allocated']); $this->assertEquals(160, $result['capacity']); $this->assertEquals(-40, $result['variance']); $this->assertEquals('UNDER', $result['status']); } // Additional test: MATCH status when variance is zero public function test_determine_status_returns_match_when_variance_is_zero(): void { $status = $this->calculator->determineStatus(0); $this->assertEquals('MATCH', $status); } // Additional test: get planned hours returns zero for non-existent plan public function test_get_planned_hours_returns_zero_for_no_plan(): void { $project = Project::factory()->create(); $plannedHours = $this->calculator->getPlannedHoursForMonth($project->id, '2026-01'); $this->assertEquals(0, $plannedHours); } // Additional test: untracked allocation is included in row variance public function test_untracked_allocation_included_in_row_variance(): void { $project = Project::factory()->create(); // Create month plan ProjectMonthPlan::create([ 'project_id' => $project->id, 'month' => '2026-01-01', 'planned_hours' => 100, ]); // Create tracked allocation $role = Role::factory()->create(); $teamMember = TeamMember::factory()->create(['role_id' => $role->id]); Allocation::factory()->create([ 'project_id' => $project->id, 'team_member_id' => $teamMember->id, 'month' => '2026-01-01', 'allocated_hours' => 60, ]); // Create untracked allocation (team_member_id = null) Allocation::factory()->create([ 'project_id' => $project->id, 'team_member_id' => null, 'month' => '2026-01-01', 'allocated_hours' => 60, ]); $result = $this->calculator->calculateRowVariance($project->id, '2026-01'); // Total should include both tracked and untracked $this->assertEquals(120, $result['allocated_total']); $this->assertEquals(20, $result['variance']); $this->assertEquals('OVER', $result['status']); } }