Add phone login support and member import functionality

Features:
- Support login via phone number or email (LoginRequest)
- Add members:import-roster command for Excel roster import
- Merge survey emails with roster data

Code Quality (Phase 1-4):
- Add database locking for balance calculation
- Add self-approval checks for finance workflow
- Create service layer (FinanceDocumentApprovalService, PaymentVerificationService)
- Add HasAccountingEntries and HasApprovalWorkflow traits
- Create FormRequest classes for validation
- Add status-badge component
- Define authorization gates in AuthServiceProvider
- Add accounting config file

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-25 03:08:06 +08:00
parent ed7169b64e
commit 42099759e8
66 changed files with 3492 additions and 3803 deletions

View File

@@ -20,15 +20,11 @@ class FinanceDocumentFactory extends Factory
*/
public function definition(): array
{
$requestTypes = ['expense_reimbursement', 'advance_payment', 'purchase_request', 'petty_cash'];
$statuses = ['pending', 'approved_cashier', 'approved_accountant', 'approved_chair', 'rejected'];
return [
'title' => $this->faker->sentence(6),
'description' => $this->faker->paragraph(3),
'amount' => $this->faker->randomFloat(2, 100, 100000),
'request_type' => $this->faker->randomElement($requestTypes),
'status' => $this->faker->randomElement($statuses),
'status' => FinanceDocument::STATUS_PENDING,
'submitted_by_user_id' => User::factory(),
'submitted_at' => now(),
'amount_tier' => null,
@@ -54,52 +50,52 @@ class FinanceDocumentFactory extends Factory
public function pending(): static
{
return $this->state(fn (array $attributes) => [
'status' => 'pending',
'status' => FinanceDocument::STATUS_PENDING,
]);
}
/**
* Indicate that the document is approved by cashier.
* Indicate that the document is approved by secretary (first stage).
*/
public function approvedByCashier(): static
public function approvedBySecretary(): static
{
return $this->state(fn (array $attributes) => [
'status' => FinanceDocument::STATUS_APPROVED_CASHIER,
'approved_by_cashier_id' => User::factory(),
'cashier_approved_at' => now(),
'status' => FinanceDocument::STATUS_APPROVED_SECRETARY,
'approved_by_secretary_id' => User::factory(),
'secretary_approved_at' => now(),
]);
}
/**
* Indicate that the document is approved by accountant.
*/
public function approvedByAccountant(): static
{
return $this->state(fn (array $attributes) => [
'status' => FinanceDocument::STATUS_APPROVED_ACCOUNTANT,
'approved_by_cashier_id' => User::factory(),
'cashier_approved_at' => now(),
'approved_by_accountant_id' => User::factory(),
'accountant_approved_at' => now(),
]);
}
/**
* Indicate that the document is approved by chair.
* Indicate that the document is approved by chair (second stage).
*/
public function approvedByChair(): static
{
return $this->state(fn (array $attributes) => [
'status' => FinanceDocument::STATUS_APPROVED_CHAIR,
'approved_by_cashier_id' => User::factory(),
'cashier_approved_at' => now(),
'approved_by_accountant_id' => User::factory(),
'accountant_approved_at' => now(),
'approved_by_secretary_id' => User::factory(),
'secretary_approved_at' => now(),
'approved_by_chair_id' => User::factory(),
'chair_approved_at' => now(),
]);
}
/**
* Indicate that the document is approved by board (third stage - large amounts).
*/
public function approvedByBoard(): static
{
return $this->state(fn (array $attributes) => [
'status' => FinanceDocument::STATUS_APPROVED_BOARD,
'approved_by_secretary_id' => User::factory(),
'secretary_approved_at' => now(),
'approved_by_chair_id' => User::factory(),
'chair_approved_at' => now(),
'board_meeting_approved_by_id' => User::factory(),
'board_meeting_approved_at' => now(),
]);
}
/**
* Indicate that the document is rejected.
*/
@@ -119,7 +115,7 @@ class FinanceDocumentFactory extends Factory
{
return $this->state(fn (array $attributes) => [
'amount' => $this->faker->randomFloat(2, 100, 4999),
'amount_tier' => 'small',
'amount_tier' => FinanceDocument::AMOUNT_TIER_SMALL,
'requires_board_meeting' => false,
]);
}
@@ -131,7 +127,7 @@ class FinanceDocumentFactory extends Factory
{
return $this->state(fn (array $attributes) => [
'amount' => $this->faker->randomFloat(2, 5000, 50000),
'amount_tier' => null,
'amount_tier' => FinanceDocument::AMOUNT_TIER_MEDIUM,
'requires_board_meeting' => false,
]);
}
@@ -143,51 +139,11 @@ class FinanceDocumentFactory extends Factory
{
return $this->state(fn (array $attributes) => [
'amount' => $this->faker->randomFloat(2, 50001, 200000),
'amount_tier' => 'large',
'amount_tier' => FinanceDocument::AMOUNT_TIER_LARGE,
'requires_board_meeting' => true,
]);
}
/**
* Indicate that the document is an expense reimbursement.
*/
public function expenseReimbursement(): static
{
return $this->state(fn (array $attributes) => [
'request_type' => 'expense_reimbursement',
]);
}
/**
* Indicate that the document is an advance payment.
*/
public function advancePayment(): static
{
return $this->state(fn (array $attributes) => [
'request_type' => 'advance_payment',
]);
}
/**
* Indicate that the document is a purchase request.
*/
public function purchaseRequest(): static
{
return $this->state(fn (array $attributes) => [
'request_type' => 'purchase_request',
]);
}
/**
* Indicate that the document is petty cash.
*/
public function pettyCash(): static
{
return $this->state(fn (array $attributes) => [
'request_type' => 'petty_cash',
]);
}
/**
* Indicate that payment order has been created.
*/
@@ -195,7 +151,7 @@ class FinanceDocumentFactory extends Factory
{
return $this->state(fn (array $attributes) => [
'payment_order_created_at' => now(),
'payment_order_created_by_id' => User::factory(),
'payment_order_created_by_accountant_id' => User::factory(),
]);
}
@@ -212,16 +168,38 @@ class FinanceDocumentFactory extends Factory
}
/**
* Determine amount tier based on amount.
* Indicate that disbursement is complete (both confirmations).
*/
protected function determineAmountTier(float $amount): string
public function disbursementComplete(): static
{
if ($amount < 5000) {
return 'small';
} elseif ($amount <= 50000) {
return 'medium';
} else {
return 'large';
}
return $this->state(fn (array $attributes) => [
'requester_confirmed_at' => now(),
'requester_confirmed_by_id' => User::factory(),
'cashier_confirmed_at' => now(),
'cashier_confirmed_by_id' => User::factory(),
'disbursement_status' => FinanceDocument::DISBURSEMENT_COMPLETED,
]);
}
/**
* Indicate that recording is complete.
*/
public function recordingComplete(): static
{
return $this->state(fn (array $attributes) => [
'accountant_recorded_at' => now(),
'accountant_recorded_by_id' => User::factory(),
'recording_status' => FinanceDocument::RECORDING_COMPLETED,
]);
}
/**
* Create a fully processed document (all stages complete).
*/
public function fullyProcessed(): static
{
return $this->approvedBySecretary()
->disbursementComplete()
->recordingComplete();
}
}

View File

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

View File

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

View File

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

View File

@@ -1,29 +0,0 @@
<?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('audit_logs', function (Blueprint $table) {
$table->text('description')->nullable()->after('action');
$table->string('ip_address', 45)->nullable()->after('metadata');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('audit_logs', function (Blueprint $table) {
$table->dropColumn(['description', 'ip_address']);
});
}
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
CREATE TABLE IF NOT EXISTS "migrations" ("id" integer primary key autoincrement not null, "migration" varchar not null, "batch" integer not null);
CREATE TABLE IF NOT EXISTS "users" ("id" integer primary key autoincrement not null, "name" varchar not null, "email" varchar not null, "email_verified_at" datetime, "password" varchar not null, "remember_token" varchar, "created_at" datetime, "updated_at" datetime, "is_admin" tinyint(1) not null default '0', "profile_photo_path" varchar);
CREATE TABLE IF NOT EXISTS "users" ("id" integer primary key autoincrement not null, "name" varchar not null, "email" varchar not null, "email_verified_at" datetime, "password" varchar not null, "remember_token" varchar, "created_at" datetime, "updated_at" datetime, "profile_photo_path" varchar);
CREATE UNIQUE INDEX "users_email_unique" on "users" ("email");
CREATE TABLE IF NOT EXISTS "password_reset_tokens" ("email" varchar not null, "token" varchar not null, "created_at" datetime, primary key ("email"));
CREATE TABLE IF NOT EXISTS "failed_jobs" ("id" integer primary key autoincrement not null, "uuid" varchar not null, "connection" text not null, "queue" text not null, "payload" text not null, "exception" text not null, "failed_at" datetime not null default CURRENT_TIMESTAMP);
@@ -27,10 +27,10 @@ CREATE INDEX "document_access_logs_document_id_index" on "document_access_logs"
CREATE INDEX "document_access_logs_user_id_index" on "document_access_logs" ("user_id");
CREATE INDEX "document_access_logs_action_index" on "document_access_logs" ("action");
CREATE INDEX "document_access_logs_accessed_at_index" on "document_access_logs" ("accessed_at");
CREATE TABLE IF NOT EXISTS "members" ("id" integer primary key autoincrement not null, "user_id" integer, "full_name" varchar not null, "email" varchar not null, "phone" varchar, "national_id_encrypted" varchar, "national_id_hash" varchar, "membership_started_at" date, "membership_expires_at" date, "created_at" datetime, "updated_at" datetime, "last_expiry_reminder_sent_at" datetime, "address_line_1" varchar, "address_line_2" varchar, "city" varchar, "postal_code" varchar, "emergency_contact_name" varchar, "emergency_contact_phone" varchar, "membership_status" varchar check ("membership_status" in ('pending', 'active', 'expired', 'suspended')) not null default 'pending', "membership_type" varchar check ("membership_type" in ('regular', 'honorary', 'lifetime', 'student')) not null default 'regular', foreign key("user_id") references "users"("id") on delete set null);
CREATE TABLE IF NOT EXISTS "members" ("id" integer primary key autoincrement not null, "user_id" integer, "full_name" varchar not null, "email" varchar not null, "phone" varchar, "national_id_encrypted" varchar, "national_id_hash" varchar, "membership_started_at" date, "membership_expires_at" date, "created_at" datetime, "updated_at" datetime, "last_expiry_reminder_sent_at" datetime, "address_line_1" varchar, "address_line_2" varchar, "city" varchar, "postal_code" varchar, "emergency_contact_name" varchar, "emergency_contact_phone" varchar, "membership_status" varchar check ("membership_status" in ('pending', 'active', 'expired', 'suspended')) not null default 'pending', "membership_type" varchar check ("membership_type" in ('regular', 'honorary', 'lifetime', 'student')) not null default 'regular', "disability_certificate_path" varchar, "disability_certificate_status" varchar, "disability_verified_by" integer, "disability_verified_at" datetime, "disability_rejection_reason" text, foreign key("user_id") references "users"("id") on delete set null);
CREATE INDEX "members_email_index" on "members" ("email");
CREATE INDEX "members_national_id_hash_index" on "members" ("national_id_hash");
CREATE TABLE IF NOT EXISTS "membership_payments" ("id" integer primary key autoincrement not null, "member_id" integer not null, "paid_at" date not null, "amount" numeric not null, "method" varchar, "reference" varchar, "created_at" datetime, "updated_at" datetime, "status" varchar check ("status" in ('pending', 'approved_cashier', 'approved_accountant', 'approved_chair', 'rejected')) not null default 'pending', "payment_method" varchar check ("payment_method" in ('bank_transfer', 'convenience_store', 'cash', 'credit_card')), "receipt_path" varchar, "submitted_by_user_id" integer, "verified_by_cashier_id" integer, "cashier_verified_at" datetime, "verified_by_accountant_id" integer, "accountant_verified_at" datetime, "verified_by_chair_id" integer, "chair_verified_at" datetime, "rejected_by_user_id" integer, "rejected_at" datetime, "rejection_reason" text, "notes" text, foreign key("member_id") references "members"("id") on delete cascade);
CREATE TABLE IF NOT EXISTS "membership_payments" ("id" integer primary key autoincrement not null, "member_id" integer not null, "paid_at" date not null, "amount" numeric not null, "method" varchar, "reference" varchar, "created_at" datetime, "updated_at" datetime, "status" varchar check ("status" in ('pending', 'approved_cashier', 'approved_accountant', 'approved_chair', 'rejected')) not null default 'pending', "payment_method" varchar check ("payment_method" in ('bank_transfer', 'convenience_store', 'cash', 'credit_card')), "receipt_path" varchar, "submitted_by_user_id" integer, "verified_by_cashier_id" integer, "cashier_verified_at" datetime, "verified_by_accountant_id" integer, "accountant_verified_at" datetime, "verified_by_chair_id" integer, "chair_verified_at" datetime, "rejected_by_user_id" integer, "rejected_at" datetime, "rejection_reason" text, "notes" text, "fee_type" varchar not null default 'entrance_fee', "base_amount" numeric, "discount_amount" numeric not null default '0', "final_amount" numeric, "disability_discount" tinyint(1) not null default '0', foreign key("member_id") references "members"("id") on delete cascade);
CREATE TABLE IF NOT EXISTS "permissions" ("id" integer primary key autoincrement not null, "name" varchar not null, "guard_name" varchar not null, "created_at" datetime, "updated_at" datetime);
CREATE UNIQUE INDEX "permissions_name_guard_name_unique" on "permissions" ("name", "guard_name");
CREATE TABLE IF NOT EXISTS "roles" ("id" integer primary key autoincrement not null, "name" varchar not null, "guard_name" varchar not null, "created_at" datetime, "updated_at" datetime, "description" varchar);
@@ -40,8 +40,8 @@ CREATE INDEX "model_has_permissions_model_id_model_type_index" on "model_has_per
CREATE TABLE IF NOT EXISTS "model_has_roles" ("role_id" integer not null, "model_type" varchar not null, "model_id" integer not null, foreign key("role_id") references "roles"("id") on delete cascade, primary key ("role_id", "model_id", "model_type"));
CREATE INDEX "model_has_roles_model_id_model_type_index" on "model_has_roles" ("model_id", "model_type");
CREATE TABLE IF NOT EXISTS "role_has_permissions" ("permission_id" integer not null, "role_id" integer not null, foreign key("permission_id") references "permissions"("id") on delete cascade, foreign key("role_id") references "roles"("id") on delete cascade, primary key ("permission_id", "role_id"));
CREATE TABLE IF NOT EXISTS "audit_logs" ("id" integer primary key autoincrement not null, "user_id" integer, "action" varchar not null, "auditable_type" varchar, "auditable_id" integer, "metadata" text, "created_at" datetime, "updated_at" datetime, foreign key("user_id") references "users"("id") on delete set null);
CREATE TABLE IF NOT EXISTS "finance_documents" ("id" integer primary key autoincrement not null, "member_id" integer, "submitted_by_user_id" integer, "title" varchar not null, "amount" numeric, "status" varchar not null default 'pending', "description" text, "submitted_at" datetime, "created_at" datetime, "updated_at" datetime, "attachment_path" varchar, "approved_by_cashier_id" integer, "cashier_approved_at" datetime, "approved_by_accountant_id" integer, "accountant_approved_at" datetime, "approved_by_chair_id" integer, "chair_approved_at" datetime, "rejected_by_user_id" integer, "rejected_at" datetime, "rejection_reason" text, "submitted_by_id" integer, "request_type" varchar check ("request_type" in ('expense_reimbursement', 'advance_payment', 'purchase_request', 'petty_cash')) not null default 'expense_reimbursement', "amount_tier" varchar check ("amount_tier" in ('small', 'medium', 'large')), "chart_of_account_id" integer, "budget_item_id" integer, "requires_board_meeting" tinyint(1) not null default '0', "board_meeting_date" date, "board_meeting_decision" text, "approved_by_board_meeting_id" integer, "board_meeting_approved_at" datetime, "payment_order_created_by_accountant_id" integer, "payment_order_created_at" datetime, "payment_method" varchar check ("payment_method" in ('bank_transfer', 'check', 'cash')), "payee_name" varchar, "payee_bank_code" varchar, "payee_account_number" varchar, "payee_bank_name" varchar, "payment_notes" text, "payment_verified_by_cashier_id" integer, "payment_verified_at" datetime, "payment_verification_notes" text, "payment_executed_by_cashier_id" integer, "payment_executed_at" datetime, "payment_transaction_id" varchar, "payment_receipt_path" varchar, "actual_payment_amount" numeric, "cashier_ledger_entry_id" integer, "cashier_recorded_at" datetime, "accounting_transaction_id" integer, "accountant_recorded_at" datetime, "bank_reconciliation_id" integer, "reconciliation_status" varchar check ("reconciliation_status" in ('pending', 'matched', 'discrepancy', 'resolved')) not null default 'pending', "reconciliation_notes" text, "reconciled_at" datetime, "reconciled_by_user_id" integer, foreign key("member_id") references "members"("id") on delete set null, foreign key("submitted_by_user_id") references "users"("id") on delete set null);
CREATE TABLE IF NOT EXISTS "audit_logs" ("id" integer primary key autoincrement not null, "user_id" integer, "action" varchar not null, "auditable_type" varchar, "auditable_id" integer, "metadata" text, "created_at" datetime, "updated_at" datetime, "description" text, "ip_address" varchar, foreign key("user_id") references "users"("id") on delete set null);
CREATE TABLE IF NOT EXISTS "finance_documents" ("id" integer primary key autoincrement not null, "member_id" integer, "submitted_by_user_id" integer, "title" varchar not null, "amount" numeric, "status" varchar not null default 'pending', "description" text, "submitted_at" datetime, "created_at" datetime, "updated_at" datetime, "attachment_path" varchar, "approved_by_cashier_id" integer, "cashier_approved_at" datetime, "approved_by_accountant_id" integer, "accountant_approved_at" datetime, "approved_by_chair_id" integer, "chair_approved_at" datetime, "rejected_by_user_id" integer, "rejected_at" datetime, "rejection_reason" text, "submitted_by_id" integer, "amount_tier" varchar check ("amount_tier" in ('small', 'medium', 'large')), "chart_of_account_id" integer, "budget_item_id" integer, "requires_board_meeting" tinyint(1) not null default '0', "board_meeting_date" date, "board_meeting_decision" text, "approved_by_board_meeting_id" integer, "board_meeting_approved_at" datetime, "payment_order_created_by_accountant_id" integer, "payment_order_created_at" datetime, "payment_method" varchar check ("payment_method" in ('bank_transfer', 'check', 'cash')), "payee_name" varchar, "payee_bank_code" varchar, "payee_account_number" varchar, "payee_bank_name" varchar, "payment_notes" text, "payment_verified_by_cashier_id" integer, "payment_verified_at" datetime, "payment_verification_notes" text, "payment_executed_by_cashier_id" integer, "payment_executed_at" datetime, "payment_transaction_id" varchar, "payment_receipt_path" varchar, "actual_payment_amount" numeric, "cashier_ledger_entry_id" integer, "cashier_recorded_at" datetime, "accounting_transaction_id" integer, "accountant_recorded_at" datetime, "bank_reconciliation_id" integer, "reconciliation_status" varchar check ("reconciliation_status" in ('pending', 'matched', 'discrepancy', 'resolved')) not null default 'pending', "reconciliation_notes" text, "reconciled_at" datetime, "reconciled_by_user_id" integer, "approved_by_secretary_id" integer, "secretary_approved_at" datetime, "disbursement_status" varchar, "requester_confirmed_at" datetime, "requester_confirmed_by_id" integer, "cashier_confirmed_at" datetime, "cashier_confirmed_by_id" integer, "recording_status" varchar, "accountant_recorded_by_id" integer, foreign key("member_id") references "members"("id") on delete set null, foreign key("submitted_by_user_id") references "users"("id") on delete set null);
CREATE TABLE IF NOT EXISTS "chart_of_accounts" ("id" integer primary key autoincrement not null, "account_code" varchar not null, "account_name_zh" varchar not null, "account_name_en" varchar, "account_type" varchar check ("account_type" in ('asset', 'liability', 'net_asset', 'income', 'expense')) not null, "category" varchar, "parent_account_id" integer, "is_active" tinyint(1) not null default '1', "display_order" integer not null default '0', "description" text, "created_at" datetime, "updated_at" datetime, foreign key("parent_account_id") references "chart_of_accounts"("id") on delete set null);
CREATE INDEX "chart_of_accounts_account_type_index" on "chart_of_accounts" ("account_type");
CREATE INDEX "chart_of_accounts_is_active_index" on "chart_of_accounts" ("is_active");
@@ -108,6 +108,22 @@ CREATE INDEX "cashier_ledger_entries_recorded_by_cashier_id_index" on "cashier_l
CREATE TABLE IF NOT EXISTS "bank_reconciliations" ("id" integer primary key autoincrement not null, "reconciliation_month" date not null, "bank_statement_balance" numeric not null, "bank_statement_date" date not null, "bank_statement_file_path" varchar, "system_book_balance" numeric not null, "outstanding_checks" text, "deposits_in_transit" text, "bank_charges" text, "adjusted_balance" numeric not null, "discrepancy_amount" numeric not null default '0', "reconciliation_status" varchar check ("reconciliation_status" in ('pending', 'completed', 'discrepancy')) not null default 'pending', "prepared_by_cashier_id" integer not null, "reviewed_by_accountant_id" integer, "approved_by_manager_id" integer, "prepared_at" datetime not null default CURRENT_TIMESTAMP, "reviewed_at" datetime, "approved_at" datetime, "notes" text, "created_at" datetime, "updated_at" datetime, foreign key("prepared_by_cashier_id") references "users"("id") on delete cascade, foreign key("reviewed_by_accountant_id") references "users"("id") on delete set null, foreign key("approved_by_manager_id") references "users"("id") on delete set null);
CREATE INDEX "bank_reconciliations_reconciliation_month_index" on "bank_reconciliations" ("reconciliation_month");
CREATE INDEX "bank_reconciliations_reconciliation_status_index" on "bank_reconciliations" ("reconciliation_status");
CREATE TABLE IF NOT EXISTS "announcements" ("id" integer primary key autoincrement not null, "title" varchar not null, "content" text not null, "status" varchar check ("status" in ('draft', 'published', 'archived')) not null default 'draft', "is_pinned" tinyint(1) not null default '0', "display_order" integer not null default '0', "access_level" varchar check ("access_level" in ('public', 'members', 'board', 'admin')) not null default 'members', "published_at" datetime, "expires_at" datetime, "archived_at" datetime, "view_count" integer not null default '0', "created_by_user_id" integer not null, "last_updated_by_user_id" integer, "created_at" datetime, "updated_at" datetime, "deleted_at" datetime, foreign key("created_by_user_id") references "users"("id") on delete cascade, foreign key("last_updated_by_user_id") references "users"("id") on delete set null);
CREATE INDEX "announcements_status_index" on "announcements" ("status");
CREATE INDEX "announcements_access_level_index" on "announcements" ("access_level");
CREATE INDEX "announcements_published_at_index" on "announcements" ("published_at");
CREATE INDEX "announcements_expires_at_index" on "announcements" ("expires_at");
CREATE INDEX "announcements_is_pinned_display_order_index" on "announcements" ("is_pinned", "display_order");
CREATE TABLE IF NOT EXISTS "accounting_entries" ("id" integer primary key autoincrement not null, "finance_document_id" integer not null, "chart_of_account_id" integer not null, "entry_type" varchar check ("entry_type" in ('debit', 'credit')) not null, "amount" numeric not null, "entry_date" date not null, "description" text, "created_at" datetime, "updated_at" datetime, "income_id" integer, foreign key("finance_document_id") references "finance_documents"("id") on delete cascade, foreign key("chart_of_account_id") references "chart_of_accounts"("id"));
CREATE INDEX "accounting_entries_finance_document_id_entry_type_index" on "accounting_entries" ("finance_document_id", "entry_type");
CREATE INDEX "accounting_entries_chart_of_account_id_entry_date_index" on "accounting_entries" ("chart_of_account_id", "entry_date");
CREATE TABLE IF NOT EXISTS "board_meetings" ("id" integer primary key autoincrement not null, "meeting_date" date not null, "title" varchar not null, "notes" text, "status" varchar check ("status" in ('scheduled', 'completed', 'cancelled')) not null default 'scheduled', "created_at" datetime, "updated_at" datetime);
CREATE TABLE IF NOT EXISTS "incomes" ("id" integer primary key autoincrement not null, "income_number" varchar not null, "title" varchar not null, "description" text, "income_date" date not null, "amount" numeric not null, "income_type" varchar not null, "chart_of_account_id" integer not null, "payment_method" varchar not null, "bank_account" varchar, "payer_name" varchar, "receipt_number" varchar, "transaction_reference" varchar, "attachment_path" varchar, "member_id" integer, "status" varchar not null default 'pending', "recorded_by_cashier_id" integer not null, "recorded_at" datetime not null, "confirmed_by_accountant_id" integer, "confirmed_at" datetime, "cashier_ledger_entry_id" integer, "notes" text, "created_at" datetime, "updated_at" datetime, foreign key("chart_of_account_id") references "chart_of_accounts"("id"), foreign key("member_id") references "members"("id") on delete set null, foreign key("recorded_by_cashier_id") references "users"("id"), foreign key("confirmed_by_accountant_id") references "users"("id"), foreign key("cashier_ledger_entry_id") references "cashier_ledger_entries"("id") on delete set null);
CREATE INDEX "incomes_income_date_index" on "incomes" ("income_date");
CREATE INDEX "incomes_income_type_index" on "incomes" ("income_type");
CREATE INDEX "incomes_status_index" on "incomes" ("status");
CREATE INDEX "incomes_member_id_income_type_index" on "incomes" ("member_id", "income_type");
CREATE UNIQUE INDEX "incomes_income_number_unique" on "incomes" ("income_number");
INSERT INTO migrations VALUES(1,'2014_10_12_000000_create_users_table',1);
INSERT INTO migrations VALUES(2,'2014_10_12_100000_create_password_reset_tokens_table',1);
INSERT INTO migrations VALUES(3,'2019_08_19_000000_create_failed_jobs_table',1);
@@ -154,3 +170,15 @@ INSERT INTO migrations VALUES(43,'2025_11_20_125121_add_payment_stage_fields_to_
INSERT INTO migrations VALUES(44,'2025_11_20_125246_create_payment_orders_table',1);
INSERT INTO migrations VALUES(45,'2025_11_20_125247_create_cashier_ledger_entries_table',1);
INSERT INTO migrations VALUES(46,'2025_11_20_125249_create_bank_reconciliations_table',1);
INSERT INTO migrations VALUES(47,'2025_11_28_182012_remove_is_admin_from_users_table',2);
INSERT INTO migrations VALUES(48,'2025_11_28_231917_create_cashier_ledger_entries_table',3);
INSERT INTO migrations VALUES(49,'2025_11_29_003312_create_announcements_table',4);
INSERT INTO migrations VALUES(50,'2025_11_29_010220_add_description_and_ip_address_to_audit_logs_table',5);
INSERT INTO migrations VALUES(51,'2025_11_30_153609_create_accounting_entries_table',6);
INSERT INTO migrations VALUES(52,'2025_11_30_163310_create_board_meetings_table',7);
INSERT INTO migrations VALUES(53,'2025_11_30_171203_add_new_workflow_fields_to_finance_documents',8);
INSERT INTO migrations VALUES(54,'2025_11_30_175637_remove_request_type_from_finance_documents',9);
INSERT INTO migrations VALUES(55,'2025_11_30_182639_create_incomes_table',10);
INSERT INTO migrations VALUES(56,'2025_11_30_212201_add_disability_fields_to_members_table',11);
INSERT INTO migrations VALUES(57,'2025_11_30_212227_add_fee_type_to_membership_payments_table',11);
INSERT INTO migrations VALUES(58,'2026_01_24_091609_add_secretary_approval_fields_to_finance_documents',12);

View File

@@ -0,0 +1,22 @@
<?php
namespace Database\Seeders;
use Illuminate\Database\Seeder;
/**
* RoleSeeder - Backward compatibility wrapper
*
* This seeder has been consolidated into FinancialWorkflowPermissionsSeeder.
* This class exists for backward compatibility with tests that reference RoleSeeder.
*/
class RoleSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$this->call(FinancialWorkflowPermissionsSeeder::class);
}
}