Implement revised retention workflow, overdue late penalty calculation, payment proof verification, and dynamic ledger reconciliation
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\ProjectProgress\Tests\Feature;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Inertia\Testing\AssertableInertia as Assert;
|
||||
use Modules\ProjectManagement\Models\Project;
|
||||
use Modules\ProjectManagement\Models\Task;
|
||||
use Modules\ProjectProgress\Services\ProjectProgressService;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProgressMonitoringFeatureTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected User $user;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$role = Role::firstOrCreate(['name' => 'Super Admin']);
|
||||
Permission::firstOrCreate(['name' => 'projects.access']);
|
||||
|
||||
$this->user = User::factory()->create([
|
||||
'status' => 'active',
|
||||
'user_type' => 'admin',
|
||||
]);
|
||||
$this->user->assignRole($role);
|
||||
}
|
||||
|
||||
public function test_progress_monitoring_page_renders_with_inertia_props_when_project_selected(): void
|
||||
{
|
||||
$project = Project::factory()->create([
|
||||
'start_date' => now()->subDays(20)->format('Y-m-d'),
|
||||
'target_end_date' => now()->addDays(20)->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
Task::factory()->create([
|
||||
'project_id' => $project->id,
|
||||
'status' => 'completed',
|
||||
'completion_percentage' => 100,
|
||||
'start_date' => now()->subDays(20)->format('Y-m-d'),
|
||||
'actual_end_date' => now()->subDays(5)->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->get(route('projects.progress.index', ['project' => $project->ulid]));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('ProjectManagement::Projects/Modules/Progress', false)
|
||||
->has('project')
|
||||
->has('projects')
|
||||
->where('currentTab', 'progress')
|
||||
->has('evmData', fn (Assert $evm) => $evm
|
||||
->has('timeSeries')
|
||||
->has('milestones')
|
||||
->has('summary', fn (Assert $summary) => $summary
|
||||
->has('planned_pv')
|
||||
->has('actual_ev')
|
||||
->has('schedule_variance')
|
||||
->has('spi')
|
||||
->has('status')
|
||||
->has('target_end_date')
|
||||
->has('projected_end_date')
|
||||
->has('days_variance')
|
||||
->has('is_completed')
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_progress_monitoring_page_without_project_param_renders_project_selection_grid(): void
|
||||
{
|
||||
Project::factory()->count(3)->create();
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->get(route('projects.progress.index'));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('ProjectManagement::Projects/Modules/Progress', false)
|
||||
->where('project', null)
|
||||
->where('evmData', null)
|
||||
->has('projects', 3)
|
||||
);
|
||||
}
|
||||
|
||||
public function test_project_overview_page_provides_evm_data_prop(): void
|
||||
{
|
||||
$project = Project::factory()->create([
|
||||
'start_date' => now()->subDays(15)->format('Y-m-d'),
|
||||
'target_end_date' => now()->addDays(15)->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$response = $this->actingAs($this->user)
|
||||
->get(route('projects.show', $project->ulid));
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertInertia(fn (Assert $page) => $page
|
||||
->component('ProjectManagement::Projects/Overview', false)
|
||||
->has('evmData')
|
||||
->has('evmData.timeSeries')
|
||||
->has('evmData.summary')
|
||||
->has('evmData.milestones')
|
||||
);
|
||||
}
|
||||
|
||||
public function test_evm_calculation_handles_ahead_of_schedule_status(): void
|
||||
{
|
||||
$project = Project::factory()->create([
|
||||
'start_date' => now()->subDays(30)->format('Y-m-d'),
|
||||
'target_end_date' => now()->addDays(30)->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Task completed way ahead of schedule
|
||||
Task::factory()->create([
|
||||
'project_id' => $project->id,
|
||||
'status' => 'completed',
|
||||
'completion_percentage' => 100,
|
||||
'start_date' => now()->subDays(30)->format('Y-m-d'),
|
||||
'actual_end_date' => now()->subDays(20)->format('Y-m-d'),
|
||||
'end_date' => now()->addDays(20)->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$service = new ProjectProgressService();
|
||||
$evmData = $service->getEvmAnalysisData($project);
|
||||
|
||||
$summary = $evmData['summary'];
|
||||
$this->assertEquals(100.0, $summary['actual_ev']);
|
||||
$this->assertGreaterThan(0, $summary['schedule_variance']);
|
||||
$this->assertGreaterThanOrEqual(1.0, $summary['spi']);
|
||||
$this->assertEquals('Completed Ahead of Schedule', $summary['status']);
|
||||
$this->assertTrue($summary['is_completed']);
|
||||
}
|
||||
|
||||
public function test_evm_calculation_handles_delayed_status_and_forecasted_end_date(): void
|
||||
{
|
||||
$project = Project::factory()->create([
|
||||
'start_date' => now()->subDays(30)->format('Y-m-d'),
|
||||
'target_end_date' => now()->addDays(10)->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Only 10% completed while planned is ~75%
|
||||
Task::factory()->create([
|
||||
'project_id' => $project->id,
|
||||
'status' => 'in_progress',
|
||||
'completion_percentage' => 10,
|
||||
'start_date' => now()->subDays(30)->format('Y-m-d'),
|
||||
'end_date' => now()->addDays(10)->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$service = new ProjectProgressService();
|
||||
$evmData = $service->getEvmAnalysisData($project);
|
||||
|
||||
$summary = $evmData['summary'];
|
||||
$this->assertLessThan(0, $summary['schedule_variance']);
|
||||
$this->assertLessThan(1.0, $summary['spi']);
|
||||
$this->assertEquals('Delayed', $summary['status']);
|
||||
$this->assertGreaterThan(0, $summary['days_variance']);
|
||||
}
|
||||
|
||||
public function test_evm_calculation_handles_empty_project_gracefully(): void
|
||||
{
|
||||
$project = Project::factory()->create([
|
||||
'start_date' => now()->format('Y-m-d'),
|
||||
'target_end_date' => now()->addDays(30)->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$service = new ProjectProgressService();
|
||||
$evmData = $service->getEvmAnalysisData($project);
|
||||
|
||||
$this->assertIsArray($evmData);
|
||||
$this->assertNotEmpty($evmData['timeSeries']);
|
||||
$this->assertEquals(0.0, $evmData['summary']['actual_ev']);
|
||||
}
|
||||
|
||||
public function test_task_and_scheduling_coordinated_with_milestone_turnover(): void
|
||||
{
|
||||
$project = Project::factory()->create([
|
||||
'start_date' => now()->subDays(30)->format('Y-m-d'),
|
||||
'target_end_date' => now()->addDays(30)->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Milestone 1: Structural Works (Turnovered Ahead)
|
||||
$m1 = \Modules\ProjectManagement\Models\ProjectMilestone::create([
|
||||
'project_id' => $project->id,
|
||||
'name' => 'Foundation & Structural Turnover',
|
||||
'weight_percentage' => 60.0,
|
||||
'planned_date' => now()->subDays(5)->format('Y-m-d'),
|
||||
'actual_date' => now()->subDays(10)->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
Task::factory()->create([
|
||||
'project_id' => $project->id,
|
||||
'milestone_id' => $m1->id,
|
||||
'status' => 'completed',
|
||||
'completion_percentage' => 100,
|
||||
'start_date' => now()->subDays(30)->format('Y-m-d'),
|
||||
'actual_end_date' => now()->subDays(10)->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
// Milestone 2: Finishing Works (In Progress / Ready for Turnover)
|
||||
$m2 = \Modules\ProjectManagement\Models\ProjectMilestone::create([
|
||||
'project_id' => $project->id,
|
||||
'name' => 'Finishing & Final Turnover',
|
||||
'weight_percentage' => 40.0,
|
||||
'planned_date' => now()->addDays(20)->format('Y-m-d'),
|
||||
'actual_date' => null,
|
||||
]);
|
||||
|
||||
Task::factory()->create([
|
||||
'project_id' => $project->id,
|
||||
'milestone_id' => $m2->id,
|
||||
'status' => 'in_progress',
|
||||
'completion_percentage' => 50,
|
||||
'start_date' => now()->subDays(9)->format('Y-m-d'),
|
||||
'end_date' => now()->addDays(20)->format('Y-m-d'),
|
||||
]);
|
||||
|
||||
$service = new ProjectProgressService();
|
||||
$evmData = $service->getEvmAnalysisData($project);
|
||||
|
||||
$this->assertNotEmpty($evmData['milestones']);
|
||||
$this->assertCount(2, $evmData['milestones']);
|
||||
|
||||
$m1Data = collect($evmData['milestones'])->firstWhere('id', $m1->id);
|
||||
$this->assertEquals('Turnovered Ahead', $m1Data['turnover_status']);
|
||||
$this->assertTrue($m1Data['is_turnovered']);
|
||||
$this->assertEquals(100.0, $m1Data['progress_percentage']);
|
||||
|
||||
$m2Data = collect($evmData['milestones'])->firstWhere('id', $m2->id);
|
||||
$this->assertEquals('In Progress', $m2Data['turnover_status']);
|
||||
$this->assertFalse($m2Data['is_turnovered']);
|
||||
$this->assertEquals(50.0, $m2Data['progress_percentage']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user