seedRolesAndPermissions(); $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 Cashier + Accountant 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()); // Cashier approves $this->actingAs($this->cashier)->post( route('admin.finance-documents.approve', $document) ); $document->refresh(); $this->assertEquals(FinanceDocument::STATUS_APPROVED_CASHIER, $document->status); // Accountant approves - should be fully approved for small amounts $this->actingAs($this->accountant)->post( route('admin.finance-documents.approve', $document) ); $document->refresh(); // Small amounts may be fully approved after accountant $this->assertTrue( $document->status === FinanceDocument::STATUS_APPROVED_ACCOUNTANT || $document->status === FinanceDocument::STATUS_APPROVED_CHAIR ); } /** * Test medium amount (5000 - 50000) complete workflow * Medium amounts require Cashier + Accountant + 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: Cashier approves $this->actingAs($this->cashier)->post( route('admin.finance-documents.approve', $document) ); $document->refresh(); $this->assertEquals(FinanceDocument::STATUS_APPROVED_CASHIER, $document->status); // Stage 2: Accountant approves $this->actingAs($this->accountant)->post( route('admin.finance-documents.approve', $document) ); $document->refresh(); $this->assertEquals(FinanceDocument::STATUS_APPROVED_ACCOUNTANT, $document->status); // Stage 3: Chair approves - final approval $this->actingAs($this->chair)->post( route('admin.finance-documents.approve', $document) ); $document->refresh(); $this->assertEquals(FinanceDocument::STATUS_APPROVED_CHAIR, $document->status); } /** * 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: Cashier → Accountant → Chair → Board $this->actingAs($this->cashier)->post( route('admin.finance-documents.approve', $document) ); $document->refresh(); $this->actingAs($this->accountant)->post( route('admin.finance-documents.approve', $document) ); $document->refresh(); $this->actingAs($this->chair)->post( route('admin.finance-documents.approve', $document) ); $document->refresh(); // For large amounts, may need board approval if ($document->requiresBoardApproval()) { $this->actingAs($this->boardMember)->post( route('admin.finance-documents.approve', $document) ); $document->refresh(); $this->assertEquals(FinanceDocument::STATUS_APPROVED_BOARD, $document->status); } } /** * Test finance document to payment order to execution flow */ public function test_finance_document_to_payment_order_to_execution(): void { // Create approved document $document = $this->createDocumentAtStage('chair_approved', [ 'amount' => 10000, 'payee_name' => 'Test Vendor', ]); // Stage 2: Accountant creates payment order $response = $this->actingAs($this->accountant)->post( route('admin.payment-orders.store'), [ 'finance_document_id' => $document->id, 'payment_method' => 'bank_transfer', 'bank_name' => 'Test Bank', 'account_number' => '1234567890', 'account_name' => 'Test Vendor', 'notes' => 'Payment for approved document', ] ); $paymentOrder = PaymentOrder::where('finance_document_id', $document->id)->first(); $this->assertNotNull($paymentOrder); $this->assertEquals(PaymentOrder::STATUS_PENDING_VERIFICATION, $paymentOrder->status); // Cashier verifies payment order $this->actingAs($this->cashier)->post( route('admin.payment-orders.verify', $paymentOrder) ); $paymentOrder->refresh(); $this->assertEquals(PaymentOrder::STATUS_VERIFIED, $paymentOrder->status); // Cashier executes payment $this->actingAs($this->cashier)->post( route('admin.payment-orders.execute', $paymentOrder), ['execution_notes' => 'Payment executed via bank transfer'] ); $paymentOrder->refresh(); $this->assertEquals(PaymentOrder::STATUS_EXECUTED, $paymentOrder->status); $this->assertNotNull($paymentOrder->executed_at); } /** * Test payment order to cashier ledger entry flow */ public function test_payment_order_to_cashier_ledger_entry(): void { // Create executed payment order $paymentOrder = $this->createPaymentOrderAtStage('executed', [ 'amount' => 5000, ]); // Cashier records ledger entry $response = $this->actingAs($this->cashier)->post( route('admin.cashier-ledger.store'), [ 'finance_document_id' => $paymentOrder->finance_document_id, 'entry_type' => 'payment', 'entry_date' => now()->format('Y-m-d'), 'amount' => 5000, 'payment_method' => 'bank_transfer', 'bank_account' => 'Main Operating Account', 'notes' => 'Payment for invoice #123', ] ); $response->assertRedirect(); $ledgerEntry = CashierLedgerEntry::where('finance_document_id', $paymentOrder->finance_document_id)->first(); $this->assertNotNull($ledgerEntry); $this->assertEquals('payment', $ledgerEntry->entry_type); $this->assertEquals(5000, $ledgerEntry->amount); $this->assertEquals($this->cashier->id, $ledgerEntry->recorded_by_cashier_id); } /** * 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 $response = $this->actingAs($this->cashier)->post( route('admin.bank-reconciliations.store'), [ 'reconciliation_month' => now()->format('Y-m'), 'bank_statement_date' => now()->format('Y-m-d'), 'bank_statement_balance' => 70000, 'system_book_balance' => 70000, 'outstanding_checks' => [], 'deposits_in_transit' => [], 'bank_charges' => [], 'notes' => 'Monthly reconciliation', ] ); $reconciliation = BankReconciliation::latest()->first(); $this->assertNotNull($reconciliation); $this->assertEquals(0, $reconciliation->discrepancy_amount); $this->assertFalse($reconciliation->hasDiscrepancy()); } /** * Test complete 4-stage financial workflow */ public function test_complete_4_stage_financial_workflow(): void { $submitter = User::factory()->create(); // Stage 1: Create and approve finance document $document = FinanceDocument::factory()->create([ 'title' => 'Complete Workflow Test', 'amount' => 25000, 'status' => FinanceDocument::STATUS_PENDING, 'submitted_by_user_id' => $submitter->id, 'request_type' => FinanceDocument::TYPE_EXPENSE_REIMBURSEMENT, ]); // Approve through all stages $this->actingAs($this->cashier)->post(route('admin.finance-documents.approve', $document)); $document->refresh(); $this->actingAs($this->accountant)->post(route('admin.finance-documents.approve', $document)); $document->refresh(); $this->actingAs($this->chair)->post(route('admin.finance-documents.approve', $document)); $document->refresh(); $this->assertEquals(FinanceDocument::STATUS_APPROVED_CHAIR, $document->status); // Stage 2: Create and execute payment order $this->actingAs($this->accountant)->post(route('admin.payment-orders.store'), [ 'finance_document_id' => $document->id, 'payment_method' => 'bank_transfer', 'bank_name' => 'Test Bank', 'account_number' => '9876543210', 'account_name' => 'Submitter Name', ]); $paymentOrder = PaymentOrder::where('finance_document_id', $document->id)->first(); $this->assertNotNull($paymentOrder); $this->actingAs($this->cashier)->post(route('admin.payment-orders.verify', $paymentOrder)); $paymentOrder->refresh(); $this->actingAs($this->cashier)->post(route('admin.payment-orders.execute', $paymentOrder)); $paymentOrder->refresh(); $this->assertEquals(PaymentOrder::STATUS_EXECUTED, $paymentOrder->status); // Stage 3: Record ledger entry $this->actingAs($this->cashier)->post(route('admin.cashier-ledger.store'), [ 'finance_document_id' => $document->id, 'entry_type' => 'payment', 'entry_date' => now()->format('Y-m-d'), 'amount' => 25000, 'payment_method' => 'bank_transfer', 'bank_account' => 'Operating Account', ]); $ledgerEntry = CashierLedgerEntry::where('finance_document_id', $document->id)->first(); $this->assertNotNull($ledgerEntry); // Stage 4: Bank reconciliation $this->actingAs($this->cashier)->post(route('admin.bank-reconciliations.store'), [ 'reconciliation_month' => now()->format('Y-m'), 'bank_statement_date' => now()->format('Y-m-d'), 'bank_statement_balance' => 75000, 'system_book_balance' => 75000, 'outstanding_checks' => [], 'deposits_in_transit' => [], 'bank_charges' => [], ]); $reconciliation = BankReconciliation::latest()->first(); // Accountant reviews $this->actingAs($this->accountant)->post( route('admin.bank-reconciliations.review', $reconciliation), ['review_notes' => 'Reviewed and verified'] ); $reconciliation->refresh(); $this->assertNotNull($reconciliation->reviewed_at); // Manager/Chair approves $this->actingAs($this->chair)->post( route('admin.bank-reconciliations.approve', $reconciliation), ['approval_notes' => 'Approved'] ); $reconciliation->refresh(); $this->assertEquals('completed', $reconciliation->reconciliation_status); $this->assertTrue($reconciliation->isCompleted()); } /** * Test rejection at each approval stage */ public function test_rejection_at_each_approval_stage(): void { // Test rejection at cashier stage $doc1 = $this->createFinanceDocument(['status' => FinanceDocument::STATUS_PENDING]); $this->actingAs($this->cashier)->post( route('admin.finance-documents.reject', $doc1), ['rejection_reason' => 'Missing documentation'] ); $doc1->refresh(); $this->assertEquals(FinanceDocument::STATUS_REJECTED, $doc1->status); // Test rejection at accountant stage $doc2 = $this->createDocumentAtStage('cashier_approved'); $this->actingAs($this->accountant)->post( route('admin.finance-documents.reject', $doc2), ['rejection_reason' => 'Amount exceeds policy limit'] ); $doc2->refresh(); $this->assertEquals(FinanceDocument::STATUS_REJECTED, $doc2->status); // Test rejection at chair stage $doc3 = $this->createDocumentAtStage('accountant_approved'); $this->actingAs($this->chair)->post( route('admin.finance-documents.reject', $doc3), ['rejection_reason' => 'Not within budget allocation'] ); $doc3->refresh(); $this->assertEquals(FinanceDocument::STATUS_REJECTED, $doc3->status); } /** * Test workflow with different payment methods */ public function test_workflow_with_different_payment_methods(): void { $paymentMethods = ['cash', 'bank_transfer', 'check']; foreach ($paymentMethods as $method) { $document = $this->createDocumentAtStage('chair_approved', [ 'amount' => 5000, ]); $this->actingAs($this->accountant)->post(route('admin.payment-orders.store'), [ 'finance_document_id' => $document->id, 'payment_method' => $method, 'bank_name' => $method === 'bank_transfer' ? 'Test Bank' : null, 'account_number' => $method === 'bank_transfer' ? '1234567890' : null, 'check_number' => $method === 'check' ? 'CHK001' : null, ]); $paymentOrder = PaymentOrder::where('finance_document_id', $document->id)->first(); $this->assertNotNull($paymentOrder); $this->assertEquals($method, $paymentOrder->payment_method); } } /** * Test budget integration with finance documents */ public function test_budget_integration_with_finance_documents(): void { $budget = $this->createBudgetWithItems(3, [ 'status' => 'active', 'fiscal_year' => now()->year, ]); $budgetItem = $budget->items->first(); $document = FinanceDocument::factory()->create([ 'amount' => 10000, 'budget_item_id' => $budgetItem->id, 'status' => FinanceDocument::STATUS_PENDING, ]); $this->assertEquals($budgetItem->id, $document->budget_item_id); // Approve through workflow $this->actingAs($this->cashier)->post(route('admin.finance-documents.approve', $document)); $this->actingAs($this->accountant)->post(route('admin.finance-documents.approve', $document->fresh())); $this->actingAs($this->chair)->post(route('admin.finance-documents.approve', $document->fresh())); $document->refresh(); $this->assertEquals(FinanceDocument::STATUS_APPROVED_CHAIR, $document->status); } }