Add membership fee system with disability discount and fix document permissions

Features:
- Implement two fee types: entrance fee and annual fee (both NT$1,000)
- Add 50% discount for disability certificate holders
- Add disability certificate upload in member profile
- Integrate disability verification into cashier approval workflow
- Add membership fee settings in system admin

Document permissions:
- Fix hard-coded role logic in Document model
- Use permission-based authorization instead of role checks

Additional features:
- Add announcements, general ledger, and trial balance modules
- Add income management and accounting entries
- Add comprehensive test suite with factories
- Update UI translations to Traditional Chinese

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-01 09:56:01 +08:00
parent 83ce1f7fc8
commit 642b879dd4
207 changed files with 19487 additions and 3048 deletions

View File

@@ -0,0 +1,247 @@
<?php
namespace Tests\Feature\BankReconciliation;
use App\Models\BankReconciliation;
use App\Models\CashierLedger;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
use Tests\Traits\CreatesFinanceData;
use Tests\Traits\SeedsRolesAndPermissions;
/**
* Bank Reconciliation Tests
*
* Tests bank reconciliation in the 4-stage finance workflow.
*/
class BankReconciliationTest extends TestCase
{
use RefreshDatabase, SeedsRolesAndPermissions, CreatesFinanceData;
protected function setUp(): void
{
parent::setUp();
Storage::fake('local');
$this->seedRolesAndPermissions();
}
/**
* Test can view bank reconciliation page
*/
public function test_can_view_bank_reconciliation_page(): void
{
$accountant = $this->createAccountant();
$response = $this->actingAs($accountant)->get(
route('admin.bank-reconciliation.index')
);
$response->assertStatus(200);
}
/**
* Test can create reconciliation
*/
public function test_can_create_reconciliation(): void
{
$accountant = $this->createAccountant();
$response = $this->actingAs($accountant)->post(
route('admin.bank-reconciliation.store'),
[
'reconciliation_date' => now()->toDateString(),
'bank_statement_balance' => 500000,
'ledger_balance' => 500000,
'notes' => '月末對帳',
]
);
$response->assertRedirect();
$this->assertDatabaseHas('bank_reconciliations', [
'bank_statement_balance' => 500000,
]);
}
/**
* Test reconciliation detects discrepancy
*/
public function test_reconciliation_detects_discrepancy(): void
{
$accountant = $this->createAccountant();
$reconciliation = $this->createBankReconciliation([
'bank_statement_balance' => 500000,
'ledger_balance' => 480000,
]);
$this->assertNotEquals(
$reconciliation->bank_statement_balance,
$reconciliation->ledger_balance
);
$discrepancy = $reconciliation->bank_statement_balance - $reconciliation->ledger_balance;
$this->assertEquals(20000, $discrepancy);
}
/**
* Test can upload bank statement
*/
public function test_can_upload_bank_statement(): void
{
$accountant = $this->createAccountant();
$file = UploadedFile::fake()->create('bank_statement.pdf', 1024);
$response = $this->actingAs($accountant)->post(
route('admin.bank-reconciliation.store'),
[
'reconciliation_date' => now()->toDateString(),
'bank_statement_balance' => 500000,
'ledger_balance' => 500000,
'bank_statement_file' => $file,
]
);
$response->assertRedirect();
}
/**
* Test reconciliation marks ledger entries
*/
public function test_reconciliation_marks_ledger_entries(): void
{
$accountant = $this->createAccountant();
$entry1 = $this->createCashierLedgerEntry(['is_reconciled' => false]);
$entry2 = $this->createCashierLedgerEntry(['is_reconciled' => false]);
$response = $this->actingAs($accountant)->post(
route('admin.bank-reconciliation.store'),
[
'reconciliation_date' => now()->toDateString(),
'bank_statement_balance' => 100000,
'ledger_balance' => 100000,
'ledger_entry_ids' => [$entry1->id, $entry2->id],
]
);
$entry1->refresh();
$entry2->refresh();
$this->assertTrue($entry1->is_reconciled);
$this->assertTrue($entry2->is_reconciled);
}
/**
* Test reconciliation status tracking
*/
public function test_reconciliation_status_tracking(): void
{
$reconciliation = $this->createBankReconciliation([
'status' => BankReconciliation::STATUS_PENDING,
]);
$this->assertEquals(BankReconciliation::STATUS_PENDING, $reconciliation->status);
}
/**
* Test reconciliation approval
*/
public function test_reconciliation_approval(): void
{
$chair = $this->createChair();
$reconciliation = $this->createBankReconciliation([
'status' => BankReconciliation::STATUS_PENDING,
]);
$response = $this->actingAs($chair)->post(
route('admin.bank-reconciliation.approve', $reconciliation)
);
$reconciliation->refresh();
$this->assertEquals(BankReconciliation::STATUS_APPROVED, $reconciliation->status);
}
/**
* Test reconciliation date filter
*/
public function test_reconciliation_date_filter(): void
{
$accountant = $this->createAccountant();
$this->createBankReconciliation([
'reconciliation_date' => now()->subMonth(),
]);
$this->createBankReconciliation([
'reconciliation_date' => now(),
]);
$response = $this->actingAs($accountant)->get(
route('admin.bank-reconciliation.index', [
'start_date' => now()->startOfMonth()->toDateString(),
'end_date' => now()->endOfMonth()->toDateString(),
])
);
$response->assertStatus(200);
}
/**
* Test reconciliation requires matching balances warning
*/
public function test_reconciliation_requires_matching_balances_warning(): void
{
$accountant = $this->createAccountant();
$response = $this->actingAs($accountant)->post(
route('admin.bank-reconciliation.store'),
[
'reconciliation_date' => now()->toDateString(),
'bank_statement_balance' => 500000,
'ledger_balance' => 400000,
]
);
// Should still create but flag discrepancy
$this->assertDatabaseHas('bank_reconciliations', [
'has_discrepancy' => true,
]);
}
/**
* Test reconciliation history
*/
public function test_reconciliation_history(): void
{
$accountant = $this->createAccountant();
for ($i = 0; $i < 3; $i++) {
$this->createBankReconciliation([
'reconciliation_date' => now()->subMonths($i),
]);
}
$response = $this->actingAs($accountant)->get(
route('admin.bank-reconciliation.history')
);
$response->assertStatus(200);
}
/**
* Test only authorized users can reconcile
*/
public function test_only_authorized_users_can_reconcile(): void
{
$regularUser = User::factory()->create();
$response = $this->actingAs($regularUser)->get(
route('admin.bank-reconciliation.index')
);
$response->assertStatus(403);
}
}