369 lines
14 KiB
PHP
369 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\BankReconciliation;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Foundation\Testing\WithFaker;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Spatie\Permission\Models\Role;
|
|
use Tests\TestCase;
|
|
|
|
/**
|
|
* Bank Reconciliation Workflow Feature Tests
|
|
*
|
|
* Tests bank reconciliation creation, review, and approval
|
|
*/
|
|
class BankReconciliationWorkflowTest extends TestCase
|
|
{
|
|
use RefreshDatabase, WithFaker;
|
|
|
|
protected User $cashier;
|
|
protected User $accountant;
|
|
protected User $manager;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
Role::create(['name' => 'finance_cashier']);
|
|
Role::create(['name' => 'finance_accountant']);
|
|
Role::create(['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->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());
|
|
}
|
|
}
|