Files
usher-manage-stack/database/seeders/TestDataSeeder.php
Gbanyan 642b879dd4 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>
2025-12-01 09:56:01 +08:00

794 lines
32 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Database\Seeders;
use App\Models\Budget;
use App\Models\BudgetItem;
use App\Models\ChartOfAccount;
use App\Models\FinanceDocument;
use App\Models\Issue;
use App\Models\IssueComment;
use App\Models\IssueLabel;
use App\Models\IssueTimeLog;
use App\Models\Member;
use App\Models\MembershipPayment;
use App\Models\Transaction;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Hash;
class TestDataSeeder extends Seeder
{
/**
* Run the database seeds.
*
* Creates comprehensive test data for manual and automated testing:
* - 5 test users with different roles
* - 20 members in various states (pending, active, expired, suspended)
* - 30 payments at different approval stages
* - 15 issues with various statuses
* - 5 budgets with items
* - 10 finance documents
* - Sample transactions
*/
public function run(): void
{
$this->command->info('🌱 Starting Test Data Seeding...');
// Ensure required seeders have run
$this->call([
FinancialWorkflowPermissionsSeeder::class,
ChartOfAccountSeeder::class,
IssueLabelSeeder::class,
]);
// Create test users with different roles
$users = $this->createTestUsers();
$this->command->info('✅ Created 6 test users with different roles');
// Create members in various states
$members = $this->createTestMembers($users);
$this->command->info('✅ Created 20 members in various membership states');
// Create payments at different approval stages
$payments = $this->createTestPayments($members, $users);
$this->command->info('✅ Created 30 membership payments at different approval stages');
// Create issues with various statuses
$issues = $this->createTestIssues($users, $members);
$this->command->info('✅ Created 15 issues with various statuses and relationships');
// Create budgets with items
$budgets = $this->createTestBudgets($users);
$this->command->info('✅ Created 5 budgets with budget items in different states');
// Create finance documents
$financeDocuments = $this->createTestFinanceDocuments($users);
$this->command->info('✅ Created 10 finance documents');
// Create sample transactions
$transactions = $this->createTestTransactions($users);
$this->command->info('✅ Created sample transactions');
$this->command->info('');
$this->command->info('🎉 Test Data Seeding Complete!');
$this->command->info('');
$this->displayTestAccounts($users);
}
/**
* Create test users with different roles
*/
private function createTestUsers(): array
{
$users = [];
// 1. Super Admin
$admin = User::firstOrCreate(
['email' => 'admin@test.com'],
[
'name' => 'Admin User',
'password' => Hash::make('password'),
]
);
$admin->assignRole('admin');
$users['admin'] = $admin;
// 2. Finance Cashier (整合原 payment_cashier)
$cashier = User::firstOrCreate(
['email' => 'cashier@test.com'],
[
'name' => 'Cashier User',
'password' => Hash::make('password'),
]
);
$cashier->assignRole('finance_cashier');
$users['cashier'] = $cashier;
// 3. Finance Accountant (整合原 payment_accountant)
$accountant = User::firstOrCreate(
['email' => 'accountant@test.com'],
[
'name' => 'Accountant User',
'password' => Hash::make('password'),
]
);
$accountant->assignRole('finance_accountant');
$users['accountant'] = $accountant;
// 4. Finance Chair (整合原 payment_chair)
$chair = User::firstOrCreate(
['email' => 'chair@test.com'],
[
'name' => 'Chair User',
'password' => Hash::make('password'),
]
);
$chair->assignRole('finance_chair');
$users['chair'] = $chair;
// 5. Membership Manager
$manager = User::firstOrCreate(
['email' => 'manager@test.com'],
[
'name' => 'Membership Manager',
'password' => Hash::make('password'),
]
);
$manager->assignRole('membership_manager');
$users['manager'] = $manager;
// 6. Regular Member User
$member = User::firstOrCreate(
['email' => 'member@test.com'],
[
'name' => 'Regular Member',
'password' => Hash::make('password'),
]
);
$users['member'] = $member;
return $users;
}
/**
* Create test members in various states
*/
private function createTestMembers(array $users): array
{
$members = [];
$taiwanCities = ['台北市', '新北市', '台中市', '台南市', '高雄市', '桃園市'];
$counter = 1;
// 5 Pending Members
for ($i = 0; $i < 5; $i++) {
$members[] = Member::firstOrCreate(
['email' => "pending{$counter}@test.com"],
[
'user_id' => $i === 0 ? $users['member']->id : null,
'full_name' => "待審核會員 {$counter}",
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
'address_line_1' => "測試地址 {$counter}",
'city' => $taiwanCities[array_rand($taiwanCities)],
'postal_code' => '100',
'membership_status' => Member::STATUS_PENDING,
'membership_type' => Member::TYPE_REGULAR,
'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
]
);
$counter++;
}
// 8 Active Members
for ($i = 0; $i < 8; $i++) {
$startDate = now()->subMonths(rand(1, 6));
$members[] = Member::firstOrCreate(
['email' => "active{$counter}@test.com"],
[
'user_id' => null,
'full_name' => "活躍會員 {$counter}",
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
'address_line_1' => "測試地址 {$counter}",
'city' => $taiwanCities[array_rand($taiwanCities)],
'postal_code' => '100',
'membership_status' => Member::STATUS_ACTIVE,
'membership_type' => $i < 6 ? Member::TYPE_REGULAR : ($i === 6 ? Member::TYPE_HONORARY : Member::TYPE_STUDENT),
'membership_started_at' => $startDate,
'membership_expires_at' => $startDate->copy()->addYear(),
'emergency_contact_name' => "緊急聯絡人 {$counter}",
'emergency_contact_phone' => '02-12345678',
'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
]
);
$counter++;
}
// 3 Expired Members
for ($i = 0; $i < 3; $i++) {
$startDate = now()->subYears(2);
$members[] = Member::firstOrCreate(
['email' => "expired{$counter}@test.com"],
[
'user_id' => null,
'full_name' => "過期會員 {$counter}",
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
'address_line_1' => "測試地址 {$counter}",
'city' => $taiwanCities[array_rand($taiwanCities)],
'postal_code' => '100',
'membership_status' => Member::STATUS_EXPIRED,
'membership_type' => Member::TYPE_REGULAR,
'membership_started_at' => $startDate,
'membership_expires_at' => $startDate->copy()->addYear(),
'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
]
);
$counter++;
}
// 2 Suspended Members
for ($i = 0; $i < 2; $i++) {
$members[] = Member::firstOrCreate(
['email' => "suspended{$counter}@test.com"],
[
'user_id' => null,
'full_name' => "停權會員 {$counter}",
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
'address_line_1' => "測試地址 {$counter}",
'city' => $taiwanCities[array_rand($taiwanCities)],
'postal_code' => '100',
'membership_status' => Member::STATUS_SUSPENDED,
'membership_type' => Member::TYPE_REGULAR,
'national_id_encrypted' => Crypt::encryptString('A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
'national_id_hash' => hash('sha256', 'A' . str_pad($counter, 9, '0', STR_PAD_LEFT)),
]
);
$counter++;
}
// 2 Additional Pending Members (total 20)
for ($i = 0; $i < 2; $i++) {
$members[] = Member::firstOrCreate(
['email' => "newmember{$counter}@test.com"],
[
'user_id' => null,
'full_name' => "新申請會員 {$counter}",
'phone' => '09' . str_pad($counter, 8, '0', STR_PAD_LEFT),
'address_line_1' => "測試地址 {$counter}",
'city' => $taiwanCities[array_rand($taiwanCities)],
'postal_code' => '100',
'membership_status' => Member::STATUS_PENDING,
'membership_type' => Member::TYPE_REGULAR,
]
);
$counter++;
}
return $members;
}
/**
* Create test membership payments at different approval stages
*/
private function createTestPayments(array $members, array $users): array
{
$payments = [];
$paymentMethods = [
MembershipPayment::METHOD_BANK_TRANSFER,
MembershipPayment::METHOD_CASH,
];
// 10 Pending Payments
for ($i = 0; $i < 10; $i++) {
$payments[] = MembershipPayment::firstOrCreate(
['reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT)],
[
'member_id' => $members[$i]->id,
'amount' => 1000,
'paid_at' => now()->subDays(rand(1, 10)),
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
'status' => MembershipPayment::STATUS_PENDING,
'notes' => '待審核的繳費記錄',
]
);
}
// 8 Approved by Cashier
for ($i = 10; $i < 18; $i++) {
$payments[] = MembershipPayment::firstOrCreate(
['reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT)],
[
'member_id' => $members[$i % count($members)]->id,
'amount' => 1000,
'paid_at' => now()->subDays(rand(5, 15)),
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
'status' => MembershipPayment::STATUS_APPROVED_CASHIER,
'verified_by_cashier_id' => $users['cashier']->id,
'cashier_verified_at' => now()->subDays(rand(3, 12)),
'cashier_notes' => '收據已核對,金額無誤',
'notes' => '已通過出納審核',
]
);
}
// 6 Approved by Accountant
for ($i = 18; $i < 24; $i++) {
$cashierVerifiedAt = now()->subDays(rand(10, 20));
$accountantVerifiedAt = $cashierVerifiedAt->copy()->addDays(rand(1, 3));
$payments[] = MembershipPayment::firstOrCreate(
['reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT)],
[
'member_id' => $members[$i % count($members)]->id,
'amount' => 1000,
'paid_at' => now()->subDays(rand(15, 25)),
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
'status' => MembershipPayment::STATUS_APPROVED_ACCOUNTANT,
'verified_by_cashier_id' => $users['cashier']->id,
'cashier_verified_at' => $cashierVerifiedAt,
'cashier_notes' => '收據已核對,金額無誤',
'verified_by_accountant_id' => $users['accountant']->id,
'accountant_verified_at' => $accountantVerifiedAt,
'accountant_notes' => '帳務核對完成',
'notes' => '已通過會計審核',
]
);
}
// 4 Fully Approved (Chair approved - member activated)
for ($i = 24; $i < 28; $i++) {
$cashierVerifiedAt = now()->subDays(rand(20, 30));
$accountantVerifiedAt = $cashierVerifiedAt->copy()->addDays(rand(1, 3));
$chairVerifiedAt = $accountantVerifiedAt->copy()->addDays(rand(1, 2));
$payments[] = MembershipPayment::firstOrCreate(
['reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT)],
[
'member_id' => $members[$i % count($members)]->id,
'amount' => 1000,
'paid_at' => now()->subDays(rand(25, 35)),
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
'status' => MembershipPayment::STATUS_APPROVED_CHAIR,
'verified_by_cashier_id' => $users['cashier']->id,
'cashier_verified_at' => $cashierVerifiedAt,
'cashier_notes' => '收據已核對,金額無誤',
'verified_by_accountant_id' => $users['accountant']->id,
'accountant_verified_at' => $accountantVerifiedAt,
'accountant_notes' => '帳務核對完成',
'verified_by_chair_id' => $users['chair']->id,
'chair_verified_at' => $chairVerifiedAt,
'chair_notes' => '最終批准',
'notes' => '已完成三階段審核',
]
);
}
// 2 Rejected Payments
for ($i = 28; $i < 30; $i++) {
$payments[] = MembershipPayment::firstOrCreate(
['reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT)],
[
'member_id' => $members[$i % count($members)]->id,
'amount' => 1000,
'paid_at' => now()->subDays(rand(5, 10)),
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
'receipt_path' => 'receipts/test-receipt-' . ($i + 1) . '.pdf',
'status' => MembershipPayment::STATUS_REJECTED,
'rejected_by_user_id' => $users['cashier']->id,
'rejected_at' => now()->subDays(rand(3, 8)),
'rejection_reason' => $i === 28 ? '收據影像不清晰,無法辨識' : '金額與收據不符',
'notes' => '已退回',
]
);
}
return $payments;
}
/**
* Create test issues with various statuses
*/
private function createTestIssues(array $users, array $members): array
{
$issues = [];
$labels = IssueLabel::all();
// 5 New/Open Issues
for ($i = 0; $i < 5; $i++) {
$issue = Issue::create([
'title' => "新任務:測試項目 " . ($i + 1),
'description' => "這是一個新的測試任務,用於系統測試。\n\n## 任務說明\n- 測試項目 A\n- 測試項目 B\n- 測試項目 C",
'issue_type' => [Issue::TYPE_WORK_ITEM, Issue::TYPE_PROJECT_TASK][array_rand([0, 1])],
'status' => Issue::STATUS_NEW,
'priority' => [Issue::PRIORITY_LOW, Issue::PRIORITY_MEDIUM, Issue::PRIORITY_HIGH][array_rand([0, 1, 2])],
'created_by_user_id' => $users['admin']->id,
'due_date' => now()->addDays(rand(7, 30)),
'estimated_hours' => rand(2, 16),
]);
// Add labels
if ($labels->count() > 0) {
$issue->labels()->attach($labels->random(rand(1, min(3, $labels->count()))));
}
$issues[] = $issue;
}
// 4 In Progress Issues
for ($i = 5; $i < 9; $i++) {
$issue = Issue::create([
'title' => "進行中:開發任務 " . ($i + 1),
'description' => "這是一個進行中的開發任務。\n\n## 進度\n- [x] 需求分析\n- [x] 設計\n- [ ] 實作\n- [ ] 測試",
'issue_type' => Issue::TYPE_WORK_ITEM,
'status' => Issue::STATUS_IN_PROGRESS,
'priority' => [Issue::PRIORITY_MEDIUM, Issue::PRIORITY_HIGH][array_rand([0, 1])],
'created_by_user_id' => $users['admin']->id,
'assigned_to_user_id' => $users['member']->id,
'due_date' => now()->addDays(rand(3, 14)),
'estimated_hours' => rand(4, 20),
]);
// Add time logs
IssueTimeLog::create([
'issue_id' => $issue->id,
'user_id' => $users['member']->id,
'hours' => rand(1, 5),
'description' => '開發進度更新',
'logged_at' => now()->subDays(rand(1, 3)),
]);
// Add comments
IssueComment::create([
'issue_id' => $issue->id,
'user_id' => $users['admin']->id,
'comment' => '請加快進度,謝謝!',
'created_at' => now()->subDays(rand(1, 2)),
]);
if ($labels->count() > 0) {
$issue->labels()->attach($labels->random(rand(1, min(2, $labels->count()))));
}
$issues[] = $issue;
}
// 3 Resolved Issues (in review)
for ($i = 9; $i < 12; $i++) {
$issue = Issue::create([
'title' => "已完成:維護項目 " . ($i + 1),
'description' => "維護任務已完成,等待審核。",
'issue_type' => Issue::TYPE_MAINTENANCE,
'status' => Issue::STATUS_REVIEW,
'priority' => Issue::PRIORITY_MEDIUM,
'created_by_user_id' => $users['admin']->id,
'assigned_to_user_id' => $users['member']->id,
'reviewer_id' => $users['manager']->id,
'due_date' => now()->subDays(rand(1, 5)),
'estimated_hours' => rand(2, 8),
]);
// Add completed time logs
IssueTimeLog::create([
'issue_id' => $issue->id,
'user_id' => $users['member']->id,
'hours' => rand(2, 6),
'description' => '任務完成',
'logged_at' => now()->subDays(rand(1, 3)),
]);
IssueComment::create([
'issue_id' => $issue->id,
'user_id' => $users['member']->id,
'comment' => '任務已完成,請審核。',
'created_at' => now()->subDays(1),
]);
if ($labels->count() > 0) {
$issue->labels()->attach($labels->random(1));
}
$issues[] = $issue;
}
// 2 Closed Issues
for ($i = 12; $i < 14; $i++) {
$closedAt = now()->subDays(rand(7, 30));
$issue = Issue::create([
'title' => "已結案:專案 " . ($i + 1),
'description' => "專案已完成並結案。",
'issue_type' => Issue::TYPE_PROJECT_TASK,
'status' => Issue::STATUS_CLOSED,
'priority' => Issue::PRIORITY_HIGH,
'created_by_user_id' => $users['admin']->id,
'assigned_to_user_id' => $users['member']->id,
'due_date' => $closedAt->copy()->subDays(rand(1, 5)),
'closed_at' => $closedAt,
'estimated_hours' => rand(8, 24),
]);
IssueTimeLog::create([
'issue_id' => $issue->id,
'user_id' => $users['member']->id,
'hours' => rand(8, 20),
'description' => '專案完成',
'logged_at' => $closedAt->copy()->subDays(1),
]);
IssueComment::create([
'issue_id' => $issue->id,
'user_id' => $users['admin']->id,
'comment' => '專案驗收通過,結案。',
'created_at' => $closedAt,
]);
if ($labels->count() > 0) {
$issue->labels()->attach($labels->random(rand(1, min(2, $labels->count()))));
}
$issues[] = $issue;
}
// 1 Member Request
$issue = Issue::create([
'title' => "會員需求:更新會員資料",
'description' => "會員要求更新個人資料。",
'issue_type' => Issue::TYPE_MEMBER_REQUEST,
'status' => Issue::STATUS_ASSIGNED,
'priority' => Issue::PRIORITY_MEDIUM,
'created_by_user_id' => $users['admin']->id,
'assigned_to_user_id' => $users['manager']->id,
'member_id' => $members[0]->id,
'due_date' => now()->addDays(3),
'estimated_hours' => 1,
]);
if ($labels->count() > 0) {
$issue->labels()->attach($labels->random(1));
}
$issues[] = $issue;
return $issues;
}
/**
* Create test budgets with items
*/
private function createTestBudgets(array $users): array
{
$budgets = [];
$accounts = ChartOfAccount::all();
if ($accounts->isEmpty()) {
$this->command->warn('⚠️ No Chart of Accounts found. Skipping budget creation.');
return $budgets;
}
$incomeAccounts = $accounts->where('account_type', ChartOfAccount::TYPE_INCOME);
$expenseAccounts = $accounts->where('account_type', ChartOfAccount::TYPE_EXPENSE);
// 1. Draft Budget
$budget = Budget::create([
'name' => '2025年度預算草案',
'fiscal_year' => 2025,
'status' => Budget::STATUS_DRAFT,
'created_by_user_id' => $users['admin']->id,
'notes' => '年度預算初稿,待提交',
]);
$this->createBudgetItems($budget, $incomeAccounts, $expenseAccounts, 50000, 40000);
$budgets[] = $budget;
// 2. Submitted Budget
$budget = Budget::create([
'name' => '2024下半年預算',
'fiscal_year' => 2024,
'status' => Budget::STATUS_SUBMITTED,
'created_by_user_id' => $users['admin']->id,
'submitted_at' => now()->subDays(5),
'notes' => '已提交,等待審核',
]);
$this->createBudgetItems($budget, $incomeAccounts, $expenseAccounts, 60000, 50000);
$budgets[] = $budget;
// 3. Approved Budget
$budget = Budget::create([
'name' => '2024上半年預算',
'fiscal_year' => 2024,
'status' => Budget::STATUS_APPROVED,
'created_by_user_id' => $users['admin']->id,
'submitted_at' => now()->subDays(60),
'approved_by_user_id' => $users['chair']->id,
'approved_at' => now()->subDays(55),
'notes' => '已核准',
]);
$this->createBudgetItems($budget, $incomeAccounts, $expenseAccounts, 70000, 60000);
$budgets[] = $budget;
// 4. Active Budget
$budget = Budget::create([
'name' => '2024年度預算',
'fiscal_year' => 2024,
'status' => Budget::STATUS_ACTIVE,
'created_by_user_id' => $users['admin']->id,
'submitted_at' => now()->subMonths(11),
'approved_by_user_id' => $users['chair']->id,
'approved_at' => now()->subMonths(10),
'activated_at' => now()->subMonths(10),
'notes' => '執行中的年度預算',
]);
$this->createBudgetItems($budget, $incomeAccounts, $expenseAccounts, 100000, 80000, true);
$budgets[] = $budget;
// 5. Closed Budget
$budget = Budget::create([
'name' => '2023年度預算',
'fiscal_year' => 2023,
'status' => Budget::STATUS_CLOSED,
'created_by_user_id' => $users['admin']->id,
'submitted_at' => now()->subMonths(23),
'approved_by_user_id' => $users['chair']->id,
'approved_at' => now()->subMonths(22),
'activated_at' => now()->subMonths(22),
'closed_at' => now()->subMonths(10),
'notes' => '已結案的年度預算',
]);
$this->createBudgetItems($budget, $incomeAccounts, $expenseAccounts, 90000, 75000, true);
$budgets[] = $budget;
return $budgets;
}
/**
* Create budget items for a budget
*/
private function createBudgetItems(
Budget $budget,
$incomeAccounts,
$expenseAccounts,
int $totalIncome,
int $totalExpense,
bool $withActuals = false
): void {
// Create income items
if ($incomeAccounts->count() > 0) {
$itemCount = min(3, $incomeAccounts->count());
$accounts = $incomeAccounts->random($itemCount);
foreach ($accounts as $index => $account) {
$budgetedAmount = (int)($totalIncome / $itemCount);
$actualAmount = $withActuals ? (int)($budgetedAmount * rand(80, 120) / 100) : 0;
BudgetItem::create([
'budget_id' => $budget->id,
'chart_of_account_id' => $account->id,
'budgeted_amount' => $budgetedAmount,
'actual_amount' => $actualAmount,
'notes' => '預算項目',
]);
}
}
// Create expense items
if ($expenseAccounts->count() > 0) {
$itemCount = min(5, $expenseAccounts->count());
$accounts = $expenseAccounts->random($itemCount);
foreach ($accounts as $index => $account) {
$budgetedAmount = (int)($totalExpense / $itemCount);
$actualAmount = $withActuals ? (int)($budgetedAmount * rand(70, 110) / 100) : 0;
BudgetItem::create([
'budget_id' => $budget->id,
'chart_of_account_id' => $account->id,
'budgeted_amount' => $budgetedAmount,
'actual_amount' => $actualAmount,
'notes' => '支出預算項目',
]);
}
}
}
/**
* Create test finance documents
*/
private function createTestFinanceDocuments(array $users): array
{
$documents = [];
$documentTypes = ['invoice', 'receipt', 'contract', 'report'];
$statuses = ['pending', 'approved', 'rejected'];
for ($i = 1; $i <= 10; $i++) {
$documents[] = FinanceDocument::create([
'document_number' => 'FIN-2024-' . str_pad($i, 4, '0', STR_PAD_LEFT),
'title' => "財務文件 {$i}",
'document_type' => $documentTypes[array_rand($documentTypes)],
'amount' => rand(1000, 50000),
'document_date' => now()->subDays(rand(1, 90)),
'status' => $statuses[array_rand($statuses)],
'uploaded_by_user_id' => $users['admin']->id,
'file_path' => "finance-documents/test-doc-{$i}.pdf",
'notes' => '測試財務文件',
]);
}
return $documents;
}
/**
* Create sample transactions
*/
private function createTestTransactions(array $users): array
{
$transactions = [];
$accounts = ChartOfAccount::all();
if ($accounts->isEmpty()) {
$this->command->warn('⚠️ No Chart of Accounts found. Skipping transaction creation.');
return $transactions;
}
// Create 20 sample transactions
for ($i = 1; $i <= 20; $i++) {
$account = $accounts->random();
$isDebit = $account->account_type === ChartOfAccount::TYPE_EXPENSE ||
$account->account_type === ChartOfAccount::TYPE_ASSET;
$transactions[] = Transaction::create([
'transaction_date' => now()->subDays(rand(1, 60)),
'chart_of_account_id' => $account->id,
'description' => "測試交易 {$i}" . $account->account_name,
'debit_amount' => $isDebit ? rand(500, 10000) : 0,
'credit_amount' => !$isDebit ? rand(500, 10000) : 0,
'created_by_user_id' => $users['accountant']->id,
'reference' => 'TXN-' . str_pad($i, 5, '0', STR_PAD_LEFT),
'notes' => '系統測試交易',
]);
}
return $transactions;
}
/**
* Display test account information
*/
private function displayTestAccounts(array $users): void
{
$this->command->info('📋 Test User Accounts:');
$this->command->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->command->table(
['Role', 'Email', 'Password', 'Permissions'],
[
['Admin (admin)', 'admin@test.com', 'password', 'All permissions'],
['Finance Cashier (finance_cashier)', 'cashier@test.com', 'password', 'Payment + Finance cashier'],
['Finance Accountant (finance_accountant)', 'accountant@test.com', 'password', 'Payment + Finance accountant'],
['Finance Chair (finance_chair)', 'chair@test.com', 'password', 'Payment + Finance chair'],
['Membership Manager (membership_manager)', 'manager@test.com', 'password', 'Membership activation'],
['Member (no role)', 'member@test.com', 'password', 'Member dashboard access'],
]
);
$this->command->info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
$this->command->info('');
$this->command->info('🎯 Test Data Summary:');
$this->command->info(' • 20 Members (5 pending, 8 active, 3 expired, 2 suspended, 2 new)');
$this->command->info(' • 30 Payments (10 pending, 8 cashier-approved, 6 accountant-approved, 4 fully-approved, 2 rejected)');
$this->command->info(' • 15 Issues (5 new, 4 in-progress, 3 in-review, 2 closed, 1 member-request)');
$this->command->info(' • 5 Budgets (draft, submitted, approved, active, closed)');
$this->command->info(' • 10 Finance Documents');
$this->command->info(' • 20 Sample Transactions');
$this->command->info('');
}
}