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:
2025-12-01 09:56:01 +08:00
parent 83ce1f7fc8
commit 642b879dd4
207 changed files with 19487 additions and 3048 deletions

View 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(),
],
];
}
}

View 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),
];
}
}

View 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(),
];
}
}

View 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,
];
}
}

View 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']),
];
}
}

View 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,
];
}
}