'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); } }