'finance_requester']); Role::create(['name' => 'finance_cashier']); Role::create(['name' => 'finance_accountant']); Role::create(['name' => 'finance_chair']); Role::create(['name' => 'finance_board_member']); // Create test users $this->requester = User::factory()->create(['email' => 'requester@test.com']); $this->cashier = User::factory()->create(['email' => 'cashier@test.com']); $this->accountant = User::factory()->create(['email' => 'accountant@test.com']); $this->chair = User::factory()->create(['email' => 'chair@test.com']); $this->boardMember = User::factory()->create(['email' => 'board@test.com']); // Assign roles $this->requester->assignRole('finance_requester'); $this->cashier->assignRole('finance_cashier'); $this->accountant->assignRole('finance_accountant'); $this->chair->assignRole('finance_chair'); $this->boardMember->assignRole('finance_board_member'); // Give permissions $this->requester->givePermissionTo('create_finance_document'); $this->cashier->givePermissionTo(['view_finance_documents', 'approve_as_cashier']); $this->accountant->givePermissionTo(['view_finance_documents', 'approve_as_accountant']); $this->chair->givePermissionTo(['view_finance_documents', 'approve_as_chair']); $this->boardMember->givePermissionTo('approve_board_meeting'); } /** @test */ public function small_amount_workflow_completes_without_chair() { // Create a small amount document (< 5000) $document = FinanceDocument::create([ 'title' => 'Small Expense Reimbursement', 'description' => 'Test small expense', 'amount' => 3000, 'request_type' => 'expense_reimbursement', 'status' => 'pending', 'submitted_by_id' => $this->requester->id, 'submitted_at' => now(), ]); $document->amount_tier = $document->determineAmountTier(); $document->save(); $this->assertEquals('small', $document->amount_tier); // Cashier approves $this->actingAs($this->cashier); $document->status = FinanceDocument::STATUS_APPROVED_CASHIER; $document->cashier_approved_by_id = $this->cashier->id; $document->cashier_approved_at = now(); $document->save(); $this->assertFalse($document->isApprovalStageComplete()); // Accountant approves (should complete workflow for small amounts) $this->actingAs($this->accountant); $document->status = FinanceDocument::STATUS_APPROVED_ACCOUNTANT; $document->accountant_approved_by_id = $this->accountant->id; $document->accountant_approved_at = now(); $document->save(); $this->assertTrue($document->isApprovalStageComplete()); $this->assertEquals('approval', $document->getCurrentWorkflowStage()); // Ready for payment stage } /** @test */ public function medium_amount_workflow_requires_chair_approval() { // Create a medium amount document (5000-50000) $document = FinanceDocument::create([ 'title' => 'Medium Purchase Request', 'description' => 'Test medium purchase', 'amount' => 25000, 'request_type' => 'purchase_request', 'status' => 'pending', 'submitted_by_id' => $this->requester->id, 'submitted_at' => now(), ]); $document->amount_tier = $document->determineAmountTier(); $document->save(); $this->assertEquals('medium', $document->amount_tier); $this->assertFalse($document->needsBoardMeetingApproval()); // Cashier approves $document->status = FinanceDocument::STATUS_APPROVED_CASHIER; $document->cashier_approved_by_id = $this->cashier->id; $document->cashier_approved_at = now(); $document->save(); $this->assertFalse($document->isApprovalStageComplete()); // Accountant approves $document->status = FinanceDocument::STATUS_APPROVED_ACCOUNTANT; $document->accountant_approved_by_id = $this->accountant->id; $document->accountant_approved_at = now(); $document->save(); $this->assertFalse($document->isApprovalStageComplete()); // Still needs chair // Chair approves (should complete workflow for medium amounts) $document->status = FinanceDocument::STATUS_APPROVED_CHAIR; $document->chair_approved_by_id = $this->chair->id; $document->chair_approved_at = now(); $document->save(); $this->assertTrue($document->isApprovalStageComplete()); } /** @test */ public function large_amount_workflow_requires_board_meeting_approval() { // Create a large amount document (> 50000) $document = FinanceDocument::create([ 'title' => 'Large Capital Expenditure', 'description' => 'Test large expenditure', 'amount' => 75000, 'request_type' => 'purchase_request', 'status' => 'pending', 'submitted_by_id' => $this->requester->id, 'submitted_at' => now(), ]); $document->amount_tier = $document->determineAmountTier(); $document->requires_board_meeting = $document->needsBoardMeetingApproval(); $document->save(); $this->assertEquals('large', $document->amount_tier); $this->assertTrue($document->requires_board_meeting); // Cashier approves $document->status = FinanceDocument::STATUS_APPROVED_CASHIER; $document->cashier_approved_by_id = $this->cashier->id; $document->cashier_approved_at = now(); $document->save(); // Accountant approves $document->status = FinanceDocument::STATUS_APPROVED_ACCOUNTANT; $document->accountant_approved_by_id = $this->accountant->id; $document->accountant_approved_at = now(); $document->save(); // Chair approves $document->status = FinanceDocument::STATUS_APPROVED_CHAIR; $document->chair_approved_by_id = $this->chair->id; $document->chair_approved_at = now(); $document->save(); $this->assertFalse($document->isApprovalStageComplete()); // Still needs board meeting // Board meeting approval $document->board_meeting_approved_at = now(); $document->board_meeting_approved_by_id = $this->boardMember->id; $document->save(); $this->assertTrue($document->isApprovalStageComplete()); } /** @test */ public function requester_can_create_finance_document() { Storage::fake('local'); $this->actingAs($this->requester); $file = UploadedFile::fake()->create('receipt.pdf', 100); $response = $this->post(route('admin.finance.store'), [ 'title' => 'Test Expense', 'description' => 'Test description', 'amount' => 5000, 'request_type' => 'expense_reimbursement', 'attachment' => $file, ]); $response->assertRedirect(); $this->assertDatabaseHas('finance_documents', [ 'title' => 'Test Expense', 'amount' => 5000, 'submitted_by_id' => $this->requester->id, ]); } /** @test */ public function cashier_cannot_approve_own_submission() { $document = FinanceDocument::create([ 'title' => 'Self Submitted', 'description' => 'Test', 'amount' => 1000, 'request_type' => 'petty_cash', 'status' => 'pending', 'submitted_by_id' => $this->cashier->id, 'submitted_at' => now(), ]); $this->assertFalse($document->canBeApprovedByCashier($this->cashier)); } /** @test */ public function accountant_cannot_approve_before_cashier() { $document = FinanceDocument::create([ 'title' => 'Pending Document', 'description' => 'Test', 'amount' => 1000, 'request_type' => 'petty_cash', 'status' => 'pending', 'submitted_by_id' => $this->requester->id, 'submitted_at' => now(), ]); $this->assertFalse($document->canBeApprovedByAccountant()); } /** @test */ public function rejected_document_cannot_proceed() { $document = FinanceDocument::create([ 'title' => 'Rejected Document', 'description' => 'Test', 'amount' => 1000, 'request_type' => 'petty_cash', 'status' => FinanceDocument::STATUS_REJECTED, 'submitted_by_id' => $this->requester->id, 'submitted_at' => now(), ]); $this->assertFalse($document->canBeApprovedByCashier($this->cashier)); $this->assertFalse($document->canBeApprovedByAccountant()); $this->assertFalse($document->canBeApprovedByChair()); } /** @test */ public function workflow_stages_are_correctly_identified() { $document = FinanceDocument::create([ 'title' => 'Test Document', 'description' => 'Test', 'amount' => 1000, 'request_type' => 'petty_cash', 'status' => 'pending', 'submitted_by_id' => $this->requester->id, 'submitted_at' => now(), ]); $document->amount_tier = 'small'; $document->save(); // Stage 1: Approval $this->assertEquals('approval', $document->getCurrentWorkflowStage()); $this->assertFalse($document->isPaymentCompleted()); $this->assertFalse($document->isRecordingComplete()); // Complete approval $document->status = FinanceDocument::STATUS_APPROVED_ACCOUNTANT; $document->cashier_approved_at = now(); $document->accountant_approved_at = now(); $document->save(); $this->assertTrue($document->isApprovalStageComplete()); // Stage 2: Payment (simulate payment order created and executed) $document->payment_order_created_at = now(); $document->payment_verified_at = now(); $document->payment_executed_at = now(); $document->save(); $this->assertTrue($document->isPaymentCompleted()); $this->assertEquals('payment', $document->getCurrentWorkflowStage()); // Stage 3: Recording $document->cashier_recorded_at = now(); $document->save(); $this->assertTrue($document->isRecordingComplete()); // Stage 4: Reconciliation $this->assertEquals('reconciliation', $document->getCurrentWorkflowStage()); $document->bank_reconciliation_id = 1; // Simulate reconciliation $document->save(); $this->assertTrue($document->isReconciled()); $this->assertEquals('completed', $document->getCurrentWorkflowStage()); } /** @test */ public function amount_tier_is_automatically_determined() { $smallDoc = FinanceDocument::factory()->make(['amount' => 3000]); $this->assertEquals('small', $smallDoc->determineAmountTier()); $mediumDoc = FinanceDocument::factory()->make(['amount' => 25000]); $this->assertEquals('medium', $mediumDoc->determineAmountTier()); $largeDoc = FinanceDocument::factory()->make(['amount' => 75000]); $this->assertEquals('large', $largeDoc->determineAmountTier()); } /** @test */ public function board_meeting_requirement_is_correctly_identified() { $smallDoc = FinanceDocument::factory()->make(['amount' => 3000]); $this->assertFalse($smallDoc->needsBoardMeetingApproval()); $mediumDoc = FinanceDocument::factory()->make(['amount' => 25000]); $this->assertFalse($mediumDoc->needsBoardMeetingApproval()); $largeDoc = FinanceDocument::factory()->make(['amount' => 75000]); $this->assertTrue($largeDoc->needsBoardMeetingApproval()); } }