Initial commit

This commit is contained in:
2025-11-20 23:21:05 +08:00
commit 13bc6db529
378 changed files with 54527 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class AdvancedPermissionsSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Create advanced document feature permissions
$permissions = [
[
'name' => 'manage_system_settings',
'description' => 'Access and modify system settings pages'
],
[
'name' => 'use_bulk_import',
'description' => 'Use document bulk import feature'
],
[
'name' => 'use_qr_codes',
'description' => 'Generate QR codes for documents'
],
[
'name' => 'view_document_statistics',
'description' => 'Access document statistics dashboard'
],
[
'name' => 'manage_document_tags',
'description' => 'Create, edit, and delete document tags'
],
[
'name' => 'manage_document_expiration',
'description' => 'Set expiration dates and configure auto-archive rules'
],
[
'name' => 'export_documents',
'description' => 'Export document lists and reports'
],
];
foreach ($permissions as $permissionData) {
Permission::firstOrCreate(
['name' => $permissionData['name']],
['guard_name' => 'web']
);
}
// Assign all advanced permissions to 'admin' role
$adminRole = Role::where('name', 'admin')->first();
if ($adminRole) {
foreach ($permissions as $permissionData) {
$permission = Permission::where('name', $permissionData['name'])->first();
if ($permission && !$adminRole->hasPermissionTo($permission)) {
$adminRole->givePermissionTo($permission);
}
}
$this->command->info('Advanced permissions assigned to admin role');
}
// Optionally assign some permissions to 'staff' role
$staffRole = Role::where('name', 'staff')->first();
if ($staffRole) {
$staffPermissions = [
'use_qr_codes',
'view_document_statistics',
'export_documents',
];
foreach ($staffPermissions as $permissionName) {
$permission = Permission::where('name', $permissionName)->first();
if ($permission && !$staffRole->hasPermissionTo($permission)) {
$staffRole->givePermissionTo($permission);
}
}
$this->command->info('Selected permissions assigned to staff role');
}
$this->command->info('Advanced permissions seeded successfully');
}
}

View File

@@ -0,0 +1,490 @@
<?php
namespace Database\Seeders;
use App\Models\ChartOfAccount;
use Illuminate\Database\Seeder;
class ChartOfAccountSeeder extends Seeder
{
/**
* Run the database seeds.
*
* Taiwan nonprofit standard chart of accounts (會計科目表)
*/
public function run(): void
{
$accounts = [
// Assets (資產) - 1xxx
[
'account_code' => '1101',
'account_name_zh' => '現金',
'account_name_en' => 'Cash',
'account_type' => 'asset',
'category' => '流動資產',
'display_order' => 10,
],
[
'account_code' => '1102',
'account_name_zh' => '零用金',
'account_name_en' => 'Petty Cash',
'account_type' => 'asset',
'category' => '流動資產',
'display_order' => 20,
],
[
'account_code' => '1201',
'account_name_zh' => '銀行存款',
'account_name_en' => 'Bank Deposits',
'account_type' => 'asset',
'category' => '流動資產',
'display_order' => 30,
],
[
'account_code' => '1301',
'account_name_zh' => '應收帳款',
'account_name_en' => 'Accounts Receivable',
'account_type' => 'asset',
'category' => '流動資產',
'display_order' => 40,
],
[
'account_code' => '1302',
'account_name_zh' => '其他應收款',
'account_name_en' => 'Other Receivables',
'account_type' => 'asset',
'category' => '流動資產',
'display_order' => 50,
],
[
'account_code' => '1401',
'account_name_zh' => '土地',
'account_name_en' => 'Land',
'account_type' => 'asset',
'category' => '固定資產',
'display_order' => 60,
],
[
'account_code' => '1402',
'account_name_zh' => '房屋及建築',
'account_name_en' => 'Buildings',
'account_type' => 'asset',
'category' => '固定資產',
'display_order' => 70,
],
[
'account_code' => '1403',
'account_name_zh' => '機器設備',
'account_name_en' => 'Machinery & Equipment',
'account_type' => 'asset',
'category' => '固定資產',
'display_order' => 80,
],
[
'account_code' => '1404',
'account_name_zh' => '辦公設備',
'account_name_en' => 'Office Equipment',
'account_type' => 'asset',
'category' => '固定資產',
'display_order' => 90,
],
[
'account_code' => '1405',
'account_name_zh' => '電腦設備',
'account_name_en' => 'Computer Equipment',
'account_type' => 'asset',
'category' => '固定資產',
'display_order' => 100,
],
[
'account_code' => '1501',
'account_name_zh' => '存出保證金',
'account_name_en' => 'Guarantee Deposits Paid',
'account_type' => 'asset',
'category' => '其他資產',
'display_order' => 110,
],
// Liabilities (負債) - 2xxx
[
'account_code' => '2101',
'account_name_zh' => '應付帳款',
'account_name_en' => 'Accounts Payable',
'account_type' => 'liability',
'category' => '流動負債',
'display_order' => 200,
],
[
'account_code' => '2102',
'account_name_zh' => '應付薪資',
'account_name_en' => 'Salaries Payable',
'account_type' => 'liability',
'category' => '流動負債',
'display_order' => 210,
],
[
'account_code' => '2103',
'account_name_zh' => '應付費用',
'account_name_en' => 'Accrued Expenses',
'account_type' => 'liability',
'category' => '流動負債',
'display_order' => 220,
],
[
'account_code' => '2104',
'account_name_zh' => '代收款',
'account_name_en' => 'Collections for Others',
'account_type' => 'liability',
'category' => '流動負債',
'display_order' => 230,
],
[
'account_code' => '2201',
'account_name_zh' => '長期借款',
'account_name_en' => 'Long-term Loans',
'account_type' => 'liability',
'category' => '長期負債',
'display_order' => 240,
],
// Net Assets/Fund Balance (淨資產/基金) - 3xxx
[
'account_code' => '3101',
'account_name_zh' => '累積餘絀',
'account_name_en' => 'Accumulated Surplus/Deficit',
'account_type' => 'net_asset',
'category' => '淨資產',
'display_order' => 300,
],
[
'account_code' => '3102',
'account_name_zh' => '本期餘絀',
'account_name_en' => 'Current Period Surplus/Deficit',
'account_type' => 'net_asset',
'category' => '淨資產',
'display_order' => 310,
],
[
'account_code' => '3201',
'account_name_zh' => '基金',
'account_name_en' => 'Fund Balance',
'account_type' => 'net_asset',
'category' => '基金',
'display_order' => 320,
],
// Income (收入) - 4xxx
[
'account_code' => '4101',
'account_name_zh' => '會費收入',
'account_name_en' => 'Membership Dues',
'account_type' => 'income',
'category' => '會費收入',
'display_order' => 400,
'description' => '會員繳交之常年會費',
],
[
'account_code' => '4102',
'account_name_zh' => '入會費收入',
'account_name_en' => 'Entrance Fees',
'account_type' => 'income',
'category' => '會費收入',
'display_order' => 410,
'description' => '新會員入會費',
],
[
'account_code' => '4201',
'account_name_zh' => '捐贈收入',
'account_name_en' => 'Donation Income',
'account_type' => 'income',
'category' => '捐贈收入',
'display_order' => 420,
'description' => '個人或團體捐贈',
],
[
'account_code' => '4202',
'account_name_zh' => '企業捐贈收入',
'account_name_en' => 'Corporate Donations',
'account_type' => 'income',
'category' => '捐贈收入',
'display_order' => 430,
'description' => '企業捐贈',
],
[
'account_code' => '4301',
'account_name_zh' => '政府補助收入',
'account_name_en' => 'Government Grants',
'account_type' => 'income',
'category' => '補助收入',
'display_order' => 440,
'description' => '政府機關補助款',
],
[
'account_code' => '4302',
'account_name_zh' => '計畫補助收入',
'account_name_en' => 'Project Grants',
'account_type' => 'income',
'category' => '補助收入',
'display_order' => 450,
'description' => '專案計畫補助',
],
[
'account_code' => '4401',
'account_name_zh' => '利息收入',
'account_name_en' => 'Interest Income',
'account_type' => 'income',
'category' => '其他收入',
'display_order' => 460,
],
[
'account_code' => '4402',
'account_name_zh' => '活動報名費收入',
'account_name_en' => 'Activity Registration Fees',
'account_type' => 'income',
'category' => '其他收入',
'display_order' => 470,
'description' => '各項活動報名費',
],
[
'account_code' => '4901',
'account_name_zh' => '雜項收入',
'account_name_en' => 'Miscellaneous Income',
'account_type' => 'income',
'category' => '其他收入',
'display_order' => 480,
],
// Expenses (支出) - 5xxx
// Personnel Expenses (人事費)
[
'account_code' => '5101',
'account_name_zh' => '薪資支出',
'account_name_en' => 'Salaries & Wages',
'account_type' => 'expense',
'category' => '人事費',
'display_order' => 500,
],
[
'account_code' => '5102',
'account_name_zh' => '勞健保費',
'account_name_en' => 'Labor & Health Insurance',
'account_type' => 'expense',
'category' => '人事費',
'display_order' => 510,
],
[
'account_code' => '5103',
'account_name_zh' => '退休金提撥',
'account_name_en' => 'Pension Contributions',
'account_type' => 'expense',
'category' => '人事費',
'display_order' => 520,
],
[
'account_code' => '5104',
'account_name_zh' => '加班費',
'account_name_en' => 'Overtime Pay',
'account_type' => 'expense',
'category' => '人事費',
'display_order' => 530,
],
[
'account_code' => '5105',
'account_name_zh' => '員工福利',
'account_name_en' => 'Employee Benefits',
'account_type' => 'expense',
'category' => '人事費',
'display_order' => 540,
],
// Operating Expenses (業務費)
[
'account_code' => '5201',
'account_name_zh' => '租金支出',
'account_name_en' => 'Rent Expense',
'account_type' => 'expense',
'category' => '業務費',
'display_order' => 550,
],
[
'account_code' => '5202',
'account_name_zh' => '水電費',
'account_name_en' => 'Utilities',
'account_type' => 'expense',
'category' => '業務費',
'display_order' => 560,
],
[
'account_code' => '5203',
'account_name_zh' => '郵電費',
'account_name_en' => 'Postage & Telecommunications',
'account_type' => 'expense',
'category' => '業務費',
'display_order' => 570,
],
[
'account_code' => '5204',
'account_name_zh' => '文具用品',
'account_name_en' => 'Office Supplies',
'account_type' => 'expense',
'category' => '業務費',
'display_order' => 580,
],
[
'account_code' => '5205',
'account_name_zh' => '印刷費',
'account_name_en' => 'Printing Expenses',
'account_type' => 'expense',
'category' => '業務費',
'display_order' => 590,
],
[
'account_code' => '5206',
'account_name_zh' => '旅運費',
'account_name_en' => 'Travel & Transportation',
'account_type' => 'expense',
'category' => '業務費',
'display_order' => 600,
],
[
'account_code' => '5207',
'account_name_zh' => '保險費',
'account_name_en' => 'Insurance Premiums',
'account_type' => 'expense',
'category' => '業務費',
'display_order' => 610,
],
[
'account_code' => '5208',
'account_name_zh' => '修繕費',
'account_name_en' => 'Repairs & Maintenance',
'account_type' => 'expense',
'category' => '業務費',
'display_order' => 620,
],
[
'account_code' => '5209',
'account_name_zh' => '會議費',
'account_name_en' => 'Meeting Expenses',
'account_type' => 'expense',
'category' => '業務費',
'display_order' => 630,
],
// Program/Activity Expenses (活動費)
[
'account_code' => '5301',
'account_name_zh' => '活動場地費',
'account_name_en' => 'Activity Venue Rental',
'account_type' => 'expense',
'category' => '活動費',
'display_order' => 640,
],
[
'account_code' => '5302',
'account_name_zh' => '活動講師費',
'account_name_en' => 'Speaker/Instructor Fees',
'account_type' => 'expense',
'category' => '活動費',
'display_order' => 650,
],
[
'account_code' => '5303',
'account_name_zh' => '活動餐費',
'account_name_en' => 'Activity Catering',
'account_type' => 'expense',
'category' => '活動費',
'display_order' => 660,
],
[
'account_code' => '5304',
'account_name_zh' => '活動材料費',
'account_name_en' => 'Activity Materials',
'account_type' => 'expense',
'category' => '活動費',
'display_order' => 670,
],
[
'account_code' => '5305',
'account_name_zh' => '活動宣傳費',
'account_name_en' => 'Activity Promotion',
'account_type' => 'expense',
'category' => '活動費',
'display_order' => 680,
],
// Administrative Expenses (行政管理費)
[
'account_code' => '5401',
'account_name_zh' => '稅捐',
'account_name_en' => 'Taxes',
'account_type' => 'expense',
'category' => '行政管理費',
'display_order' => 690,
],
[
'account_code' => '5402',
'account_name_zh' => '規費',
'account_name_en' => 'Administrative Fees',
'account_type' => 'expense',
'category' => '行政管理費',
'display_order' => 700,
],
[
'account_code' => '5403',
'account_name_zh' => '銀行手續費',
'account_name_en' => 'Bank Service Charges',
'account_type' => 'expense',
'category' => '行政管理費',
'display_order' => 710,
],
[
'account_code' => '5404',
'account_name_zh' => '電腦網路費',
'account_name_en' => 'IT & Network Expenses',
'account_type' => 'expense',
'category' => '行政管理費',
'display_order' => 720,
],
[
'account_code' => '5405',
'account_name_zh' => '專業服務費',
'account_name_en' => 'Professional Services',
'account_type' => 'expense',
'category' => '行政管理費',
'display_order' => 730,
'description' => '會計師、律師等專業服務費',
],
[
'account_code' => '5406',
'account_name_zh' => '折舊費用',
'account_name_en' => 'Depreciation',
'account_type' => 'expense',
'category' => '行政管理費',
'display_order' => 740,
],
// Other Expenses (其他支出)
[
'account_code' => '5901',
'account_name_zh' => '雜項支出',
'account_name_en' => 'Miscellaneous Expenses',
'account_type' => 'expense',
'category' => '其他支出',
'display_order' => 750,
],
[
'account_code' => '5902',
'account_name_zh' => '呆帳損失',
'account_name_en' => 'Bad Debt Expense',
'account_type' => 'expense',
'category' => '其他支出',
'display_order' => 760,
],
];
foreach ($accounts as $account) {
ChartOfAccount::create($account);
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace Database\Seeders;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
{
// \App\Models\User::factory(10)->create();
// \App\Models\User::factory()->create([
// 'name' => 'Test User',
// 'email' => 'test@example.com',
// ]);
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Database\Seeders;
use App\Models\DocumentCategory;
use Illuminate\Database\Seeder;
class DocumentCategorySeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$categories = [
[
'name' => '協會辦法',
'slug' => 'bylaws',
'description' => '協會章程、組織辦法等基本規範文件',
'icon' => '📜',
'sort_order' => 1,
'default_access_level' => 'public',
],
[
'name' => '法規與規範',
'slug' => 'regulations',
'description' => '內部規章、管理辦法、作業規範',
'icon' => '📋',
'sort_order' => 2,
'default_access_level' => 'members',
],
[
'name' => '會議記錄',
'slug' => 'meeting-minutes',
'description' => '理事會、會員大會、各委員會會議記錄',
'icon' => '📝',
'sort_order' => 3,
'default_access_level' => 'members',
],
[
'name' => '表格與申請書',
'slug' => 'forms',
'description' => '各類申請表格、會員服務表單',
'icon' => '📄',
'sort_order' => 4,
'default_access_level' => 'public',
],
[
'name' => '年度報告',
'slug' => 'annual-reports',
'description' => '年度工作報告、財務報告',
'icon' => '📊',
'sort_order' => 5,
'default_access_level' => 'members',
],
[
'name' => '活動文件',
'slug' => 'events',
'description' => '活動企劃、執行報告、相關文件',
'icon' => '🎯',
'sort_order' => 6,
'default_access_level' => 'members',
],
];
foreach ($categories as $category) {
DocumentCategory::updateOrCreate(
['slug' => $category['slug']],
$category
);
}
$this->command->info('Document categories seeded successfully!');
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class FinancialWorkflowPermissionsSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Create permissions for financial workflow
$permissions = [
// Approval Stage Permissions
'approve_finance_cashier' => '出納審核財務申請單(第一階段)',
'approve_finance_accountant' => '會計審核財務申請單(第二階段)',
'approve_finance_chair' => '理事長審核財務申請單(第三階段)',
'approve_finance_board' => '理事會審核大額財務申請大於50,000',
// Payment Stage Permissions
'create_payment_order' => '會計製作付款單',
'verify_payment_order' => '出納覆核付款單',
'execute_payment' => '出納執行付款',
'upload_payment_receipt' => '上傳付款憑證',
// Recording Stage Permissions
'record_cashier_ledger' => '出納記錄現金簿',
'record_accounting_transaction' => '會計記錄會計分錄',
'view_cashier_ledger' => '查看出納現金簿',
'view_accounting_transactions' => '查看會計分錄',
// Reconciliation Permissions
'prepare_bank_reconciliation' => '出納製作銀行調節表',
'review_bank_reconciliation' => '會計覆核銀行調節表',
'approve_bank_reconciliation' => '主管核准銀行調節表',
// General Finance Document Permissions
'view_finance_documents' => '查看財務申請單',
'create_finance_documents' => '建立財務申請單',
'edit_finance_documents' => '編輯財務申請單',
'delete_finance_documents' => '刪除財務申請單',
// Chart of Accounts & Budget Permissions
'assign_chart_of_account' => '指定會計科目',
'assign_budget_item' => '指定預算項目',
// Dashboard & Reports Permissions
'view_finance_dashboard' => '查看財務儀表板',
'view_finance_reports' => '查看財務報表',
'export_finance_reports' => '匯出財務報表',
];
foreach ($permissions as $name => $description) {
Permission::firstOrCreate(
['name' => $name],
['guard_name' => 'web']
);
$this->command->info("Permission created: {$name}");
}
// Create roles for financial workflow
$roles = [
'finance_cashier' => [
'permissions' => [
// Approval stage
'approve_finance_cashier',
// Payment stage
'verify_payment_order',
'execute_payment',
'upload_payment_receipt',
// Recording stage
'record_cashier_ledger',
'view_cashier_ledger',
// Reconciliation
'prepare_bank_reconciliation',
// General
'view_finance_documents',
'view_finance_dashboard',
],
'description' => '出納 - 管錢(覆核付款單、執行付款、記錄現金簿、製作銀行調節表)',
],
'finance_accountant' => [
'permissions' => [
// Approval stage
'approve_finance_accountant',
// Payment stage
'create_payment_order',
// Recording stage
'record_accounting_transaction',
'view_accounting_transactions',
// Reconciliation
'review_bank_reconciliation',
// Chart of accounts & budget
'assign_chart_of_account',
'assign_budget_item',
// General
'view_finance_documents',
'view_finance_dashboard',
'view_finance_reports',
'export_finance_reports',
],
'description' => '會計 - 管帳(製作付款單、記錄會計分錄、覆核銀行調節表、指定會計科目)',
],
'finance_chair' => [
'permissions' => [
// Approval stage
'approve_finance_chair',
// Reconciliation
'approve_bank_reconciliation',
// General
'view_finance_documents',
'view_finance_dashboard',
'view_finance_reports',
'export_finance_reports',
],
'description' => '理事長 - 審核中大額財務申請、核准銀行調節表',
],
'finance_board_member' => [
'permissions' => [
// Approval stage (for large amounts)
'approve_finance_board',
// General
'view_finance_documents',
'view_finance_dashboard',
'view_finance_reports',
],
'description' => '理事 - 審核大額財務申請大於50,000',
],
'finance_requester' => [
'permissions' => [
'view_finance_documents',
'create_finance_documents',
'edit_finance_documents',
],
'description' => '財務申請人 - 可建立和編輯自己的財務申請單',
],
];
foreach ($roles as $roleName => $roleData) {
$role = Role::firstOrCreate(
['name' => $roleName],
['guard_name' => 'web']
);
// Assign permissions to role
$role->syncPermissions($roleData['permissions']);
$this->command->info("Role created: {$roleName} with permissions: " . implode(', ', $roleData['permissions']));
}
// Assign all financial workflow permissions to admin role (if exists)
$adminRole = Role::where('name', 'admin')->first();
if ($adminRole) {
$adminRole->givePermissionTo(array_keys($permissions));
$this->command->info("Admin role updated with all financial workflow permissions");
}
$this->command->info("\n=== Financial Workflow Roles & Permissions Created ===");
$this->command->info("Roles created:");
$this->command->info("1. finance_cashier - 出納(管錢)");
$this->command->info("2. finance_accountant - 會計(管帳)");
$this->command->info("3. finance_chair - 理事長");
$this->command->info("4. finance_board_member - 理事");
$this->command->info("5. finance_requester - 財務申請人");
$this->command->info("\nWorkflow stages:");
$this->command->info("1. Approval Stage: Cashier → Accountant → Chair (→ Board for large amounts)");
$this->command->info("2. Payment Stage: Accountant creates order → Cashier verifies → Cashier executes");
$this->command->info("3. Recording Stage: Cashier records ledger + Accountant records transactions");
$this->command->info("4. Reconciliation: Cashier prepares → Accountant reviews → Chair approves");
}
}

View File

@@ -0,0 +1,393 @@
<?php
namespace Database\Seeders;
use App\Models\BankReconciliation;
use App\Models\CashierLedgerEntry;
use App\Models\FinanceDocument;
use App\Models\PaymentOrder;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
/**
* Financial Workflow Test Data Seeder
*
* Generates comprehensive test data for the financial workflow system
*/
class FinancialWorkflowTestDataSeeder extends Seeder
{
protected User $requester;
protected User $cashier;
protected User $accountant;
protected User $chair;
protected User $boardMember;
/**
* Run the database seeds.
*/
public function run(): void
{
$this->command->info('🌱 Seeding financial workflow test data...');
// Create or get test users
$this->createTestUsers();
// Seed finance documents at various stages
$this->seedFinanceDocuments();
// Seed payment orders
$this->seedPaymentOrders();
// Seed cashier ledger entries
$this->seedCashierLedgerEntries();
// Seed bank reconciliations
$this->seedBankReconciliations();
$this->command->info('✅ Financial workflow test data seeded successfully!');
}
/**
* Create test users with appropriate roles
*/
protected function createTestUsers(): void
{
$this->command->info('Creating test users...');
$this->requester = User::firstOrCreate(
['email' => 'requester@test.com'],
[
'name' => 'Test Requester',
'password' => Hash::make('password'),
]
);
$this->requester->assignRole('finance_requester');
$this->cashier = User::firstOrCreate(
['email' => 'cashier@test.com'],
[
'name' => 'Test Cashier',
'password' => Hash::make('password'),
]
);
$this->cashier->assignRole('finance_cashier');
$this->accountant = User::firstOrCreate(
['email' => 'accountant@test.com'],
[
'name' => 'Test Accountant',
'password' => Hash::make('password'),
]
);
$this->accountant->assignRole('finance_accountant');
$this->chair = User::firstOrCreate(
['email' => 'chair@test.com'],
[
'name' => 'Test Chair',
'password' => Hash::make('password'),
]
);
$this->chair->assignRole('finance_chair');
$this->boardMember = User::firstOrCreate(
['email' => 'board@test.com'],
[
'name' => 'Test Board Member',
'password' => Hash::make('password'),
]
);
$this->boardMember->assignRole('finance_board_member');
$this->command->info('✓ Test users created');
}
/**
* Seed finance documents at various stages of the workflow
*/
protected function seedFinanceDocuments(): void
{
$this->command->info('Seeding finance documents...');
// Pending documents (Stage 1)
FinanceDocument::factory()
->count(3)
->smallAmount()
->pending()
->create(['submitted_by_id' => $this->requester->id]);
FinanceDocument::factory()
->count(2)
->mediumAmount()
->pending()
->create(['submitted_by_id' => $this->requester->id]);
// Approved by cashier (Stage 1)
FinanceDocument::factory()
->count(2)
->smallAmount()
->approvedByCashier()
->create([
'submitted_by_id' => $this->requester->id,
'cashier_approved_by_id' => $this->cashier->id,
]);
// Approved by accountant - small amounts (Ready for payment)
FinanceDocument::factory()
->count(3)
->smallAmount()
->approvedByAccountant()
->create([
'submitted_by_id' => $this->requester->id,
'cashier_approved_by_id' => $this->cashier->id,
'accountant_approved_by_id' => $this->accountant->id,
]);
// Approved by chair - medium amounts (Ready for payment)
FinanceDocument::factory()
->count(2)
->mediumAmount()
->approvedByChair()
->create([
'submitted_by_id' => $this->requester->id,
'cashier_approved_by_id' => $this->cashier->id,
'accountant_approved_by_id' => $this->accountant->id,
'chair_approved_by_id' => $this->chair->id,
]);
// Large amount with board approval (Ready for payment)
FinanceDocument::factory()
->count(1)
->largeAmount()
->approvedByChair()
->create([
'submitted_by_id' => $this->requester->id,
'cashier_approved_by_id' => $this->cashier->id,
'accountant_approved_by_id' => $this->accountant->id,
'chair_approved_by_id' => $this->chair->id,
'board_meeting_approved_at' => now(),
'board_meeting_approved_by_id' => $this->boardMember->id,
]);
// Completed workflow
FinanceDocument::factory()
->count(5)
->smallAmount()
->approvedByAccountant()
->paymentExecuted()
->create([
'submitted_by_id' => $this->requester->id,
'cashier_approved_by_id' => $this->cashier->id,
'accountant_approved_by_id' => $this->accountant->id,
'cashier_recorded_at' => now(),
]);
// Rejected documents
FinanceDocument::factory()
->count(2)
->rejected()
->create([
'submitted_by_id' => $this->requester->id,
]);
$this->command->info('✓ Finance documents seeded');
}
/**
* Seed payment orders
*/
protected function seedPaymentOrders(): void
{
$this->command->info('Seeding payment orders...');
// Get approved documents without payment orders
$approvedDocs = FinanceDocument::whereIn('status', [
FinanceDocument::STATUS_APPROVED_ACCOUNTANT,
FinanceDocument::STATUS_APPROVED_CHAIR,
])
->whereNull('payment_order_created_at')
->limit(5)
->get();
foreach ($approvedDocs as $doc) {
// Pending verification
PaymentOrder::factory()
->pendingVerification()
->create([
'finance_document_id' => $doc->id,
'payment_amount' => $doc->amount,
'created_by_accountant_id' => $this->accountant->id,
]);
$doc->update([
'payment_order_created_at' => now(),
'payment_order_created_by_id' => $this->accountant->id,
]);
}
// Verified payment orders
PaymentOrder::factory()
->count(3)
->verified()
->create([
'created_by_accountant_id' => $this->accountant->id,
'verified_by_cashier_id' => $this->cashier->id,
]);
// Executed payment orders
PaymentOrder::factory()
->count(5)
->executed()
->create([
'created_by_accountant_id' => $this->accountant->id,
'verified_by_cashier_id' => $this->cashier->id,
'executed_by_cashier_id' => $this->cashier->id,
]);
$this->command->info('✓ Payment orders seeded');
}
/**
* Seed cashier ledger entries with running balances
*/
protected function seedCashierLedgerEntries(): void
{
$this->command->info('Seeding cashier ledger entries...');
$bankAccounts = [
'First Bank - 1234567890',
'Second Bank - 0987654321',
'Petty Cash',
];
foreach ($bankAccounts as $account) {
$currentBalance = 100000; // Starting balance
// Create 10 entries for each account
for ($i = 0; $i < 10; $i++) {
$isReceipt = $i % 3 !== 0; // 2/3 receipts, 1/3 payments
$amount = rand(1000, 10000);
$entry = CashierLedgerEntry::create([
'entry_type' => $isReceipt ? 'receipt' : 'payment',
'entry_date' => now()->subDays(rand(1, 30)),
'amount' => $amount,
'payment_method' => $account === 'Petty Cash' ? 'cash' : 'bank_transfer',
'bank_account' => $account,
'balance_before' => $currentBalance,
'balance_after' => $isReceipt
? $currentBalance + $amount
: $currentBalance - $amount,
'receipt_number' => $isReceipt ? 'RCP' . str_pad($i + 1, 6, '0', STR_PAD_LEFT) : null,
'notes' => $isReceipt ? 'Test receipt entry' : 'Test payment entry',
'recorded_by_cashier_id' => $this->cashier->id,
'recorded_at' => now()->subDays(rand(1, 30)),
]);
$currentBalance = $entry->balance_after;
}
}
$this->command->info('✓ Cashier ledger entries seeded');
}
/**
* Seed bank reconciliations
*/
protected function seedBankReconciliations(): void
{
$this->command->info('Seeding bank reconciliations...');
// Pending reconciliation
BankReconciliation::create([
'reconciliation_month' => now()->startOfMonth(),
'bank_statement_date' => now(),
'bank_statement_balance' => 100000,
'system_book_balance' => 95000,
'outstanding_checks' => [
['check_number' => 'CHK001', 'amount' => 3000, 'description' => 'Vendor A payment'],
['check_number' => 'CHK002', 'amount' => 2000, 'description' => 'Service fee'],
],
'deposits_in_transit' => [
['date' => now()->format('Y-m-d'), 'amount' => 5000, 'description' => 'Member dues'],
],
'bank_charges' => [
['amount' => 500, 'description' => 'Monthly service charge'],
],
'discrepancy_amount' => 4500,
'notes' => 'Pending review',
'prepared_by_cashier_id' => $this->cashier->id,
'prepared_at' => now(),
'reconciliation_status' => 'pending',
]);
// Reviewed reconciliation
BankReconciliation::create([
'reconciliation_month' => now()->subMonth()->startOfMonth(),
'bank_statement_date' => now()->subMonth(),
'bank_statement_balance' => 95000,
'system_book_balance' => 93000,
'outstanding_checks' => [
['check_number' => 'CHK003', 'amount' => 1500, 'description' => 'Supplies'],
],
'deposits_in_transit' => [
['date' => now()->subMonth()->format('Y-m-d'), 'amount' => 3000, 'description' => 'Donation'],
],
'bank_charges' => [
['amount' => 500, 'description' => 'Service charge'],
],
'discrepancy_amount' => 0,
'notes' => 'All items reconciled',
'prepared_by_cashier_id' => $this->cashier->id,
'prepared_at' => now()->subMonth(),
'reviewed_by_accountant_id' => $this->accountant->id,
'reviewed_at' => now()->subMonth()->addDays(2),
'reconciliation_status' => 'pending',
]);
// Completed reconciliation
BankReconciliation::create([
'reconciliation_month' => now()->subMonths(2)->startOfMonth(),
'bank_statement_date' => now()->subMonths(2),
'bank_statement_balance' => 90000,
'system_book_balance' => 90000,
'outstanding_checks' => [],
'deposits_in_transit' => [],
'bank_charges' => [
['amount' => 500, 'description' => 'Service charge'],
],
'discrepancy_amount' => 0,
'notes' => 'Perfect match',
'prepared_by_cashier_id' => $this->cashier->id,
'prepared_at' => now()->subMonths(2),
'reviewed_by_accountant_id' => $this->accountant->id,
'reviewed_at' => now()->subMonths(2)->addDays(2),
'approved_by_manager_id' => $this->chair->id,
'approved_at' => now()->subMonths(2)->addDays(3),
'reconciliation_status' => 'completed',
]);
// Reconciliation with discrepancy
BankReconciliation::create([
'reconciliation_month' => now()->subMonths(3)->startOfMonth(),
'bank_statement_date' => now()->subMonths(3),
'bank_statement_balance' => 85000,
'system_book_balance' => 75000,
'outstanding_checks' => [
['check_number' => 'CHK004', 'amount' => 2000, 'description' => 'Payment'],
],
'deposits_in_transit' => [],
'bank_charges' => [
['amount' => 500, 'description' => 'Service charge'],
],
'discrepancy_amount' => 7500,
'notes' => 'Large discrepancy - needs investigation',
'prepared_by_cashier_id' => $this->cashier->id,
'prepared_at' => now()->subMonths(3),
'reconciliation_status' => 'discrepancy',
]);
$this->command->info('✓ Bank reconciliations seeded');
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Database\Seeders;
use App\Models\IssueLabel;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class IssueLabelSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$labels = [
[
'name' => 'urgent',
'color' => '#DC2626',
'description' => 'Requires immediate attention',
],
[
'name' => 'bug',
'color' => '#EF4444',
'description' => 'Something is not working correctly',
],
[
'name' => 'enhancement',
'color' => '#3B82F6',
'description' => 'New feature or improvement request',
],
[
'name' => 'documentation',
'color' => '#10B981',
'description' => 'Documentation related task',
],
[
'name' => 'member-facing',
'color' => '#8B5CF6',
'description' => 'Affects members directly',
],
[
'name' => 'internal',
'color' => '#F59E0B',
'description' => 'Internal staff operations',
],
[
'name' => 'event',
'color' => '#EC4899',
'description' => 'Event planning or execution',
],
[
'name' => 'finance',
'color' => '#14B8A6',
'description' => 'Financial or budget related',
],
[
'name' => 'communications',
'color' => '#6366F1',
'description' => 'Marketing, PR, or communications',
],
[
'name' => 'blocked',
'color' => '#64748B',
'description' => 'Blocked by another issue or dependency',
],
[
'name' => 'help-wanted',
'color' => '#22C55E',
'description' => 'Looking for volunteers or assistance',
],
[
'name' => 'question',
'color' => '#A855F7',
'description' => 'Question or clarification needed',
],
];
foreach ($labels as $label) {
IssueLabel::updateOrCreate(
['name' => $label['name']],
$label
);
}
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class PaymentVerificationRolesSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
// Create permissions for payment verification workflow
$permissions = [
'verify_payments_cashier' => 'Verify membership payments as cashier (Tier 1)',
'verify_payments_accountant' => 'Verify membership payments as accountant (Tier 2)',
'verify_payments_chair' => 'Verify membership payments as chair (Tier 3)',
'activate_memberships' => 'Activate member accounts after payment approval',
'view_payment_verifications' => 'View payment verification dashboard',
];
foreach ($permissions as $name => $description) {
Permission::firstOrCreate(
['name' => $name],
['guard_name' => 'web']
);
$this->command->info("Permission created: {$name}");
}
// Create roles for payment verification
$roles = [
'payment_cashier' => [
'permissions' => ['verify_payments_cashier', 'view_payment_verifications'],
'description' => 'Cashier - First tier payment verification',
],
'payment_accountant' => [
'permissions' => ['verify_payments_accountant', 'view_payment_verifications'],
'description' => 'Accountant - Second tier payment verification',
],
'payment_chair' => [
'permissions' => ['verify_payments_chair', 'view_payment_verifications'],
'description' => 'Chair - Final tier payment verification',
],
'membership_manager' => [
'permissions' => ['activate_memberships', 'view_payment_verifications'],
'description' => 'Membership Manager - Can activate memberships after approval',
],
];
foreach ($roles as $roleName => $roleData) {
$role = Role::firstOrCreate(
['name' => $roleName],
['guard_name' => 'web']
);
// Assign permissions to role
$role->syncPermissions($roleData['permissions']);
$this->command->info("Role created: {$roleName} with permissions: " . implode(', ', $roleData['permissions']));
}
// Assign all payment verification permissions to admin role (if exists)
$adminRole = Role::where('name', 'admin')->first();
if ($adminRole) {
$adminRole->givePermissionTo([
'verify_payments_cashier',
'verify_payments_accountant',
'verify_payments_chair',
'activate_memberships',
'view_payment_verifications',
]);
$this->command->info("Admin role updated with all payment verification permissions");
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;
class RoleSeeder extends Seeder
{
public function run(): void
{
$roles = [
'admin' => 'Full system administrator',
'staff' => 'General staff with access to internal tools',
'cashier' => 'Handles payment recording and finance intake',
'accountant' => 'Reviews finance docs and approvals',
'chair' => 'Board chairperson for final approvals',
];
collect($roles)->each(function ($description, $role) {
Role::updateOrCreate(
['name' => $role, 'guard_name' => 'web'],
['description' => $description]
);
});
}
}

View File

@@ -0,0 +1,240 @@
<?php
namespace Database\Seeders;
use App\Models\SystemSetting;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
class SystemSettingsSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$settings = [
// General Settings
[
'key' => 'general.system_name',
'value' => 'Usher Management System',
'type' => 'string',
'group' => 'general',
'description' => 'System name displayed throughout the application'
],
[
'key' => 'general.timezone',
'value' => 'Asia/Taipei',
'type' => 'string',
'group' => 'general',
'description' => 'System timezone'
],
// Document Features
[
'key' => 'features.qr_codes_enabled',
'value' => '1',
'type' => 'boolean',
'group' => 'features',
'description' => 'Enable QR code generation for documents'
],
[
'key' => 'features.tagging_enabled',
'value' => '1',
'type' => 'boolean',
'group' => 'features',
'description' => 'Enable document tagging system'
],
[
'key' => 'features.expiration_enabled',
'value' => '1',
'type' => 'boolean',
'group' => 'features',
'description' => 'Enable document expiration dates and auto-archive'
],
[
'key' => 'features.bulk_import_enabled',
'value' => '1',
'type' => 'boolean',
'group' => 'features',
'description' => 'Enable bulk document import feature'
],
[
'key' => 'features.statistics_enabled',
'value' => '1',
'type' => 'boolean',
'group' => 'features',
'description' => 'Enable document statistics dashboard'
],
[
'key' => 'features.version_history_enabled',
'value' => '1',
'type' => 'boolean',
'group' => 'features',
'description' => 'Enable document version history tracking'
],
// Security & Limits
[
'key' => 'security.rate_limit_authenticated',
'value' => '50',
'type' => 'integer',
'group' => 'security',
'description' => 'Downloads per hour for authenticated users'
],
[
'key' => 'security.rate_limit_guest',
'value' => '10',
'type' => 'integer',
'group' => 'security',
'description' => 'Downloads per hour for guest users'
],
[
'key' => 'security.max_file_size_mb',
'value' => '10',
'type' => 'integer',
'group' => 'security',
'description' => 'Maximum file upload size in MB'
],
[
'key' => 'security.allowed_file_types',
'value' => json_encode(['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt', 'jpg', 'jpeg', 'png']),
'type' => 'json',
'group' => 'security',
'description' => 'Allowed file types for uploads'
],
// Document Settings
[
'key' => 'documents.default_access_level',
'value' => 'members',
'type' => 'string',
'group' => 'documents',
'description' => 'Default access level for new documents (public, members, admin, board)'
],
[
'key' => 'documents.default_expiration_days',
'value' => '90',
'type' => 'integer',
'group' => 'documents',
'description' => 'Default expiration period in days (0 = no expiration)'
],
[
'key' => 'documents.expiration_warning_days',
'value' => '30',
'type' => 'integer',
'group' => 'documents',
'description' => 'Days before expiration to show warning'
],
[
'key' => 'documents.auto_archive_enabled',
'value' => '0',
'type' => 'boolean',
'group' => 'documents',
'description' => 'Automatically archive expired documents'
],
[
'key' => 'documents.max_tags_per_document',
'value' => '10',
'type' => 'integer',
'group' => 'documents',
'description' => 'Maximum number of tags per document'
],
// Notifications
[
'key' => 'notifications.enabled',
'value' => '1',
'type' => 'boolean',
'group' => 'notifications',
'description' => 'Enable email notifications'
],
[
'key' => 'notifications.expiration_alerts_enabled',
'value' => '1',
'type' => 'boolean',
'group' => 'notifications',
'description' => 'Send email alerts for expiring documents'
],
[
'key' => 'notifications.expiration_recipients',
'value' => json_encode([]),
'type' => 'json',
'group' => 'notifications',
'description' => 'Email recipients for expiration alerts'
],
[
'key' => 'notifications.archive_notifications_enabled',
'value' => '1',
'type' => 'boolean',
'group' => 'notifications',
'description' => 'Send notifications when documents are auto-archived'
],
[
'key' => 'notifications.new_document_alerts_enabled',
'value' => '0',
'type' => 'boolean',
'group' => 'notifications',
'description' => 'Send alerts when new documents are uploaded'
],
// Advanced Settings
[
'key' => 'advanced.qr_code_size',
'value' => '300',
'type' => 'integer',
'group' => 'advanced',
'description' => 'QR code size in pixels'
],
[
'key' => 'advanced.qr_code_format',
'value' => 'png',
'type' => 'string',
'group' => 'advanced',
'description' => 'QR code format (png or svg)'
],
[
'key' => 'advanced.statistics_time_range',
'value' => '30',
'type' => 'integer',
'group' => 'advanced',
'description' => 'Default time range for statistics in days'
],
[
'key' => 'advanced.statistics_top_n',
'value' => '10',
'type' => 'integer',
'group' => 'advanced',
'description' => 'Number of top items to display in statistics'
],
[
'key' => 'advanced.audit_log_retention_days',
'value' => '365',
'type' => 'integer',
'group' => 'advanced',
'description' => 'How long to retain audit logs in days'
],
[
'key' => 'advanced.max_versions_retain',
'value' => '0',
'type' => 'integer',
'group' => 'advanced',
'description' => 'Maximum versions to retain per document (0 = unlimited)'
],
];
foreach ($settings as $settingData) {
SystemSetting::updateOrCreate(
['key' => $settingData['key']],
[
'value' => $settingData['value'],
'type' => $settingData['type'],
'group' => $settingData['group'],
'description' => $settingData['description'],
]
);
}
$this->command->info('System settings seeded successfully (' . count($settings) . ' settings)');
}
}

View File

@@ -0,0 +1,769 @@
<?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('');
}
}