'Super Admin']); Role::firstOrCreate(['name' => 'Admin']); Role::firstOrCreate(['name' => 'Project Manager']); Role::firstOrCreate(['name' => 'Main Contractor Admin']); $financePermission = Permission::firstOrCreate(['name' => 'finance.access']); $this->superAdmin = User::factory()->create(['user_type' => 'admin']); $this->superAdmin->assignRole('Super Admin'); $this->superAdmin->givePermissionTo($financePermission); $this->admin = User::factory()->create(['user_type' => 'admin']); $this->admin->assignRole('Admin'); $this->admin->givePermissionTo($financePermission); $this->projectManager = User::factory()->create(['user_type' => 'employee']); $this->projectManager->assignRole('Project Manager'); $this->projectManager->givePermissionTo($financePermission); $this->contractorAdmin = User::factory()->create(['user_type' => 'contractor']); $this->contractorAdmin->assignRole('Main Contractor Admin'); $this->contractorAdmin->givePermissionTo($financePermission); $this->project = Project::create([ 'name' => 'Metro Office Tower', 'code' => 'PRJ-METRO-01', 'status' => 'in_progress', 'budget' => 5000000, 'contract_value' => 5000000, 'total_capitalization' => 5000000, 'completion_percentage' => 0, 'current_wizard_step' => 8, ]); } public function test_contractor_admin_can_generate_progress_invoice(): void { $response = $this->actingAs($this->contractorAdmin)->post(route('finance.store'), [ 'project_id' => $this->project->ulid, 'current_percentage' => 20.00, 'retention_rate' => 10.00, ]); $response->assertRedirect(route('finance.index')); $this->assertDatabaseHas('financial_invoices', [ 'project_id' => $this->project->id, 'billed_percentage' => 20.00, 'retention_rate' => 10.00, 'status' => 'draft', ]); } public function test_superadmin_admin_and_pm_can_approve_invoice_and_hold_retention(): void { $invoice = FinancialInvoice::create([ 'project_id' => $this->project->id, 'invoice_number' => 'INV-TEST-001', 'status' => InvoiceStatus::Submitted, 'subtotal' => 1000000.00, 'retention_amount' => 100000.00, 'total_amount' => 900000.00, 'retention_rate' => 10.00, 'billed_percentage' => 20.00, 'invoice_date' => now()->toDateString(), ]); // Project Manager approves invoice $response = $this->actingAs($this->projectManager) ->patch(route('finance.approve', $invoice->ulid)); $response->assertSessionHas('success'); $invoice->refresh(); $this->assertEquals(InvoiceStatus::Approved, $invoice->status); // Retention debit entry created in ledger $this->assertDatabaseHas('retention_ledger', [ 'project_id' => $this->project->id, 'invoice_id' => $invoice->id, 'type' => 'debit', 'amount' => 100000.00, ]); } public function test_contractor_admin_can_send_10_percent_payment_proof(): void { Storage::fake('public'); $invoice = FinancialInvoice::create([ 'project_id' => $this->project->id, 'invoice_number' => 'INV-TEST-002', 'status' => InvoiceStatus::Approved, 'subtotal' => 1000000.00, 'retention_amount' => 100000.00, 'total_amount' => 900000.00, 'retention_rate' => 10.00, 'billed_percentage' => 20.00, 'invoice_date' => now()->toDateString(), ]); $file = UploadedFile::fake()->create('payment_receipt_10percent.pdf', 200, 'application/pdf'); $response = $this->actingAs($this->contractorAdmin)->post(route('finance.send-payment-proof', $invoice->ulid), [ 'media' => $file, 'notes' => '10% Retention Remittance via BDO Bank Transfer Ref #992144', ]); $response->assertSessionHas('success'); $invoice->refresh(); $this->assertEquals(InvoiceStatus::PaymentSent, $invoice->status); $this->assertNotNull($invoice->payment_proof_path); $this->assertEquals('payment_receipt_10percent.pdf', $invoice->payment_proof_name); $this->assertEquals('10% Retention Remittance via BDO Bank Transfer Ref #992144', $invoice->payment_proof_notes); $this->assertEquals($this->contractorAdmin->id, $invoice->payment_proof_submitted_by); $this->assertNotNull($invoice->payment_proof_submitted_at); Storage::disk('public')->assertExists($invoice->payment_proof_path); } public function test_executive_can_view_proof_and_confirm_payment_received(): void { Storage::fake('public'); $file = UploadedFile::fake()->create('proof.pdf', 100, 'application/pdf'); $path = $file->store('invoice_payment_proofs/1', 'public'); $invoice = FinancialInvoice::create([ 'project_id' => $this->project->id, 'invoice_number' => 'INV-TEST-003', 'status' => InvoiceStatus::PaymentSent, 'subtotal' => 1000000.00, 'retention_amount' => 100000.00, 'total_amount' => 900000.00, 'retention_rate' => 10.00, 'billed_percentage' => 20.00, 'invoice_date' => now()->toDateString(), 'payment_proof_path' => $path, 'payment_proof_name' => 'proof.pdf', 'payment_proof_submitted_at' => now(), 'payment_proof_submitted_by' => $this->contractorAdmin->id, ]); // Executive can view payment proof $viewResponse = $this->actingAs($this->superAdmin) ->get(route('finance.payment-proof.view', $invoice->ulid)); $viewResponse->assertOk(); // Project Manager confirms received payment $confirmResponse = $this->actingAs($this->projectManager) ->patch(route('finance.receive-payment', $invoice->ulid), [ 'notes' => 'Verified with Treasury. Payment confirmed.', ]); $confirmResponse->assertSessionHas('success'); $invoice->refresh(); $this->assertEquals(InvoiceStatus::Paid, $invoice->status); $this->assertEquals(900000.00, (float) $invoice->paid_amount); $this->assertNotNull($invoice->paid_at); $this->assertEquals(20.00, (float) $this->project->refresh()->completion_percentage); } public function test_contractor_cannot_confirm_received_payment_on_behalf_of_executive(): void { $invoice = FinancialInvoice::create([ 'project_id' => $this->project->id, 'invoice_number' => 'INV-TEST-004', 'status' => InvoiceStatus::PaymentSent, 'subtotal' => 1000000.00, 'retention_amount' => 100000.00, 'total_amount' => 900000.00, 'retention_rate' => 10.00, 'billed_percentage' => 20.00, 'invoice_date' => now()->toDateString(), ]); $response = $this->actingAs($this->contractorAdmin) ->patch(route('finance.receive-payment', $invoice->ulid)); $response->assertSessionHas('error'); $invoice->refresh(); $this->assertNotEquals(InvoiceStatus::Paid, $invoice->status); } public function test_overdue_invoices_past_2_months_appear_in_retention_index(): void { // Invoice created 75 days ago (older than 2 months) $overdueInvoice = FinancialInvoice::create([ 'project_id' => $this->project->id, 'invoice_number' => 'INV-OVERDUE-01', 'status' => InvoiceStatus::Approved, 'subtotal' => 2000000.00, 'retention_amount' => 200000.00, 'total_amount' => 1800000.00, 'retention_rate' => 10.00, 'billed_percentage' => 40.00, 'invoice_date' => now()->subDays(75)->toDateString(), 'approved_at' => now()->subDays(75), ]); $response = $this->actingAs($this->superAdmin) ->get(route('retention.index')); $response->assertOk(); $response->assertInertia(fn ($page) => $page ->component('FinancialManagement::Retention/Index', false) ->has('overdueInvoices', 1) ->where('overdueInvoices.0.invoice_number', 'INV-OVERDUE-01') ->where('overdueInvoices.0.retention_amount', 200000) ); } public function test_executive_can_apply_percentage_penalty_to_overdue_invoice(): void { $overdueInvoice = FinancialInvoice::create([ 'project_id' => $this->project->id, 'invoice_number' => 'INV-OVERDUE-02', 'status' => InvoiceStatus::Approved, 'subtotal' => 1000000.00, 'retention_amount' => 100000.00, 'total_amount' => 900000.00, 'retention_rate' => 10.00, 'billed_percentage' => 20.00, 'invoice_date' => now()->subDays(65)->toDateString(), ]); // Super Admin applies a 5% penalty $response = $this->actingAs($this->superAdmin) ->post(route('finance.apply-penalty', $overdueInvoice->ulid), [ 'penalty_rate' => 5.00, 'reason' => 'Overdue 65 days without remittance confirmation.', ]); $response->assertSessionHas('success'); $overdueInvoice->refresh(); // 5% of 100,000 retention = 5,000.00 penalty $this->assertEquals(5.00, (float) $overdueInvoice->penalty_rate); $this->assertEquals(5000.00, (float) $overdueInvoice->penalty_amount); $this->assertEquals('Overdue 65 days without remittance confirmation.', $overdueInvoice->penalty_reason); $this->assertEquals($this->superAdmin->id, $overdueInvoice->penalty_applied_by); $this->assertNotNull($overdueInvoice->penalty_applied_at); // Penalty adjustment recorded in retention ledger $this->assertDatabaseHas('retention_ledger', [ 'project_id' => $this->project->id, 'invoice_id' => $overdueInvoice->id, 'type' => 'debit', 'amount' => 5000.00, ]); } public function test_contractor_cannot_apply_penalty(): void { $overdueInvoice = FinancialInvoice::create([ 'project_id' => $this->project->id, 'invoice_number' => 'INV-OVERDUE-03', 'status' => InvoiceStatus::Approved, 'subtotal' => 1000000.00, 'retention_amount' => 100000.00, 'total_amount' => 900000.00, 'retention_rate' => 10.00, 'billed_percentage' => 20.00, 'invoice_date' => now()->subDays(70)->toDateString(), ]); $response = $this->actingAs($this->contractorAdmin) ->post(route('finance.apply-penalty', $overdueInvoice->ulid), [ 'penalty_rate' => 5.00, 'reason' => 'Attempted unauthorized penalty.', ]); $response->assertForbidden(); $this->assertNull($overdueInvoice->refresh()->penalty_rate); } public function test_main_contractor_admin_can_view_retention_index(): void { $response = $this->actingAs($this->contractorAdmin)->get(route('retention.index')); $response->assertOk(); } public function test_site_operations_roles_are_forbidden_from_retention(): void { Role::firstOrCreate(['name' => 'Site Technical']); Role::firstOrCreate(['name' => 'Construction Supervisor']); $siteTech = User::factory()->create(['user_type' => 'employee']); $siteTech->assignRole('Site Technical'); $siteTech->givePermissionTo('finance.access'); $supervisor = User::factory()->create(['user_type' => 'employee']); $supervisor->assignRole('Construction Supervisor'); $supervisor->givePermissionTo('finance.access'); // Site Technical gets 403 $this->actingAs($siteTech)->get(route('retention.index'))->assertForbidden(); // Construction Supervisor gets 403 $this->actingAs($supervisor)->get(route('retention.index'))->assertForbidden(); } public function test_site_operations_cannot_submit_payment_proof(): void { Role::firstOrCreate(['name' => 'Site Technical']); $siteTech = User::factory()->create(['user_type' => 'employee']); $siteTech->assignRole('Site Technical'); $siteTech->givePermissionTo('finance.access'); $invoice = FinancialInvoice::create([ 'project_id' => $this->project->id, 'invoice_number' => 'INV-PROOF-TEST-01', 'status' => InvoiceStatus::Approved, 'subtotal' => 1000000.00, 'retention_amount' => 100000.00, 'total_amount' => 900000.00, 'retention_rate' => 10.00, 'billed_percentage' => 20.00, 'invoice_date' => now()->toDateString(), ]); Storage::fake('public'); $file = UploadedFile::fake()->create('proof.pdf', 500, 'application/pdf'); $response = $this->actingAs($siteTech) ->post(route('finance.send-payment-proof', $invoice->ulid), [ 'media' => $file, 'notes' => 'Attempted unauthorized upload', ]); $response->assertForbidden(); } }