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:
192
tests/Feature/EdgeCases/MemberEdgeCasesTest.php
Normal file
192
tests/Feature/EdgeCases/MemberEdgeCasesTest.php
Normal file
@@ -0,0 +1,192 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\EdgeCases;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
use Tests\Traits\CreatesMemberData;
|
||||
use Tests\Traits\SeedsRolesAndPermissions;
|
||||
|
||||
/**
|
||||
* Member Edge Cases Tests
|
||||
*
|
||||
* Tests boundary values and edge cases for member operations.
|
||||
*/
|
||||
class MemberEdgeCasesTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase, SeedsRolesAndPermissions, CreatesMemberData;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Storage::fake('private');
|
||||
$this->seedRolesAndPermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Test membership expiry on boundary date
|
||||
*/
|
||||
public function test_membership_expiry_on_boundary_date(): void
|
||||
{
|
||||
// Member expires today
|
||||
$memberExpiringToday = $this->createMember([
|
||||
'membership_status' => Member::STATUS_ACTIVE,
|
||||
'membership_started_at' => now()->subYear(),
|
||||
'membership_expires_at' => now()->endOfDay(),
|
||||
]);
|
||||
|
||||
// Should still be considered active if expires at end of today
|
||||
$this->assertTrue(
|
||||
$memberExpiringToday->membership_expires_at->isToday() ||
|
||||
$memberExpiringToday->membership_expires_at->isFuture()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test membership renewal before expiry
|
||||
*/
|
||||
public function test_membership_renewal_before_expiry(): void
|
||||
{
|
||||
$member = $this->createActiveMember([
|
||||
'membership_expires_at' => now()->addMonth(),
|
||||
]);
|
||||
|
||||
$originalExpiry = $member->membership_expires_at->copy();
|
||||
|
||||
// Simulate renewal - extend by one year
|
||||
$member->membership_expires_at = $member->membership_expires_at->addYear();
|
||||
$member->save();
|
||||
|
||||
$this->assertTrue($member->membership_expires_at->gt($originalExpiry));
|
||||
$this->assertTrue($member->hasPaidMembership());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test membership renewal after expiry
|
||||
*/
|
||||
public function test_membership_renewal_after_expiry(): void
|
||||
{
|
||||
$member = $this->createExpiredMember();
|
||||
|
||||
$this->assertFalse($member->hasPaidMembership());
|
||||
|
||||
// Reactivate membership
|
||||
$member->membership_status = Member::STATUS_ACTIVE;
|
||||
$member->membership_started_at = now();
|
||||
$member->membership_expires_at = now()->addYear();
|
||||
$member->save();
|
||||
|
||||
$this->assertTrue($member->hasPaidMembership());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test duplicate national ID detection
|
||||
*/
|
||||
public function test_duplicate_national_id_detection(): void
|
||||
{
|
||||
// Create first member with national ID
|
||||
$member1 = $this->createMember(['national_id' => 'A123456789']);
|
||||
|
||||
// Attempt to create second member with same national ID
|
||||
// This should be handled by validation or unique constraint
|
||||
$this->assertDatabaseHas('members', ['id' => $member1->id]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test unicode characters in name
|
||||
*/
|
||||
public function test_unicode_characters_in_name(): void
|
||||
{
|
||||
$member = $this->createMember([
|
||||
'full_name' => '張三李四 王五',
|
||||
]);
|
||||
|
||||
$this->assertEquals('張三李四 王五', $member->full_name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test very long address handling
|
||||
*/
|
||||
public function test_very_long_address_handling(): void
|
||||
{
|
||||
$longAddress = str_repeat('台北市信義區信義路五段7號 ', 10);
|
||||
|
||||
$member = $this->createMember([
|
||||
'address_line_1' => $longAddress,
|
||||
]);
|
||||
|
||||
// Address should be stored (possibly truncated depending on DB column size)
|
||||
$this->assertNotEmpty($member->address_line_1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test special characters in fields
|
||||
*/
|
||||
public function test_special_characters_in_fields(): void
|
||||
{
|
||||
$member = $this->createMember([
|
||||
'full_name' => "O'Connor-Smith",
|
||||
'address_line_1' => '123 Main St. #456 & Co.',
|
||||
]);
|
||||
|
||||
$this->assertEquals("O'Connor-Smith", $member->full_name);
|
||||
$this->assertStringContainsString('&', $member->address_line_1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test null optional fields
|
||||
*/
|
||||
public function test_null_optional_fields(): void
|
||||
{
|
||||
$member = $this->createMember([
|
||||
'address_line_2' => null,
|
||||
'emergency_contact_name' => null,
|
||||
'emergency_contact_phone' => null,
|
||||
]);
|
||||
|
||||
$this->assertNull($member->address_line_2);
|
||||
$this->assertNull($member->emergency_contact_name);
|
||||
$this->assertNull($member->emergency_contact_phone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test concurrent status updates
|
||||
*/
|
||||
public function test_concurrent_status_updates(): void
|
||||
{
|
||||
$member = $this->createPendingMember();
|
||||
|
||||
// Simulate two concurrent updates
|
||||
$member1 = Member::find($member->id);
|
||||
$member2 = Member::find($member->id);
|
||||
|
||||
$member1->membership_status = Member::STATUS_ACTIVE;
|
||||
$member1->save();
|
||||
|
||||
// Second update should still work
|
||||
$member2->refresh();
|
||||
$member2->membership_status = Member::STATUS_SUSPENDED;
|
||||
$member2->save();
|
||||
|
||||
$member->refresh();
|
||||
$this->assertEquals(Member::STATUS_SUSPENDED, $member->membership_status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test orphaned member without user
|
||||
*/
|
||||
public function test_orphaned_member_without_user(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$member = Member::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
// Delete user (cascade should handle this, or soft delete)
|
||||
$userId = $user->id;
|
||||
|
||||
// Check member still exists and relationship handling
|
||||
$this->assertDatabaseHas('members', ['user_id' => $userId]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user