withoutMiddleware([\App\Http\Middleware\EnsureUserIsAdmin::class, \App\Http\Middleware\VerifyCsrfToken::class]); $this->artisan('db:seed', ['--class' => 'FinancialWorkflowPermissionsSeeder']); \Spatie\Permission\Models\Permission::findOrCreate('prepare_bank_reconciliation', 'web'); \Spatie\Permission\Models\Permission::findOrCreate('review_bank_reconciliation', 'web'); \Spatie\Permission\Models\Permission::findOrCreate('approve_bank_reconciliation', 'web'); \Spatie\Permission\Models\Permission::findOrCreate('view_bank_reconciliations', 'web'); Role::firstOrCreate(['name' => 'finance_cashier']); Role::firstOrCreate(['name' => 'finance_accountant']); Role::firstOrCreate(['name' => 'finance_chair']); $this->cashier = User::factory()->create(['email' => 'cashier@test.com']); $this->accountant = User::factory()->create(['email' => 'accountant@test.com']); $this->manager = User::factory()->create(['email' => 'manager@test.com']); $this->cashier->update(['is_admin' => true]); $this->accountant->update(['is_admin' => true]); $this->manager->update(['is_admin' => true]); $this->cashier->assignRole('finance_cashier'); $this->accountant->assignRole('finance_accountant'); $this->manager->assignRole('finance_chair'); $this->cashier->givePermissionTo(['prepare_bank_reconciliation', 'view_bank_reconciliations']); $this->accountant->givePermissionTo(['review_bank_reconciliation', 'view_bank_reconciliations']); $this->manager->givePermissionTo(['approve_bank_reconciliation', 'view_bank_reconciliations']); } /** @test */ public function cashier_can_create_bank_reconciliation() { Storage::fake('local'); $this->actingAs($this->cashier); $statement = UploadedFile::fake()->create('statement.pdf', 100); $response = $this->post(route('admin.bank-reconciliations.store'), [ 'reconciliation_month' => now()->format('Y-m'), 'bank_statement_date' => now()->format('Y-m-d'), 'bank_statement_balance' => 100000, 'system_book_balance' => 95000, 'bank_statement_file' => $statement, 'outstanding_checks' => [ ['check_number' => 'CHK001', 'amount' => 3000, 'description' => 'Vendor payment'], ['check_number' => 'CHK002', 'amount' => 2000, 'description' => 'Service fee'], ], 'deposits_in_transit' => [ ['date' => now()->format('Y-m-d'), 'amount' => 5000, 'description' => 'Member dues'], ], 'bank_charges' => [ ['amount' => 500, 'description' => 'Monthly service charge'], ], 'notes' => 'Monthly reconciliation', ]); $response->assertRedirect(); $this->assertDatabaseHas('bank_reconciliations', [ 'bank_statement_balance' => 100000, 'system_book_balance' => 95000, 'prepared_by_cashier_id' => $this->cashier->id, 'reconciliation_status' => 'pending', ]); } /** @test */ public function reconciliation_calculates_adjusted_balance_correctly() { $reconciliation = BankReconciliation::create([ 'reconciliation_month' => now()->startOfMonth(), 'bank_statement_date' => now(), 'bank_statement_balance' => 100000, 'system_book_balance' => 95000, 'outstanding_checks' => [ ['check_number' => 'CHK001', 'amount' => 3000, 'description' => 'Test'], ['check_number' => 'CHK002', 'amount' => 2000, 'description' => 'Test'], ], 'deposits_in_transit' => [ ['date' => now()->format('Y-m-d'), 'amount' => 5000, 'description' => 'Test'], ], 'bank_charges' => [ ['amount' => 500, 'description' => 'Test'], ], 'prepared_by_cashier_id' => $this->cashier->id, 'prepared_at' => now(), 'reconciliation_status' => 'pending', ]); // Adjusted balance = 95000 + 5000 - 3000 - 2000 - 500 = 94500 $adjustedBalance = $reconciliation->calculateAdjustedBalance(); $this->assertEquals(94500, $adjustedBalance); } /** @test */ public function discrepancy_is_calculated_correctly() { $reconciliation = BankReconciliation::create([ 'reconciliation_month' => now()->startOfMonth(), 'bank_statement_date' => now(), 'bank_statement_balance' => 100000, 'system_book_balance' => 95000, 'outstanding_checks' => [ ['check_number' => 'CHK001', 'amount' => 3000, 'description' => 'Test'], ], 'deposits_in_transit' => [ ['date' => now()->format('Y-m-d'), 'amount' => 5000, 'description' => 'Test'], ], 'bank_charges' => [ ['amount' => 500, 'description' => 'Test'], ], 'prepared_by_cashier_id' => $this->cashier->id, 'prepared_at' => now(), 'reconciliation_status' => 'pending', ]); // Adjusted balance = 95000 + 5000 - 3000 - 500 = 96500 // Discrepancy = |100000 - 96500| = 3500 $discrepancy = $reconciliation->calculateDiscrepancy(); $this->assertEquals(3500, $discrepancy); $reconciliation->discrepancy_amount = $discrepancy; $reconciliation->save(); $this->assertTrue($reconciliation->hasDiscrepancy()); } /** @test */ public function accountant_can_review_reconciliation() { $reconciliation = BankReconciliation::create([ 'reconciliation_month' => now()->startOfMonth(), 'bank_statement_date' => now(), 'bank_statement_balance' => 100000, 'system_book_balance' => 100000, 'outstanding_checks' => [], 'deposits_in_transit' => [], 'bank_charges' => [], 'prepared_by_cashier_id' => $this->cashier->id, 'prepared_at' => now(), 'reconciliation_status' => 'pending', 'discrepancy_amount' => 0, ]); $this->actingAs($this->accountant); $response = $this->post(route('admin.bank-reconciliations.review', $reconciliation), [ 'review_notes' => 'Reviewed and looks correct', ]); $response->assertRedirect(); $reconciliation->refresh(); $this->assertNotNull($reconciliation->reviewed_at); $this->assertEquals($this->accountant->id, $reconciliation->reviewed_by_accountant_id); } /** @test */ public function manager_can_approve_reviewed_reconciliation() { $reconciliation = BankReconciliation::create([ 'reconciliation_month' => now()->startOfMonth(), 'bank_statement_date' => now(), 'bank_statement_balance' => 100000, 'system_book_balance' => 100000, 'outstanding_checks' => [], 'deposits_in_transit' => [], 'bank_charges' => [], 'prepared_by_cashier_id' => $this->cashier->id, 'prepared_at' => now(), 'reviewed_by_accountant_id' => $this->accountant->id, 'reviewed_at' => now(), 'reconciliation_status' => 'pending', 'discrepancy_amount' => 0, ]); $this->actingAs($this->manager); $response = $this->post(route('admin.bank-reconciliations.approve', $reconciliation), [ 'approval_notes' => 'Approved', ]); $response->assertRedirect(); $reconciliation->refresh(); $this->assertNotNull($reconciliation->approved_at); $this->assertEquals($this->manager->id, $reconciliation->approved_by_manager_id); $this->assertEquals('completed', $reconciliation->reconciliation_status); } /** @test */ public function cannot_approve_unreviewed_reconciliation() { $reconciliation = BankReconciliation::create([ 'reconciliation_month' => now()->startOfMonth(), 'bank_statement_date' => now(), 'bank_statement_balance' => 100000, 'system_book_balance' => 100000, 'outstanding_checks' => [], 'deposits_in_transit' => [], 'bank_charges' => [], 'prepared_by_cashier_id' => $this->cashier->id, 'prepared_at' => now(), 'reconciliation_status' => 'pending', 'discrepancy_amount' => 0, ]); $this->assertFalse($reconciliation->canBeApproved()); } /** @test */ public function reconciliation_with_large_discrepancy_is_flagged() { $reconciliation = BankReconciliation::create([ 'reconciliation_month' => now()->startOfMonth(), 'bank_statement_date' => now(), 'bank_statement_balance' => 100000, 'system_book_balance' => 90000, // Large discrepancy 'outstanding_checks' => [], 'deposits_in_transit' => [], 'bank_charges' => [], 'prepared_by_cashier_id' => $this->cashier->id, 'prepared_at' => now(), 'reconciliation_status' => 'pending', 'discrepancy_amount' => 10000, ]); $this->assertTrue($reconciliation->hasDiscrepancy()); $this->assertTrue($reconciliation->hasUnresolvedDiscrepancy()); } /** @test */ public function outstanding_items_summary_is_calculated_correctly() { $reconciliation = BankReconciliation::create([ 'reconciliation_month' => now()->startOfMonth(), 'bank_statement_date' => now(), 'bank_statement_balance' => 100000, 'system_book_balance' => 95000, 'outstanding_checks' => [ ['check_number' => 'CHK001', 'amount' => 3000, 'description' => 'Test 1'], ['check_number' => 'CHK002', 'amount' => 2000, 'description' => 'Test 2'], ], 'deposits_in_transit' => [ ['date' => now()->format('Y-m-d'), 'amount' => 5000, 'description' => 'Test 1'], ['date' => now()->format('Y-m-d'), 'amount' => 3000, 'description' => 'Test 2'], ], 'bank_charges' => [ ['amount' => 500, 'description' => 'Test 1'], ['amount' => 200, 'description' => 'Test 2'], ], 'prepared_by_cashier_id' => $this->cashier->id, 'prepared_at' => now(), 'reconciliation_status' => 'pending', ]); $summary = $reconciliation->getOutstandingItemsSummary(); $this->assertEquals(5000, $summary['total_outstanding_checks']); $this->assertEquals(2, $summary['outstanding_checks_count']); $this->assertEquals(8000, $summary['total_deposits_in_transit']); $this->assertEquals(2, $summary['deposits_in_transit_count']); $this->assertEquals(700, $summary['total_bank_charges']); $this->assertEquals(2, $summary['bank_charges_count']); } /** @test */ public function completed_reconciliation_can_generate_pdf() { $reconciliation = BankReconciliation::create([ 'reconciliation_month' => now()->startOfMonth(), 'bank_statement_date' => now(), 'bank_statement_balance' => 100000, 'system_book_balance' => 100000, 'outstanding_checks' => [], 'deposits_in_transit' => [], 'bank_charges' => [], 'prepared_by_cashier_id' => $this->cashier->id, 'prepared_at' => now(), 'reviewed_by_accountant_id' => $this->accountant->id, 'reviewed_at' => now(), 'approved_by_manager_id' => $this->manager->id, 'approved_at' => now(), 'reconciliation_status' => 'completed', 'discrepancy_amount' => 0, ]); $this->assertTrue($reconciliation->isCompleted()); $this->actingAs($this->cashier); $response = $this->get(route('admin.bank-reconciliations.pdf', $reconciliation)); $response->assertStatus(200); } /** @test */ public function reconciliation_status_text_is_correct() { $pending = new BankReconciliation(['reconciliation_status' => 'pending']); $this->assertEquals('待覆核', $pending->getStatusText()); $completed = new BankReconciliation(['reconciliation_status' => 'completed']); $this->assertEquals('已完成', $completed->getStatusText()); $discrepancy = new BankReconciliation(['reconciliation_status' => 'discrepancy']); $this->assertEquals('有差異', $discrepancy->getStatusText()); } /** @test */ public function reconciliation_workflow_is_sequential() { $reconciliation = BankReconciliation::create([ 'reconciliation_month' => now()->startOfMonth(), 'bank_statement_date' => now(), 'bank_statement_balance' => 100000, 'system_book_balance' => 100000, 'outstanding_checks' => [], 'deposits_in_transit' => [], 'bank_charges' => [], 'prepared_by_cashier_id' => $this->cashier->id, 'prepared_at' => now(), 'reconciliation_status' => 'pending', 'discrepancy_amount' => 0, ]); // Can be reviewed initially $this->assertTrue($reconciliation->canBeReviewed()); $this->assertFalse($reconciliation->canBeApproved()); // After review $reconciliation->reviewed_by_accountant_id = $this->accountant->id; $reconciliation->reviewed_at = now(); $reconciliation->save(); $this->assertFalse($reconciliation->canBeReviewed()); $this->assertTrue($reconciliation->canBeApproved()); // After approval $reconciliation->approved_by_manager_id = $this->manager->id; $reconciliation->approved_at = now(); $reconciliation->reconciliation_status = 'completed'; $reconciliation->save(); $this->assertFalse($reconciliation->canBeReviewed()); $this->assertFalse($reconciliation->canBeApproved()); $this->assertTrue($reconciliation->isCompleted()); } }