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,120 @@
<?php
namespace Tests\Feature\Validation;
use App\Http\Middleware\VerifyCsrfToken;
use App\Models\Member;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Tests\TestCase;
use Tests\Traits\CreatesMemberData;
use Tests\Traits\SeedsRolesAndPermissions;
/**
* Member Validation Tests
*
* Tests validation rules for member registration and model behavior.
*/
class MemberValidationTest extends TestCase
{
use RefreshDatabase, SeedsRolesAndPermissions, CreatesMemberData;
protected function setUp(): void
{
parent::setUp();
Mail::fake();
$this->seedRolesAndPermissions();
}
public function test_member_can_be_created(): void
{
$member = $this->createMember(['full_name' => 'Test Member']);
$this->assertDatabaseHas('members', ['full_name' => 'Test Member']);
}
public function test_member_defaults_to_pending_status(): void
{
$member = $this->createPendingMember();
$this->assertEquals(Member::STATUS_PENDING, $member->membership_status);
}
public function test_member_can_be_activated(): void
{
$member = $this->createActiveMember();
$this->assertEquals(Member::STATUS_ACTIVE, $member->membership_status);
}
public function test_member_can_be_expired(): void
{
$member = $this->createExpiredMember();
$this->assertEquals(Member::STATUS_EXPIRED, $member->membership_status);
}
public function test_member_has_user_relationship(): void
{
$member = $this->createMember();
$this->assertNotNull($member->user);
$this->assertInstanceOf(User::class, $member->user);
}
public function test_email_uniqueness_in_users_table(): void
{
User::factory()->create(['email' => 'existing@example.com']);
$data = $this->getValidMemberRegistrationData(['email' => 'existing@example.com']);
$response = $this->withoutMiddleware(VerifyCsrfToken::class)
->post(route('register.member.store'), $data);
$response->assertSessionHasErrors('email');
}
public function test_email_uniqueness_in_members_table(): void
{
$existingMember = $this->createMember(['email' => 'member@example.com']);
$data = $this->getValidMemberRegistrationData(['email' => 'member@example.com']);
$response = $this->withoutMiddleware(VerifyCsrfToken::class)
->post(route('register.member.store'), $data);
$response->assertSessionHasErrors('email');
}
public function test_membership_payment_can_be_created(): void
{
$data = $this->createMemberWithPendingPayment();
$this->assertNotNull($data['payment']);
$this->assertEquals($data['member']->id, $data['payment']->member_id);
}
public function test_members_have_unique_ids(): void
{
$member1 = $this->createMember();
$member2 = $this->createMember();
$this->assertNotEquals($member1->id, $member2->id);
}
public function test_active_member_has_paid_membership(): void
{
$member = $this->createActiveMember([
'membership_started_at' => now()->subMonth(),
'membership_expires_at' => now()->addYear(),
]);
$this->assertTrue($member->hasPaidMembership());
}
public function test_pending_member_does_not_have_paid_membership(): void
{
$member = $this->createPendingMember();
$this->assertFalse($member->hasPaidMembership());
}
}