155 lines
6.2 KiB
PHP
155 lines
6.2 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Modules\ContractorManagement\Models\Contractor;
|
|
use Modules\FinancialManagement\Models\CashAdvance;
|
|
use Modules\FinancialManagement\Models\RetentionEntry;
|
|
use Modules\ProjectManagement\Models\Project;
|
|
use Spatie\Permission\Models\Role;
|
|
use Tests\TestCase;
|
|
|
|
class FinancialLifecycleE2ETest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected User $superAdmin;
|
|
protected User $projectManager;
|
|
protected User $contractorAdmin;
|
|
protected User $supervisor;
|
|
protected User $siteTech;
|
|
protected Project $project;
|
|
protected Contractor $contractor;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
Role::firstOrCreate(['name' => 'Super Admin']);
|
|
Role::firstOrCreate(['name' => 'admin']);
|
|
Role::firstOrCreate(['name' => 'Project Manager']);
|
|
Role::firstOrCreate(['name' => 'Main Contractor Admin']);
|
|
Role::firstOrCreate(['name' => 'Construction Supervisor']);
|
|
Role::firstOrCreate(['name' => 'Site Technical']);
|
|
|
|
$permFinance = \Spatie\Permission\Models\Permission::firstOrCreate(['name' => 'finance.access']);
|
|
$permProjects = \Spatie\Permission\Models\Permission::firstOrCreate(['name' => 'projects.access']);
|
|
|
|
$this->contractor = Contractor::create([
|
|
'company_name' => 'Apex Buildworks',
|
|
'contact_person' => 'Sam Apex',
|
|
'email' => 'sam@apex.test',
|
|
'status' => 'active',
|
|
]);
|
|
|
|
$this->superAdmin = User::factory()->create(['user_type' => 'admin']);
|
|
$this->superAdmin->assignRole('Super Admin');
|
|
$this->superAdmin->givePermissionTo([$permFinance, $permProjects]);
|
|
|
|
$this->projectManager = User::factory()->create(['user_type' => 'employee']);
|
|
$this->projectManager->assignRole('Project Manager');
|
|
$this->projectManager->givePermissionTo([$permFinance, $permProjects]);
|
|
|
|
$this->contractorAdmin = User::factory()->create([
|
|
'user_type' => 'contractor',
|
|
'contractor_id' => $this->contractor->id,
|
|
]);
|
|
$this->contractorAdmin->assignRole('Main Contractor Admin');
|
|
$this->contractorAdmin->givePermissionTo([$permFinance, $permProjects]);
|
|
|
|
$this->supervisor = User::factory()->create([
|
|
'user_type' => 'employee',
|
|
'contractor_id' => $this->contractor->id,
|
|
]);
|
|
$this->supervisor->assignRole('Construction Supervisor');
|
|
$this->supervisor->givePermissionTo([$permFinance, $permProjects]);
|
|
|
|
$this->siteTech = User::factory()->create([
|
|
'user_type' => 'employee',
|
|
'contractor_id' => $this->contractor->id,
|
|
]);
|
|
$this->siteTech->assignRole('Site Technical');
|
|
$this->siteTech->givePermissionTo([$permFinance, $permProjects]);
|
|
|
|
$this->project = Project::create([
|
|
'name' => 'Skyline Tower Alpha',
|
|
'code' => 'PRJ-SKY-01',
|
|
'location' => 'Metropolis Central',
|
|
'status' => 'in_progress',
|
|
]);
|
|
|
|
$this->project->personnel()->attach($this->supervisor->id, ['role' => 'supervisor']);
|
|
$this->project->personnel()->attach($this->siteTech->id, ['role' => 'site_tech']);
|
|
$this->project->contractors()->attach($this->contractor->id, ['role' => 'general_contractor']);
|
|
}
|
|
|
|
public function test_complete_cash_advance_request_and_approval_flow(): void
|
|
{
|
|
// 1. Site Supervisor initiates Cash Advance
|
|
$response = $this->actingAs($this->supervisor)->post(route('cash-advances.store'), [
|
|
'project_ulid' => $this->project->ulid,
|
|
'amount' => 3500.00,
|
|
'reason' => 'Emergency fasteners and generator diesel top-up',
|
|
]);
|
|
|
|
$response->assertRedirect();
|
|
$this->assertDatabaseHas('cash_advances', [
|
|
'project_id' => $this->project->id,
|
|
'requested_by' => $this->supervisor->id,
|
|
'amount' => 3500.00,
|
|
'status' => 'pending',
|
|
]);
|
|
|
|
$advance = CashAdvance::where('project_id', $this->project->id)->first();
|
|
$this->assertNotNull($advance);
|
|
|
|
// 2. Site Technical attempts approval -> Must be blocked (redirect with error)
|
|
$unauthResp = $this->actingAs($this->siteTech)->patch(route('cash-advances.approve', $advance->ulid));
|
|
$unauthResp->assertSessionHas('error');
|
|
$this->assertEquals('pending', $advance->fresh()->status);
|
|
|
|
// 3. Project Manager approves Cash Advance
|
|
$approveResp = $this->actingAs($this->projectManager)->patch(route('cash-advances.approve', $advance->ulid));
|
|
$approveResp->assertRedirect();
|
|
|
|
$advance->refresh();
|
|
$this->assertEquals('approved', $advance->status);
|
|
$this->assertEquals($this->projectManager->id, $advance->approved_by);
|
|
}
|
|
|
|
public function test_complete_retention_release_and_contractor_confirmation_flow(): void
|
|
{
|
|
// 1. Initial debit entry for retention held
|
|
RetentionEntry::create([
|
|
'project_id' => $this->project->id,
|
|
'type' => 'debit',
|
|
'amount' => 50000.00,
|
|
'description' => 'Milestone 1 Retention Held',
|
|
'status' => 'posted',
|
|
]);
|
|
|
|
// 2. Executive creates credit entry for released retention payment sent
|
|
$retentionCredit = RetentionEntry::create([
|
|
'project_id' => $this->project->id,
|
|
'type' => 'credit',
|
|
'amount' => 50000.00,
|
|
'description' => 'Final Inspection Completed - Retention Settled',
|
|
'status' => 'payment_sent',
|
|
]);
|
|
|
|
// 3. Executive attempting to confirm contractor receipt -> Must be forbidden (403)
|
|
$selfConfirmResp = $this->actingAs($this->superAdmin)->patch(route('retention.confirm', $retentionCredit->ulid));
|
|
$selfConfirmResp->assertForbidden();
|
|
$this->assertEquals('payment_sent', $retentionCredit->fresh()->status);
|
|
|
|
// 4. Contractor Admin confirms receipt of released retention funds
|
|
$confirmResp = $this->actingAs($this->contractorAdmin)->patch(route('retention.confirm', $retentionCredit->ulid));
|
|
$confirmResp->assertRedirect();
|
|
|
|
$retentionCredit->refresh();
|
|
$this->assertEquals('paid', $retentionCredit->status);
|
|
}
|
|
}
|