create(); $member = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]); // Create allocations for Jan, Feb, Mar Allocation::factory()->create([ 'team_member_id' => $member->id, 'month' => '2026-01-01', 'allocated_hours' => 140, ]); Allocation::factory()->create([ 'team_member_id' => $member->id, 'month' => '2026-02-01', 'allocated_hours' => 150, ]); Allocation::factory()->create([ 'team_member_id' => $member->id, 'month' => '2026-03-01', 'allocated_hours' => 160, ]); $service = app(UtilizationService::class); $result = $service->calculateRunningUtilization($member->id, '2026-03'); // Expected: YTD allocated = 140 + 150 + 160 = 450 // Expected: YTD capacity = varies by working days per month expect($result['allocated_ytd'])->toBe(450.0) ->and($result['months_included'])->toBe(3) ->and($result['utilization'])->toBeGreaterThan(80.0); }); test('7.1.12b Running utilization at start of year (January only)', function () { $role = Role::factory()->create(); $member = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]); Allocation::factory()->create([ 'team_member_id' => $member->id, 'month' => '2026-01-01', 'allocated_hours' => 120, ]); $service = app(UtilizationService::class); $result = $service->calculateRunningUtilization($member->id, '2026-01'); // Only January (Jan 2026 has 21 working days = 176 hours capacity) // 120 / 176 = 68.2% expect($result['months_included'])->toBe(1) ->and($result['allocated_ytd'])->toBe(120.0) ->and($result['utilization'])->toBeGreaterThan(60.0); }); // ============================================================================ // 7.1.13 UtilizationService calculates overall utilization (monthly) // ============================================================================ test('7.1.13a UtilizationService calculates overall utilization monthly', function () { $role = Role::factory()->create(); $member = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]); Allocation::factory()->create([ 'team_member_id' => $member->id, 'month' => '2026-02-01', 'allocated_hours' => 140, ]); $service = app(UtilizationService::class); $result = $service->calculateOverallUtilization($member->id, '2026-02'); // Feb 2026: 20 working days = 160 hours capacity // 140 / 160 * 100 = 87.5% expect($result['capacity'])->toBe(160.0) ->and($result['allocated'])->toBe(140.0) ->and($result['utilization'])->toBe(87.5); }); test('7.1.13b Full utilization 100%', function () { $role = Role::factory()->create(); $member = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]); Allocation::factory()->create([ 'team_member_id' => $member->id, 'month' => '2026-02-01', 'allocated_hours' => 160, ]); $service = app(UtilizationService::class); $result = $service->calculateOverallUtilization($member->id, '2026-02'); expect($result['utilization'])->toBe(100.0); }); test('7.1.13c Over-utilization >100%', function () { $role = Role::factory()->create(); $member = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]); Allocation::factory()->create([ 'team_member_id' => $member->id, 'month' => '2026-02-01', 'allocated_hours' => 180, ]); $service = app(UtilizationService::class); $result = $service->calculateOverallUtilization($member->id, '2026-02'); // 180 / 160 * 100 = 112.5% expect($result['utilization'])->toBe(112.5); }); // ============================================================================ // 7.1.14 UtilizationService handles edge cases // ============================================================================ test('7.1.14a Zero capacity returns zero utilization', function () { $role = Role::factory()->create(); $member = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]); // Create a holiday for every working day in February to make capacity zero foreach (range(1, 28) as $day) { $date = sprintf('2026-02-%02d', $day); $carbon = \Carbon\Carbon::parse($date); if (! $carbon->isWeekend()) { Holiday::create(['date' => $date, 'name' => "Holiday $day", 'description' => 'Test']); } } Allocation::factory()->create([ 'team_member_id' => $member->id, 'month' => '2026-02-01', 'allocated_hours' => 100, ]); $service = app(UtilizationService::class); $result = $service->calculateOverallUtilization($member->id, '2026-02'); expect($result['capacity'])->toBe(0.0) ->and($result['utilization'])->toBe(0.0); }); test('7.1.14b No allocations returns zero utilization', function () { $role = Role::factory()->create(); $member = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]); $service = app(UtilizationService::class); $result = $service->calculateOverallUtilization($member->id, '2026-02'); expect($result['allocated'])->toBe(0.0) ->and($result['utilization'])->toBe(0.0); }); test('7.1.14c Team utilization excludes inactive members', function () { $role = Role::factory()->create(); $activeMember = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]); $inactiveMember = TeamMember::factory()->create(['role_id' => $role->id, 'active' => false]); Allocation::factory()->create([ 'team_member_id' => $activeMember->id, 'month' => '2026-02-01', 'allocated_hours' => 140, ]); Allocation::factory()->create([ 'team_member_id' => $inactiveMember->id, 'month' => '2026-02-01', 'allocated_hours' => 160, ]); $service = app(UtilizationService::class); $result = $service->calculateTeamUtilization('2026-02'); // Only active member counted expect($result['member_count'])->toBe(1); }); // ============================================================================ // 7.1.15 Color coding logic // ============================================================================ test('7.1.15a Low utilization (< 70%) is gray', function () { $service = app(UtilizationService::class); expect($service->getUtilizationIndicator(0))->toBe('gray') ->and($service->getUtilizationIndicator(50))->toBe('gray') ->and($service->getUtilizationIndicator(69.9))->toBe('gray'); }); test('7.1.15b Low utilization (70-80%) is blue', function () { $service = app(UtilizationService::class); expect($service->getUtilizationIndicator(70))->toBe('blue') ->and($service->getUtilizationIndicator(75))->toBe('blue') ->and($service->getUtilizationIndicator(79.9))->toBe('blue'); }); test('7.1.15c Optimal utilization (80-100%) is green', function () { $service = app(UtilizationService::class); expect($service->getUtilizationIndicator(80))->toBe('green') ->and($service->getUtilizationIndicator(90))->toBe('green') ->and($service->getUtilizationIndicator(100))->toBe('green'); }); test('7.1.15d High utilization (100-110%) is yellow', function () { $service = app(UtilizationService::class); expect($service->getUtilizationIndicator(100.1))->toBe('yellow') ->and($service->getUtilizationIndicator(105))->toBe('yellow') ->and($service->getUtilizationIndicator(110))->toBe('yellow'); }); test('7.1.15e Over-utilization (> 110%) is red', function () { $service = app(UtilizationService::class); expect($service->getUtilizationIndicator(110.1))->toBe('red') ->and($service->getUtilizationIndicator(120))->toBe('red') ->and($service->getUtilizationIndicator(200))->toBe('red'); }); // ============================================================================ // Additional coverage tests // ============================================================================ test('7.1.16 getUtilizationData combines overall and running', function () { $role = Role::factory()->create(); $member = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]); Allocation::factory()->create([ 'team_member_id' => $member->id, 'month' => '2026-02-01', 'allocated_hours' => 140, ]); $service = app(UtilizationService::class); $result = $service->getUtilizationData($member->id, '2026-02'); expect($result)->toHaveKeys(['overall', 'running']) ->and($result['overall'])->toHaveKeys(['capacity', 'allocated', 'utilization', 'indicator']) ->and($result['running'])->toHaveKeys(['capacity_ytd', 'allocated_ytd', 'utilization', 'indicator', 'months_included']); }); test('7.1.17 getUtilizationTrend returns monthly data', function () { $role = Role::factory()->create(); $member = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]); Allocation::factory()->create([ 'team_member_id' => $member->id, 'month' => '2026-01-01', 'allocated_hours' => 140, ]); Allocation::factory()->create([ 'team_member_id' => $member->id, 'month' => '2026-02-01', 'allocated_hours' => 150, ]); Allocation::factory()->create([ 'team_member_id' => $member->id, 'month' => '2026-03-01', 'allocated_hours' => 160, ]); $service = app(UtilizationService::class); $result = $service->getUtilizationTrend($member->id, '2026-01', '2026-03'); expect($result)->toHaveCount(3) ->and($result[0])->toHaveKeys(['month', 'utilization', 'indicator', 'capacity', 'allocated']) ->and($result[0]['month'])->toBe('2026-01') ->and($result[2]['month'])->toBe('2026-03'); }); test('7.1.18 calculateTeamRunningUtilization calculates YTD team average', function () { $role = Role::factory()->create(); $memberA = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]); $memberB = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]); // Jan 2026 has 21 working days = 176 hours capacity // Member A: 140/176 = 79.5% utilization Allocation::factory()->create([ 'team_member_id' => $memberA->id, 'month' => '2026-01-01', 'allocated_hours' => 140, ]); // Member B: 150/176 = 85.2% utilization Allocation::factory()->create([ 'team_member_id' => $memberB->id, 'month' => '2026-01-01', 'allocated_hours' => 150, ]); $service = app(UtilizationService::class); $result = $service->calculateTeamRunningUtilization('2026-01'); // Average: (79.5 + 85.2) / 2 = 82.35 expect($result['member_count'])->toBe(2) ->and($result['average_utilization'])->toBe(82.4); });