seedRolesAndPermissions(); $this->secretary = $this->createSecretary(['email' => 'secretary@test.com']); $this->cashier = $this->createCashier(['email' => 'cashier@test.com']); $this->accountant = $this->createAccountant(['email' => 'accountant@test.com']); $this->chair = $this->createChair(['email' => 'chair@test.com']); $this->boardMember = $this->createBoardMember(['email' => 'board@test.com']); } /** * Test small amount (< 5000) complete workflow * Small amounts only require Secretary approval */ public function test_small_amount_complete_workflow(): void { // Create small amount document $document = $this->createSmallAmountDocument([ 'status' => FinanceDocument::STATUS_PENDING, 'submitted_by_user_id' => User::factory()->create()->id, ]); $this->assertEquals(FinanceDocument::AMOUNT_TIER_SMALL, $document->determineAmountTier()); // Secretary approves - should be fully approved for small amounts $this->actingAs($this->secretary)->post( route('admin.finance.approve', $document) ); $document->refresh(); $this->assertEquals(FinanceDocument::STATUS_APPROVED_SECRETARY, $document->status); // Small amounts are complete after secretary approval $this->assertTrue($document->isApprovalComplete()); } /** * Test medium amount (5000 - 50000) complete workflow * Medium amounts require Secretary + Chair approval */ public function test_medium_amount_complete_workflow(): void { $document = $this->createMediumAmountDocument([ 'status' => FinanceDocument::STATUS_PENDING, 'submitted_by_user_id' => User::factory()->create()->id, ]); $this->assertEquals(FinanceDocument::AMOUNT_TIER_MEDIUM, $document->determineAmountTier()); // Stage 1: Secretary approves $this->actingAs($this->secretary)->post( route('admin.finance.approve', $document) ); $document->refresh(); $this->assertEquals(FinanceDocument::STATUS_APPROVED_SECRETARY, $document->status); // Stage 2: Chair approves - final approval for medium amounts $this->actingAs($this->chair)->post( route('admin.finance.approve', $document) ); $document->refresh(); $this->assertEquals(FinanceDocument::STATUS_APPROVED_CHAIR, $document->status); $this->assertTrue($document->isApprovalComplete()); } /** * Test large amount (> 50000) complete workflow with board approval */ public function test_large_amount_complete_workflow_with_board_approval(): void { $document = $this->createLargeAmountDocument([ 'status' => FinanceDocument::STATUS_PENDING, 'submitted_by_user_id' => User::factory()->create()->id, ]); $this->assertEquals(FinanceDocument::AMOUNT_TIER_LARGE, $document->determineAmountTier()); // Approval sequence: Secretary → Chair → Board $this->actingAs($this->secretary)->post( route('admin.finance.approve', $document) ); $document->refresh(); $this->assertEquals(FinanceDocument::STATUS_APPROVED_SECRETARY, $document->status); $this->actingAs($this->chair)->post( route('admin.finance.approve', $document) ); $document->refresh(); $this->assertEquals(FinanceDocument::STATUS_APPROVED_CHAIR, $document->status); // Board approval for large amounts $this->actingAs($this->boardMember)->post( route('admin.finance.approve', $document) ); $document->refresh(); $this->assertEquals(FinanceDocument::STATUS_APPROVED_BOARD, $document->status); $this->assertTrue($document->isApprovalComplete()); } /** * Test cashier ledger to bank reconciliation flow */ public function test_cashier_ledger_to_bank_reconciliation(): void { // Create ledger entries $this->createReceiptEntry(100000, 'Main Account', [ 'recorded_by_cashier_id' => $this->cashier->id, ]); $this->createPaymentEntry(30000, 'Main Account', [ 'recorded_by_cashier_id' => $this->cashier->id, ]); // Current balance should be 70000 $balance = CashierLedgerEntry::getLatestBalance('Main Account'); $this->assertEquals(70000, $balance); // Create bank reconciliation $reconciliation = $this->createBankReconciliation([ 'bank_statement_balance' => 70000, 'system_book_balance' => 70000, 'discrepancy_amount' => 0, ]); $this->assertEquals(0, $reconciliation->discrepancy_amount); } /** * Test rejection at each approval stage */ public function test_rejection_at_each_approval_stage(): void { // Test rejection at secretary stage $doc1 = $this->createFinanceDocument(['status' => FinanceDocument::STATUS_PENDING]); $this->actingAs($this->secretary)->post( route('admin.finance.reject', $doc1), ['rejection_reason' => 'Missing documentation'] ); $doc1->refresh(); $this->assertEquals(FinanceDocument::STATUS_REJECTED, $doc1->status); // Test rejection at chair stage $doc2 = $this->createDocumentAtStage('secretary_approved', ['amount' => 25000]); $this->actingAs($this->chair)->post( route('admin.finance.reject', $doc2), ['rejection_reason' => 'Amount exceeds policy limit'] ); $doc2->refresh(); $this->assertEquals(FinanceDocument::STATUS_REJECTED, $doc2->status); // Test rejection at board stage $doc3 = $this->createDocumentAtStage('chair_approved', ['amount' => 75000]); $this->actingAs($this->boardMember)->post( route('admin.finance.reject', $doc3), ['rejection_reason' => 'Not within budget allocation'] ); $doc3->refresh(); $this->assertEquals(FinanceDocument::STATUS_REJECTED, $doc3->status); } /** * Test amount tier determination */ public function test_amount_tier_determination(): void { $smallDoc = $this->createFinanceDocument(['amount' => 3000]); $this->assertEquals(FinanceDocument::AMOUNT_TIER_SMALL, $smallDoc->determineAmountTier()); $mediumDoc = $this->createFinanceDocument(['amount' => 25000]); $this->assertEquals(FinanceDocument::AMOUNT_TIER_MEDIUM, $mediumDoc->determineAmountTier()); $largeDoc = $this->createFinanceDocument(['amount' => 75000]); $this->assertEquals(FinanceDocument::AMOUNT_TIER_LARGE, $largeDoc->determineAmountTier()); } /** * Test document status constants match workflow */ public function test_document_status_constants(): void { $this->assertEquals('pending', FinanceDocument::STATUS_PENDING); $this->assertEquals('approved_secretary', FinanceDocument::STATUS_APPROVED_SECRETARY); $this->assertEquals('approved_chair', FinanceDocument::STATUS_APPROVED_CHAIR); $this->assertEquals('approved_board', FinanceDocument::STATUS_APPROVED_BOARD); $this->assertEquals('rejected', FinanceDocument::STATUS_REJECTED); } }