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>
274 lines
9.0 KiB
PHP
274 lines
9.0 KiB
PHP
<?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);
|
|
}
|
|
}
|