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,273 @@
<?php
namespace Tests\Traits;
use App\Models\BankReconciliation;
use App\Models\Budget;
use App\Models\BudgetItem;
use App\Models\CashierLedgerEntry;
use App\Models\ChartOfAccount;
use App\Models\FinanceDocument;
use App\Models\PaymentOrder;
use App\Models\User;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
trait CreatesFinanceData
{
/**
* Create a finance document at a specific approval stage
*/
protected function createFinanceDocument(array $attributes = []): FinanceDocument
{
return FinanceDocument::factory()->create($attributes);
}
/**
* Create a small amount finance document (< 5000)
*/
protected function createSmallAmountDocument(array $attributes = []): FinanceDocument
{
$doc = $this->createFinanceDocument(array_merge([
'amount' => 3000,
], $attributes));
// Verify it's small amount
assert($doc->determineAmountTier() === FinanceDocument::AMOUNT_TIER_SMALL);
return $doc;
}
/**
* Create a medium amount finance document (5000 - 50000)
*/
protected function createMediumAmountDocument(array $attributes = []): FinanceDocument
{
$doc = $this->createFinanceDocument(array_merge([
'amount' => 25000,
], $attributes));
// Verify it's medium amount
assert($doc->determineAmountTier() === FinanceDocument::AMOUNT_TIER_MEDIUM);
return $doc;
}
/**
* Create a large amount finance document (> 50000)
*/
protected function createLargeAmountDocument(array $attributes = []): FinanceDocument
{
$doc = $this->createFinanceDocument(array_merge([
'amount' => 75000,
], $attributes));
// Verify it's large amount
assert($doc->determineAmountTier() === FinanceDocument::AMOUNT_TIER_LARGE);
return $doc;
}
/**
* Create a finance document at specific approval stage
*/
protected function createDocumentAtStage(string $stage, array $attributes = []): FinanceDocument
{
$statusMap = [
'pending' => FinanceDocument::STATUS_PENDING,
'cashier_approved' => FinanceDocument::STATUS_APPROVED_CASHIER,
'accountant_approved' => FinanceDocument::STATUS_APPROVED_ACCOUNTANT,
'chair_approved' => FinanceDocument::STATUS_APPROVED_CHAIR,
'rejected' => FinanceDocument::STATUS_REJECTED,
];
return $this->createFinanceDocument(array_merge([
'status' => $statusMap[$stage] ?? FinanceDocument::STATUS_PENDING,
], $attributes));
}
/**
* Create a payment order
*/
protected function createPaymentOrder(array $attributes = []): PaymentOrder
{
if (!isset($attributes['finance_document_id'])) {
$document = $this->createDocumentAtStage('chair_approved');
$attributes['finance_document_id'] = $document->id;
}
return PaymentOrder::factory()->create($attributes);
}
/**
* Create a payment order at specific stage
*/
protected function createPaymentOrderAtStage(string $stage, array $attributes = []): PaymentOrder
{
$statusMap = [
'draft' => PaymentOrder::STATUS_DRAFT,
'pending_verification' => PaymentOrder::STATUS_PENDING_VERIFICATION,
'verified' => PaymentOrder::STATUS_VERIFIED,
'executed' => PaymentOrder::STATUS_EXECUTED,
'cancelled' => PaymentOrder::STATUS_CANCELLED,
];
return $this->createPaymentOrder(array_merge([
'status' => $statusMap[$stage] ?? PaymentOrder::STATUS_DRAFT,
], $attributes));
}
/**
* Create a cashier ledger entry
*/
protected function createCashierLedgerEntry(array $attributes = []): CashierLedgerEntry
{
$cashier = $attributes['recorded_by_cashier_id'] ?? User::factory()->create()->id;
return CashierLedgerEntry::create(array_merge([
'entry_type' => 'receipt',
'entry_date' => now(),
'amount' => 10000,
'payment_method' => 'bank_transfer',
'bank_account' => 'Test Bank Account',
'balance_before' => 0,
'balance_after' => 10000,
'recorded_by_cashier_id' => $cashier,
'recorded_at' => now(),
], $attributes));
}
/**
* Create a receipt entry (income)
*/
protected function createReceiptEntry(int $amount, string $bankAccount = 'Test Account', array $attributes = []): CashierLedgerEntry
{
$latestBalance = CashierLedgerEntry::getLatestBalance($bankAccount);
return $this->createCashierLedgerEntry(array_merge([
'entry_type' => 'receipt',
'amount' => $amount,
'bank_account' => $bankAccount,
'balance_before' => $latestBalance,
'balance_after' => $latestBalance + $amount,
], $attributes));
}
/**
* Create a payment entry (expense)
*/
protected function createPaymentEntry(int $amount, string $bankAccount = 'Test Account', array $attributes = []): CashierLedgerEntry
{
$latestBalance = CashierLedgerEntry::getLatestBalance($bankAccount);
return $this->createCashierLedgerEntry(array_merge([
'entry_type' => 'payment',
'amount' => $amount,
'bank_account' => $bankAccount,
'balance_before' => $latestBalance,
'balance_after' => $latestBalance - $amount,
], $attributes));
}
/**
* Create a bank reconciliation
*/
protected function createBankReconciliation(array $attributes = []): BankReconciliation
{
$cashier = $attributes['prepared_by_cashier_id'] ?? User::factory()->create()->id;
return BankReconciliation::create(array_merge([
'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' => $cashier,
'prepared_at' => now(),
'reconciliation_status' => 'pending',
'discrepancy_amount' => 0,
], $attributes));
}
/**
* Create a bank reconciliation with discrepancy
*/
protected function createReconciliationWithDiscrepancy(int $discrepancy, array $attributes = []): BankReconciliation
{
return $this->createBankReconciliation(array_merge([
'bank_statement_balance' => 100000,
'system_book_balance' => 100000 - $discrepancy,
'discrepancy_amount' => $discrepancy,
'reconciliation_status' => 'discrepancy',
], $attributes));
}
/**
* Create a completed bank reconciliation
*/
protected function createCompletedReconciliation(array $attributes = []): BankReconciliation
{
$cashier = User::factory()->create();
$accountant = User::factory()->create();
$manager = User::factory()->create();
return $this->createBankReconciliation(array_merge([
'prepared_by_cashier_id' => $cashier->id,
'prepared_at' => now()->subDays(3),
'reviewed_by_accountant_id' => $accountant->id,
'reviewed_at' => now()->subDays(2),
'approved_by_manager_id' => $manager->id,
'approved_at' => now()->subDay(),
'reconciliation_status' => 'completed',
], $attributes));
}
/**
* Create a budget
*/
protected function createBudget(array $attributes = []): Budget
{
return Budget::factory()->create($attributes);
}
/**
* Create a budget with items
*/
protected function createBudgetWithItems(int $itemCount = 3, array $budgetAttributes = []): Budget
{
$budget = $this->createBudget($budgetAttributes);
for ($i = 0; $i < $itemCount; $i++) {
$account = ChartOfAccount::factory()->create();
BudgetItem::factory()->create([
'budget_id' => $budget->id,
'chart_of_account_id' => $account->id,
'budgeted_amount' => rand(10000, 50000),
]);
}
return $budget->fresh('items');
}
/**
* Create a fake attachment file
*/
protected function createFakeAttachment(string $name = 'document.pdf'): UploadedFile
{
return UploadedFile::fake()->create($name, 100, 'application/pdf');
}
/**
* Get valid finance document data
*/
protected function getValidFinanceDocumentData(array $overrides = []): array
{
return array_merge([
'title' => 'Test Finance Document',
'description' => 'Test description',
'amount' => 10000,
'request_type' => FinanceDocument::REQUEST_TYPE_EXPENSE_REIMBURSEMENT,
'payee_name' => 'Test Payee',
'notes' => 'Test notes',
], $overrides);
}
}