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).
181 lines
5.3 KiB
PHP
181 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit;
|
|
|
|
use App\Models\Project;
|
|
use App\Models\ProjectMonthPlan;
|
|
use App\Services\ReconciliationCalculator;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class ReconciliationCalculatorTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private ReconciliationCalculator $calculator;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->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']);
|
|
}
|
|
}
|