Add membership fee system with disability discount and fix document permissions
Features: - Implement two fee types: entrance fee and annual fee (both NT$1,000) - Add 50% discount for disability certificate holders - Add disability certificate upload in member profile - Integrate disability verification into cashier approval workflow - Add membership fee settings in system admin Document permissions: - Fix hard-coded role logic in Document model - Use permission-based authorization instead of role checks Additional features: - Add announcements, general ledger, and trial balance modules - Add income management and accounting entries - Add comprehensive test suite with factories - Update UI translations to Traditional Chinese 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
40
database/factories/AuditLogFactory.php
Normal file
40
database/factories/AuditLogFactory.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class AuditLogFactory extends Factory
|
||||
{
|
||||
protected $model = AuditLog::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => User::factory(),
|
||||
'action' => $this->faker->randomElement([
|
||||
'member_created',
|
||||
'member_status_changed',
|
||||
'payment_approved',
|
||||
'payment_rejected',
|
||||
'finance_document_created',
|
||||
'finance_document_approved',
|
||||
'user_login',
|
||||
'role_assigned',
|
||||
]),
|
||||
'auditable_type' => $this->faker->randomElement([
|
||||
'App\Models\Member',
|
||||
'App\Models\MembershipPayment',
|
||||
'App\Models\FinanceDocument',
|
||||
'App\Models\User',
|
||||
]),
|
||||
'auditable_id' => $this->faker->numberBetween(1, 100),
|
||||
'metadata' => [
|
||||
'ip_address' => $this->faker->ipv4(),
|
||||
'user_agent' => $this->faker->userAgent(),
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
32
database/factories/BankReconciliationFactory.php
Normal file
32
database/factories/BankReconciliationFactory.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\BankReconciliation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class BankReconciliationFactory extends Factory
|
||||
{
|
||||
protected $model = BankReconciliation::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
$bankBalance = $this->faker->numberBetween(50000, 500000);
|
||||
$bookBalance = $bankBalance + $this->faker->numberBetween(-5000, 5000);
|
||||
|
||||
return [
|
||||
'reconciliation_month' => now()->startOfMonth(),
|
||||
'bank_statement_date' => now(),
|
||||
'bank_statement_balance' => $bankBalance,
|
||||
'system_book_balance' => $bookBalance,
|
||||
'outstanding_checks' => [],
|
||||
'deposits_in_transit' => [],
|
||||
'bank_charges' => [],
|
||||
'prepared_by_cashier_id' => User::factory(),
|
||||
'prepared_at' => now(),
|
||||
'reconciliation_status' => 'pending',
|
||||
'discrepancy_amount' => abs($bankBalance - $bookBalance),
|
||||
];
|
||||
}
|
||||
}
|
||||
16
database/factories/BudgetCategoryFactory.php
Normal file
16
database/factories/BudgetCategoryFactory.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class BudgetCategoryFactory extends Factory
|
||||
{
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->unique()->words(2, true),
|
||||
'description' => $this->faker->sentence(),
|
||||
];
|
||||
}
|
||||
}
|
||||
22
database/factories/ChartOfAccountFactory.php
Normal file
22
database/factories/ChartOfAccountFactory.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\ChartOfAccount;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class ChartOfAccountFactory extends Factory
|
||||
{
|
||||
protected $model = ChartOfAccount::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'code' => $this->faker->unique()->numerify('####'),
|
||||
'name' => $this->faker->words(3, true),
|
||||
'type' => $this->faker->randomElement(['asset', 'liability', 'equity', 'revenue', 'expense']),
|
||||
'description' => $this->faker->sentence(),
|
||||
'is_active' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
23
database/factories/DocumentCategoryFactory.php
Normal file
23
database/factories/DocumentCategoryFactory.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\DocumentCategory;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class DocumentCategoryFactory extends Factory
|
||||
{
|
||||
protected $model = DocumentCategory::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->faker->unique()->words(2, true),
|
||||
'slug' => $this->faker->unique()->slug(2),
|
||||
'description' => $this->faker->sentence(),
|
||||
'icon' => $this->faker->randomElement(['📄', '📁', '📋', '📊', '📈']),
|
||||
'sort_order' => $this->faker->numberBetween(1, 100),
|
||||
'default_access_level' => $this->faker->randomElement(['public', 'members', 'admin', 'board']),
|
||||
];
|
||||
}
|
||||
}
|
||||
31
database/factories/DocumentFactory.php
Normal file
31
database/factories/DocumentFactory.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Document;
|
||||
use App\Models\DocumentCategory;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class DocumentFactory extends Factory
|
||||
{
|
||||
protected $model = Document::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'document_category_id' => DocumentCategory::factory(),
|
||||
'title' => $this->faker->sentence(3),
|
||||
'document_number' => 'DOC-'.now()->format('Y').'-'.str_pad(rand(1, 9999), 4, '0', STR_PAD_LEFT),
|
||||
'description' => $this->faker->paragraph(),
|
||||
'public_uuid' => (string) Str::uuid(),
|
||||
'access_level' => $this->faker->randomElement(['public', 'members', 'admin']),
|
||||
'status' => 'active',
|
||||
'created_by_user_id' => User::factory(),
|
||||
'view_count' => $this->faker->numberBetween(0, 100),
|
||||
'download_count' => $this->faker->numberBetween(0, 50),
|
||||
'version_count' => 1,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* 移除 is_admin 欄位,統一使用 Spatie Permission 的 admin 角色進行權限管理
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropColumn('is_admin');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->boolean('is_admin')->default(false)->after('email');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
if (Schema::hasTable('cashier_ledger_entries')) {
|
||||
return;
|
||||
}
|
||||
|
||||
Schema::create('cashier_ledger_entries', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('finance_document_id')->nullable()->constrained()->nullOnDelete();
|
||||
$table->date('entry_date');
|
||||
$table->string('entry_type'); // receipt, payment
|
||||
$table->string('payment_method'); // bank_transfer, check, cash
|
||||
$table->string('bank_account')->nullable();
|
||||
$table->decimal('amount', 12, 2);
|
||||
$table->decimal('balance_before', 12, 2)->default(0);
|
||||
$table->decimal('balance_after', 12, 2)->default(0);
|
||||
$table->string('receipt_number')->nullable();
|
||||
$table->string('transaction_reference')->nullable();
|
||||
$table->foreignId('recorded_by_cashier_id')->constrained('users');
|
||||
$table->timestamp('recorded_at');
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('entry_date');
|
||||
$table->index('entry_type');
|
||||
$table->index('bank_account');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('cashier_ledger_entries');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('announcements', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// 基本資訊
|
||||
$table->string('title');
|
||||
$table->text('content');
|
||||
|
||||
// 狀態管理
|
||||
$table->enum('status', ['draft', 'published', 'archived'])->default('draft');
|
||||
|
||||
// 顯示控制
|
||||
$table->boolean('is_pinned')->default(false);
|
||||
$table->integer('display_order')->default(0);
|
||||
|
||||
// 訪問控制
|
||||
$table->enum('access_level', ['public', 'members', 'board', 'admin'])->default('members');
|
||||
|
||||
// 時間控制
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamp('expires_at')->nullable();
|
||||
$table->timestamp('archived_at')->nullable();
|
||||
|
||||
// 統計
|
||||
$table->integer('view_count')->default(0);
|
||||
|
||||
// 用戶關聯
|
||||
$table->foreignId('created_by_user_id')->constrained('users')->onDelete('cascade');
|
||||
$table->foreignId('last_updated_by_user_id')->nullable()->constrained('users')->onDelete('set null');
|
||||
|
||||
$table->timestamps();
|
||||
$table->softDeletes();
|
||||
|
||||
// 索引
|
||||
$table->index('status');
|
||||
$table->index('access_level');
|
||||
$table->index('published_at');
|
||||
$table->index('expires_at');
|
||||
$table->index(['is_pinned', 'display_order']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('announcements');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('accounting_entries', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('finance_document_id')->constrained()->onDelete('cascade');
|
||||
$table->foreignId('chart_of_account_id')->constrained();
|
||||
$table->enum('entry_type', ['debit', 'credit']);
|
||||
$table->decimal('amount', 15, 2);
|
||||
$table->date('entry_date');
|
||||
$table->text('description')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// Indexes for performance
|
||||
$table->index(['finance_document_id', 'entry_type']);
|
||||
$table->index(['chart_of_account_id', 'entry_date']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('accounting_entries');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('board_meetings', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->date('meeting_date');
|
||||
$table->string('title');
|
||||
$table->text('notes')->nullable();
|
||||
$table->enum('status', ['scheduled', 'completed', 'cancelled'])->default('scheduled');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('board_meetings');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('finance_documents', function (Blueprint $table) {
|
||||
// 只新增尚未存在的欄位
|
||||
if (!Schema::hasColumn('finance_documents', 'accountant_recorded_by_id')) {
|
||||
$table->unsignedBigInteger('accountant_recorded_by_id')->nullable();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('finance_documents', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('finance_documents', 'accountant_recorded_by_id')) {
|
||||
$table->dropColumn('accountant_recorded_by_id');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('finance_documents', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('finance_documents', 'request_type')) {
|
||||
$table->dropColumn('request_type');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('finance_documents', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('finance_documents', 'request_type')) {
|
||||
$table->string('request_type')->nullable()->after('amount');
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('incomes', function (Blueprint $table) {
|
||||
$table->id();
|
||||
|
||||
// 基本資訊
|
||||
$table->string('income_number')->unique(); // 收入編號:INC-2025-0001
|
||||
$table->string('title'); // 收入標題
|
||||
$table->text('description')->nullable(); // 說明
|
||||
$table->date('income_date'); // 收入日期
|
||||
$table->decimal('amount', 12, 2); // 金額
|
||||
|
||||
// 收入分類
|
||||
$table->string('income_type'); // 收入類型
|
||||
$table->foreignId('chart_of_account_id') // 會計科目
|
||||
->constrained('chart_of_accounts');
|
||||
|
||||
// 付款資訊
|
||||
$table->string('payment_method'); // 付款方式
|
||||
$table->string('bank_account')->nullable(); // 銀行帳戶
|
||||
$table->string('payer_name')->nullable(); // 付款人姓名
|
||||
$table->string('receipt_number')->nullable(); // 收據編號
|
||||
$table->string('transaction_reference')->nullable(); // 銀行交易參考號
|
||||
$table->string('attachment_path')->nullable(); // 附件路徑
|
||||
|
||||
// 會員關聯
|
||||
$table->foreignId('member_id')->nullable()
|
||||
->constrained()->nullOnDelete();
|
||||
|
||||
// 審核流程
|
||||
$table->string('status')->default('pending'); // pending, confirmed, cancelled
|
||||
|
||||
// 出納記錄
|
||||
$table->foreignId('recorded_by_cashier_id')
|
||||
->constrained('users');
|
||||
$table->timestamp('recorded_at');
|
||||
|
||||
// 會計確認
|
||||
$table->foreignId('confirmed_by_accountant_id')->nullable()
|
||||
->constrained('users');
|
||||
$table->timestamp('confirmed_at')->nullable();
|
||||
|
||||
// 關聯出納日記帳
|
||||
$table->foreignId('cashier_ledger_entry_id')->nullable()
|
||||
->constrained('cashier_ledger_entries')->nullOnDelete();
|
||||
|
||||
$table->text('notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
// 索引
|
||||
$table->index('income_date');
|
||||
$table->index('income_type');
|
||||
$table->index('status');
|
||||
$table->index(['member_id', 'income_type']);
|
||||
});
|
||||
|
||||
// 在 accounting_entries 表新增 income_id 欄位
|
||||
Schema::table('accounting_entries', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('accounting_entries', 'income_id')) {
|
||||
$table->foreignId('income_id')->nullable()
|
||||
->after('finance_document_id')
|
||||
->constrained('incomes')->nullOnDelete();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('accounting_entries', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('accounting_entries', 'income_id')) {
|
||||
$table->dropForeign(['income_id']);
|
||||
$table->dropColumn('income_id');
|
||||
}
|
||||
});
|
||||
|
||||
Schema::dropIfExists('incomes');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('members', function (Blueprint $table) {
|
||||
// 身心障礙手冊相關欄位
|
||||
$table->string('disability_certificate_path')->nullable()->after('membership_type');
|
||||
$table->string('disability_certificate_status')->nullable()->after('disability_certificate_path');
|
||||
$table->foreignId('disability_verified_by')->nullable()->after('disability_certificate_status')
|
||||
->constrained('users')->nullOnDelete();
|
||||
$table->timestamp('disability_verified_at')->nullable()->after('disability_verified_by');
|
||||
$table->text('disability_rejection_reason')->nullable()->after('disability_verified_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('members', function (Blueprint $table) {
|
||||
$table->dropForeign(['disability_verified_by']);
|
||||
$table->dropColumn([
|
||||
'disability_certificate_path',
|
||||
'disability_certificate_status',
|
||||
'disability_verified_by',
|
||||
'disability_verified_at',
|
||||
'disability_rejection_reason',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('membership_payments', function (Blueprint $table) {
|
||||
// 會費類型欄位
|
||||
$table->string('fee_type')->default('entrance_fee')->after('member_id');
|
||||
$table->decimal('base_amount', 10, 2)->nullable()->after('amount');
|
||||
$table->decimal('discount_amount', 10, 2)->default(0)->after('base_amount');
|
||||
$table->decimal('final_amount', 10, 2)->nullable()->after('discount_amount');
|
||||
$table->boolean('disability_discount')->default(false)->after('final_amount');
|
||||
});
|
||||
|
||||
// 為現有記錄設定預設值
|
||||
\DB::statement("UPDATE membership_payments SET base_amount = amount, final_amount = amount WHERE base_amount IS NULL");
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('membership_payments', function (Blueprint $table) {
|
||||
$table->dropColumn([
|
||||
'fee_type',
|
||||
'base_amount',
|
||||
'discount_amount',
|
||||
'final_amount',
|
||||
'disability_discount',
|
||||
]);
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -484,7 +484,10 @@ class ChartOfAccountSeeder extends Seeder
|
||||
];
|
||||
|
||||
foreach ($accounts as $account) {
|
||||
ChartOfAccount::create($account);
|
||||
ChartOfAccount::firstOrCreate(
|
||||
['account_code' => $account['account_code']],
|
||||
$account
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,48 +11,85 @@ class FinancialWorkflowPermissionsSeeder extends Seeder
|
||||
{
|
||||
/**
|
||||
* Run the database seeds.
|
||||
*
|
||||
* 此 Seeder 建立統一的角色與權限系統,整合:
|
||||
* - 財務工作流程權限
|
||||
* - 會員繳費審核權限(原 PaymentVerificationRolesSeeder)
|
||||
* - 基礎角色(原 RoleSeeder)
|
||||
*/
|
||||
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)',
|
||||
// ===== 會員繳費審核權限(原 PaymentVerificationRolesSeeder) =====
|
||||
'verify_payments_cashier' => '出納審核會員繳費(第一階段)',
|
||||
'verify_payments_accountant' => '會計審核會員繳費(第二階段)',
|
||||
'verify_payments_chair' => '理事長審核會員繳費(第三階段)',
|
||||
'activate_memberships' => '啟用會員帳號',
|
||||
'view_payment_verifications' => '查看繳費審核儀表板',
|
||||
|
||||
// Payment Stage Permissions
|
||||
// ===== 財務申請單審核權限(新工作流程) =====
|
||||
'approve_finance_secretary' => '秘書長審核財務申請單(第一階段)',
|
||||
'approve_finance_chair' => '理事長審核財務申請單(第二階段:中額以上)',
|
||||
'approve_finance_board' => '董理事會審核財務申請單(第三階段:大額)',
|
||||
// Legacy permissions
|
||||
'approve_finance_cashier' => '出納審核財務申請單(舊流程)',
|
||||
'approve_finance_accountant' => '會計審核財務申請單(舊流程)',
|
||||
|
||||
// ===== 出帳確認權限 =====
|
||||
'confirm_disbursement_requester' => '申請人確認領款',
|
||||
'confirm_disbursement_cashier' => '出納確認出帳',
|
||||
|
||||
// ===== 入帳確認權限 =====
|
||||
'confirm_recording_accountant' => '會計確認入帳',
|
||||
|
||||
// ===== 收入管理權限 =====
|
||||
'view_incomes' => '查看收入記錄',
|
||||
'record_income' => '記錄收入(出納)',
|
||||
'confirm_income' => '確認收入(會計)',
|
||||
'cancel_income' => '取消收入',
|
||||
'export_incomes' => '匯出收入報表',
|
||||
'view_income_statistics' => '查看收入統計',
|
||||
|
||||
// ===== 付款階段權限 =====
|
||||
'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' => '匯出財務報表',
|
||||
|
||||
// ===== 公告系統權限 =====
|
||||
'view_announcements' => '查看公告',
|
||||
'create_announcements' => '建立公告',
|
||||
'edit_announcements' => '編輯公告',
|
||||
'delete_announcements' => '刪除公告',
|
||||
'publish_announcements' => '發布公告',
|
||||
'manage_all_announcements' => '管理所有公告',
|
||||
];
|
||||
|
||||
foreach ($permissions as $name => $description) {
|
||||
@@ -63,81 +100,175 @@ class FinancialWorkflowPermissionsSeeder extends Seeder
|
||||
$this->command->info("Permission created: {$name}");
|
||||
}
|
||||
|
||||
// Create roles for financial workflow
|
||||
// ===== 建立基礎角色(原 RoleSeeder) =====
|
||||
$baseRoles = [
|
||||
'admin' => '系統管理員 - 擁有系統所有權限,負責使用者管理、系統設定與維護',
|
||||
'staff' => '工作人員 - 一般協會工作人員,可檢視文件與協助行政事務',
|
||||
];
|
||||
|
||||
foreach ($baseRoles as $roleName => $description) {
|
||||
Role::updateOrCreate(
|
||||
['name' => $roleName, 'guard_name' => 'web'],
|
||||
['description' => $description]
|
||||
);
|
||||
$this->command->info("Base role created: {$roleName}");
|
||||
}
|
||||
|
||||
// ===== 建立財務與會員管理角色 =====
|
||||
$roles = [
|
||||
'secretary_general' => [
|
||||
'permissions' => [
|
||||
// 財務申請單審核(新工作流程第一階段)
|
||||
'approve_finance_secretary',
|
||||
// 一般
|
||||
'view_finance_documents',
|
||||
'view_finance_dashboard',
|
||||
'view_finance_reports',
|
||||
// 公告系統
|
||||
'view_announcements',
|
||||
'create_announcements',
|
||||
'edit_announcements',
|
||||
'delete_announcements',
|
||||
'publish_announcements',
|
||||
'manage_all_announcements',
|
||||
],
|
||||
'description' => '秘書長 - 協會行政負責人,負責初審所有財務申請',
|
||||
],
|
||||
'finance_cashier' => [
|
||||
'permissions' => [
|
||||
// Approval stage
|
||||
// 會員繳費審核(原 payment_cashier)
|
||||
'verify_payments_cashier',
|
||||
'view_payment_verifications',
|
||||
// 財務申請單審核(舊流程,保留)
|
||||
'approve_finance_cashier',
|
||||
// Payment stage
|
||||
// 出帳確認(新工作流程)
|
||||
'confirm_disbursement_cashier',
|
||||
// 收入管理
|
||||
'view_incomes',
|
||||
'record_income',
|
||||
// 付款階段
|
||||
'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',
|
||||
// 公告系統
|
||||
'view_announcements',
|
||||
'create_announcements',
|
||||
'edit_announcements',
|
||||
'delete_announcements',
|
||||
'publish_announcements',
|
||||
],
|
||||
'description' => '出納 - 管錢(覆核付款單、執行付款、記錄現金簿、製作銀行調節表)',
|
||||
'description' => '出納 - 負責現金收付、銀行調節表製作、出帳確認、記錄收入',
|
||||
],
|
||||
'finance_accountant' => [
|
||||
'permissions' => [
|
||||
// Approval stage
|
||||
// 會員繳費審核(原 payment_accountant)
|
||||
'verify_payments_accountant',
|
||||
'view_payment_verifications',
|
||||
// 財務申請單審核(舊流程,保留)
|
||||
'approve_finance_accountant',
|
||||
// Payment stage
|
||||
// 入帳確認(新工作流程)
|
||||
'confirm_recording_accountant',
|
||||
// 收入管理
|
||||
'view_incomes',
|
||||
'confirm_income',
|
||||
'cancel_income',
|
||||
'export_incomes',
|
||||
'view_income_statistics',
|
||||
// 付款階段
|
||||
'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',
|
||||
// 公告系統
|
||||
'view_announcements',
|
||||
'create_announcements',
|
||||
'edit_announcements',
|
||||
'delete_announcements',
|
||||
'publish_announcements',
|
||||
],
|
||||
'description' => '會計 - 管帳(製作付款單、記錄會計分錄、覆核銀行調節表、指定會計科目)',
|
||||
'description' => '會計 - 負責會計傳票製作、財務報表編製、入帳確認、確認收入',
|
||||
],
|
||||
'finance_chair' => [
|
||||
'permissions' => [
|
||||
// Approval stage
|
||||
// 會員繳費審核(原 payment_chair)
|
||||
'verify_payments_chair',
|
||||
'view_payment_verifications',
|
||||
// 財務申請單審核
|
||||
'approve_finance_chair',
|
||||
// Reconciliation
|
||||
// 銀行調節
|
||||
'approve_bank_reconciliation',
|
||||
// General
|
||||
// 一般
|
||||
'view_finance_documents',
|
||||
'view_finance_dashboard',
|
||||
'view_finance_reports',
|
||||
'export_finance_reports',
|
||||
// 公告系統
|
||||
'view_announcements',
|
||||
'create_announcements',
|
||||
'edit_announcements',
|
||||
'delete_announcements',
|
||||
'publish_announcements',
|
||||
'manage_all_announcements',
|
||||
],
|
||||
'description' => '理事長 - 審核中大額財務申請、核准銀行調節表',
|
||||
'description' => '理事長 - 協會負責人,負責核決重大財務支出與會員繳費最終審核',
|
||||
],
|
||||
'finance_board_member' => [
|
||||
'permissions' => [
|
||||
// Approval stage (for large amounts)
|
||||
// 大額審核
|
||||
'approve_finance_board',
|
||||
// General
|
||||
// 一般
|
||||
'view_finance_documents',
|
||||
'view_finance_dashboard',
|
||||
'view_finance_reports',
|
||||
// 公告系統
|
||||
'view_announcements',
|
||||
'create_announcements',
|
||||
'edit_announcements',
|
||||
'delete_announcements',
|
||||
'publish_announcements',
|
||||
],
|
||||
'description' => '理事 - 審核大額財務申請(大於50,000)',
|
||||
'description' => '理事 - 理事會成員,協助監督協會運作與審核特定議案',
|
||||
],
|
||||
'finance_requester' => [
|
||||
'permissions' => [
|
||||
'view_finance_documents',
|
||||
'create_finance_documents',
|
||||
'edit_finance_documents',
|
||||
// 出帳確認(新工作流程)
|
||||
'confirm_disbursement_requester',
|
||||
],
|
||||
'description' => '財務申請人 - 可建立和編輯自己的財務申請單',
|
||||
'description' => '財務申請人 - 一般有權申請款項之人員(如活動負責人),可確認領款',
|
||||
],
|
||||
'membership_manager' => [
|
||||
'permissions' => [
|
||||
'activate_memberships',
|
||||
'view_payment_verifications',
|
||||
// 公告系統
|
||||
'view_announcements',
|
||||
'create_announcements',
|
||||
'edit_announcements',
|
||||
'delete_announcements',
|
||||
'publish_announcements',
|
||||
],
|
||||
'description' => '會員管理員 - 專責處理會員入會審核、資料維護與會籍管理',
|
||||
],
|
||||
];
|
||||
|
||||
@@ -153,24 +284,32 @@ class FinancialWorkflowPermissionsSeeder extends Seeder
|
||||
$this->command->info("Role created: {$roleName} with permissions: " . implode(', ', $roleData['permissions']));
|
||||
}
|
||||
|
||||
// Assign all financial workflow permissions to admin role (if exists)
|
||||
// Assign all permissions to admin role
|
||||
$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("Admin role updated with all 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");
|
||||
$this->command->info("\n=== 統一角色系統建立完成 ===");
|
||||
$this->command->info("基礎角色:");
|
||||
$this->command->info(" - admin - 系統管理員");
|
||||
$this->command->info(" - staff - 工作人員");
|
||||
$this->command->info("\n財務角色:");
|
||||
$this->command->info(" - secretary_general - 秘書長(新增:財務申請初審)");
|
||||
$this->command->info(" - finance_cashier - 出納(出帳確認)");
|
||||
$this->command->info(" - finance_accountant - 會計(入帳確認)");
|
||||
$this->command->info(" - finance_chair - 理事長(中額以上審核)");
|
||||
$this->command->info(" - finance_board_member - 理事(大額審核)");
|
||||
$this->command->info(" - finance_requester - 財務申請人(可確認領款)");
|
||||
$this->command->info("\n會員管理角色:");
|
||||
$this->command->info(" - membership_manager - 會員管理員");
|
||||
$this->command->info("\n新財務申請審核工作流程:");
|
||||
$this->command->info(" 審核階段:");
|
||||
$this->command->info(" 小額 (< 5,000): secretary_general");
|
||||
$this->command->info(" 中額 (5,000-50,000): secretary_general → finance_chair");
|
||||
$this->command->info(" 大額 (> 50,000): secretary_general → finance_chair → finance_board_member");
|
||||
$this->command->info(" 出帳階段: finance_requester(申請人確認) + finance_cashier(出納確認)");
|
||||
$this->command->info(" 入帳階段: finance_accountant(會計入帳)");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
<?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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<?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]
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,7 @@ class TestDataSeeder extends Seeder
|
||||
|
||||
// Ensure required seeders have run
|
||||
$this->call([
|
||||
RoleSeeder::class,
|
||||
PaymentVerificationRolesSeeder::class,
|
||||
FinancialWorkflowPermissionsSeeder::class,
|
||||
ChartOfAccountSeeder::class,
|
||||
IssueLabelSeeder::class,
|
||||
]);
|
||||
@@ -86,62 +85,68 @@ class TestDataSeeder extends Seeder
|
||||
$users = [];
|
||||
|
||||
// 1. Super Admin
|
||||
$admin = User::create([
|
||||
'name' => 'Admin User',
|
||||
'email' => 'admin@test.com',
|
||||
'password' => Hash::make('password'),
|
||||
'is_admin' => true,
|
||||
]);
|
||||
$admin = User::firstOrCreate(
|
||||
['email' => 'admin@test.com'],
|
||||
[
|
||||
'name' => 'Admin User',
|
||||
'password' => Hash::make('password'),
|
||||
]
|
||||
);
|
||||
$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');
|
||||
// 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. Payment Accountant
|
||||
$accountant = User::create([
|
||||
'name' => 'Accountant User',
|
||||
'email' => 'accountant@test.com',
|
||||
'password' => Hash::make('password'),
|
||||
'is_admin' => true,
|
||||
]);
|
||||
$accountant->assignRole('payment_accountant');
|
||||
// 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. Payment Chair
|
||||
$chair = User::create([
|
||||
'name' => 'Chair User',
|
||||
'email' => 'chair@test.com',
|
||||
'password' => Hash::make('password'),
|
||||
'is_admin' => true,
|
||||
]);
|
||||
$chair->assignRole('payment_chair');
|
||||
// 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::create([
|
||||
'name' => 'Membership Manager',
|
||||
'email' => 'manager@test.com',
|
||||
'password' => Hash::make('password'),
|
||||
'is_admin' => true,
|
||||
]);
|
||||
$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::create([
|
||||
'name' => 'Regular Member',
|
||||
'email' => 'member@test.com',
|
||||
'password' => Hash::make('password'),
|
||||
'is_admin' => false,
|
||||
]);
|
||||
$member = User::firstOrCreate(
|
||||
['email' => 'member@test.com'],
|
||||
[
|
||||
'name' => 'Regular Member',
|
||||
'password' => Hash::make('password'),
|
||||
]
|
||||
);
|
||||
$users['member'] = $member;
|
||||
|
||||
return $users;
|
||||
@@ -158,97 +163,107 @@ class TestDataSeeder extends Seeder
|
||||
|
||||
// 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)),
|
||||
]);
|
||||
$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::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)),
|
||||
]);
|
||||
$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::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)),
|
||||
]);
|
||||
$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::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)),
|
||||
]);
|
||||
$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::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,
|
||||
]);
|
||||
$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++;
|
||||
}
|
||||
|
||||
@@ -264,38 +279,41 @@ class TestDataSeeder extends Seeder
|
||||
$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' => '待審核的繳費記錄',
|
||||
]);
|
||||
$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::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' => '已通過出納審核',
|
||||
]);
|
||||
$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
|
||||
@@ -303,22 +321,24 @@ class TestDataSeeder extends Seeder
|
||||
$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' => '已通過會計審核',
|
||||
]);
|
||||
$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)
|
||||
@@ -327,42 +347,46 @@ class TestDataSeeder extends Seeder
|
||||
$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' => '已完成三階段審核',
|
||||
]);
|
||||
$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::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' => '已退回',
|
||||
]);
|
||||
$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;
|
||||
@@ -747,12 +771,12 @@ class TestDataSeeder extends Seeder
|
||||
$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'],
|
||||
['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('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
|
||||
Reference in New Issue
Block a user