test(backend): add comprehensive tests for reporting and allocation

Add test coverage for:
- ReportTest: 9 tests for reporting API (payload, view types, filters, validation)
- ProjectMonthPlanTest: CRUD operations for monthly planning
- UntrackedAllocationTest: untracked allocation handling
- ReconciliationCalculatorTest: plan vs estimate reconciliation logic
- VarianceCalculatorTest: variance and status calculations

All tests passing (157 total).
This commit is contained in:
2026-03-08 18:22:40 -04:00
parent 2a93245970
commit 7fa5b9061c
5 changed files with 954 additions and 0 deletions

View File

@@ -0,0 +1,160 @@
<?php
namespace Tests\Unit;
use App\Models\Allocation;
use App\Models\Project;
use App\Models\ProjectMonthPlan;
use App\Models\Role;
use App\Models\TeamMember;
use App\Services\VarianceCalculator;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class VarianceCalculatorTest extends TestCase
{
use RefreshDatabase;
private VarianceCalculator $calculator;
protected function setUp(): void
{
parent::setUp();
$this->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']);
}
}