770 lines
31 KiB
PHP
770 lines
31 KiB
PHP
<?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([
|
||
RoleSeeder::class,
|
||
PaymentVerificationRolesSeeder::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::create([
|
||
'name' => 'Admin User',
|
||
'email' => 'admin@test.com',
|
||
'password' => Hash::make('password'),
|
||
'is_admin' => true,
|
||
]);
|
||
$admin->assignRole('admin');
|
||
$users['admin'] = $admin;
|
||
|
||
// 2. Payment Cashier
|
||
$cashier = User::create([
|
||
'name' => 'Cashier User',
|
||
'email' => 'cashier@test.com',
|
||
'password' => Hash::make('password'),
|
||
'is_admin' => true,
|
||
]);
|
||
$cashier->assignRole('payment_cashier');
|
||
$users['cashier'] = $cashier;
|
||
|
||
// 3. Payment Accountant
|
||
$accountant = User::create([
|
||
'name' => 'Accountant User',
|
||
'email' => 'accountant@test.com',
|
||
'password' => Hash::make('password'),
|
||
'is_admin' => true,
|
||
]);
|
||
$accountant->assignRole('payment_accountant');
|
||
$users['accountant'] = $accountant;
|
||
|
||
// 4. Payment Chair
|
||
$chair = User::create([
|
||
'name' => 'Chair User',
|
||
'email' => 'chair@test.com',
|
||
'password' => Hash::make('password'),
|
||
'is_admin' => true,
|
||
]);
|
||
$chair->assignRole('payment_chair');
|
||
$users['chair'] = $chair;
|
||
|
||
// 5. Membership Manager
|
||
$manager = User::create([
|
||
'name' => 'Membership Manager',
|
||
'email' => 'manager@test.com',
|
||
'password' => Hash::make('password'),
|
||
'is_admin' => true,
|
||
]);
|
||
$manager->assignRole('membership_manager');
|
||
$users['manager'] = $manager;
|
||
|
||
// 6. Regular Member User
|
||
$member = User::create([
|
||
'name' => 'Regular Member',
|
||
'email' => 'member@test.com',
|
||
'password' => Hash::make('password'),
|
||
'is_admin' => false,
|
||
]);
|
||
$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::create([
|
||
'user_id' => $i === 0 ? $users['member']->id : null,
|
||
'full_name' => "待審核會員 {$counter}",
|
||
'email' => "pending{$counter}@test.com",
|
||
'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::create([
|
||
'user_id' => null,
|
||
'full_name' => "活躍會員 {$counter}",
|
||
'email' => "active{$counter}@test.com",
|
||
'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::create([
|
||
'user_id' => null,
|
||
'full_name' => "過期會員 {$counter}",
|
||
'email' => "expired{$counter}@test.com",
|
||
'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::create([
|
||
'user_id' => null,
|
||
'full_name' => "停權會員 {$counter}",
|
||
'email' => "suspended{$counter}@test.com",
|
||
'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::create([
|
||
'user_id' => null,
|
||
'full_name' => "新申請會員 {$counter}",
|
||
'email' => "newmember{$counter}@test.com",
|
||
'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,
|
||
MembershipPayment::METHOD_CHECK,
|
||
];
|
||
|
||
// 10 Pending Payments
|
||
for ($i = 0; $i < 10; $i++) {
|
||
$payments[] = MembershipPayment::create([
|
||
'member_id' => $members[$i]->id,
|
||
'amount' => 1000,
|
||
'paid_at' => now()->subDays(rand(1, 10)),
|
||
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
|
||
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT),
|
||
'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::create([
|
||
'member_id' => $members[$i % count($members)]->id,
|
||
'amount' => 1000,
|
||
'paid_at' => now()->subDays(rand(5, 15)),
|
||
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
|
||
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT),
|
||
'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::create([
|
||
'member_id' => $members[$i % count($members)]->id,
|
||
'amount' => 1000,
|
||
'paid_at' => now()->subDays(rand(15, 25)),
|
||
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
|
||
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT),
|
||
'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::create([
|
||
'member_id' => $members[$i % count($members)]->id,
|
||
'amount' => 1000,
|
||
'paid_at' => now()->subDays(rand(25, 35)),
|
||
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
|
||
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT),
|
||
'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::create([
|
||
'member_id' => $members[$i % count($members)]->id,
|
||
'amount' => 1000,
|
||
'paid_at' => now()->subDays(rand(5, 10)),
|
||
'payment_method' => $paymentMethods[array_rand($paymentMethods)],
|
||
'reference' => 'REF-' . str_pad($i + 1, 5, '0', STR_PAD_LEFT),
|
||
'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@test.com', 'password', 'All permissions'],
|
||
['Cashier', 'cashier@test.com', 'password', 'Tier 1 payment verification'],
|
||
['Accountant', 'accountant@test.com', 'password', 'Tier 2 payment verification'],
|
||
['Chair', 'chair@test.com', 'password', 'Tier 3 payment verification'],
|
||
['Manager', 'manager@test.com', 'password', 'Membership activation'],
|
||
['Member', '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('');
|
||
}
|
||
}
|