calculator = new ReconciliationCalculator; } // 1.1: Unit test - reconciliation status OVER public function test_returns_over_when_plan_sum_exceeds_approved_estimate(): void { $project = Project::factory()->create([ 'approved_estimate' => 1000, ]); ProjectMonthPlan::create([ 'project_id' => $project->id, 'month' => '2026-01-01', 'planned_hours' => 600, ]); ProjectMonthPlan::create([ 'project_id' => $project->id, 'month' => '2026-02-01', 'planned_hours' => 600, ]); $status = $this->calculator->calculateStatus($project); $this->assertEquals('OVER', $status); } // 1.2: Unit test - reconciliation status UNDER public function test_returns_under_when_plan_sum_is_less_than_approved_estimate(): void { $project = Project::factory()->create([ 'approved_estimate' => 1000, ]); ProjectMonthPlan::create([ 'project_id' => $project->id, 'month' => '2026-01-01', 'planned_hours' => 400, ]); $status = $this->calculator->calculateStatus($project); $this->assertEquals('UNDER', $status); } // 1.3: Unit test - reconciliation status MATCH public function test_returns_match_when_plan_sum_equals_approved_estimate(): void { $project = Project::factory()->create([ 'approved_estimate' => 1000, ]); ProjectMonthPlan::create([ 'project_id' => $project->id, 'month' => '2026-01-01', 'planned_hours' => 500, ]); ProjectMonthPlan::create([ 'project_id' => $project->id, 'month' => '2026-02-01', 'planned_hours' => 500, ]); $status = $this->calculator->calculateStatus($project); $this->assertEquals('MATCH', $status); } // Additional test: decimal-safe MATCH public function test_returns_match_with_decimal_precision(): void { $project = Project::factory()->create([ 'approved_estimate' => 100.50, ]); ProjectMonthPlan::create([ 'project_id' => $project->id, 'month' => '2026-01-01', 'planned_hours' => 50.25, ]); ProjectMonthPlan::create([ 'project_id' => $project->id, 'month' => '2026-02-01', 'planned_hours' => 50.25, ]); $status = $this->calculator->calculateStatus($project); $this->assertEquals('MATCH', $status); } // Test: blank/null planned_hours are excluded from sum public function test_excludes_null_planned_hours_from_sum(): void { $project = Project::factory()->create([ 'approved_estimate' => 500, ]); // Create plan with null planned_hours (blank cell) ProjectMonthPlan::create([ 'project_id' => $project->id, 'month' => '2026-01-01', 'planned_hours' => null, ]); ProjectMonthPlan::create([ 'project_id' => $project->id, 'month' => '2026-02-01', 'planned_hours' => 500, ]); $status = $this->calculator->calculateStatus($project); $planSum = $this->calculator->calculatePlanSum($project); $this->assertEquals(500, $planSum); $this->assertEquals('MATCH', $status); } // Test: no approved estimate returns UNDER public function test_returns_under_when_no_approved_estimate(): void { $project = Project::factory()->create([ 'approved_estimate' => 0, ]); $status = $this->calculator->calculateStatus($project); $this->assertEquals('UNDER', $status); } // Test: calculateForProjects returns array with correct structure public function test_calculate_for_projects_returns_correct_structure(): void { $project1 = Project::factory()->create(['approved_estimate' => 1000]); $project2 = Project::factory()->create(['approved_estimate' => 500]); ProjectMonthPlan::create([ 'project_id' => $project1->id, 'month' => '2026-01-01', 'planned_hours' => 1000, ]); ProjectMonthPlan::create([ 'project_id' => $project2->id, 'month' => '2026-01-01', 'planned_hours' => 300, ]); $projects = Project::whereIn('id', [$project1->id, $project2->id])->get(); $results = $this->calculator->calculateForProjects($projects, 2026); $this->assertArrayHasKey($project1->id, $results); $this->assertArrayHasKey($project2->id, $results); $this->assertEquals(1000, $results[$project1->id]['plan_sum']); $this->assertEquals('MATCH', $results[$project1->id]['status']); $this->assertEquals(300, $results[$project2->id]['plan_sum']); $this->assertEquals('UNDER', $results[$project2->id]['status']); } }