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,210 @@
<?php
namespace Tests\Feature\Email;
use App\Mail\MembershipActivatedMail;
use App\Mail\PaymentApprovedByAccountantMail;
use App\Mail\PaymentApprovedByCashierMail;
use App\Mail\PaymentFullyApprovedMail;
use App\Mail\PaymentRejectedMail;
use App\Mail\PaymentSubmittedMail;
use App\Mail\WelcomeMemberMail;
use App\Models\Member;
use App\Models\MembershipPayment;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
use Tests\Traits\CreatesMemberData;
use Tests\Traits\SeedsRolesAndPermissions;
/**
* Membership Email Content Tests
*
* Tests email content, recipients, and subjects for membership-related emails.
*/
class MembershipEmailContentTest extends TestCase
{
use RefreshDatabase, SeedsRolesAndPermissions, CreatesMemberData;
protected function setUp(): void
{
parent::setUp();
Storage::fake('private');
$this->seedRolesAndPermissions();
}
/**
* Test welcome email has correct subject
*/
public function test_welcome_email_has_correct_subject(): void
{
$member = $this->createPendingMember();
$mail = new WelcomeMemberMail($member);
$this->assertStringContainsString('歡迎', $mail->envelope()->subject);
}
/**
* Test welcome email contains member name
*/
public function test_welcome_email_contains_member_name(): void
{
$member = $this->createPendingMember(['full_name' => 'Test Member Name']);
$mail = new WelcomeMemberMail($member);
$rendered = $mail->render();
$this->assertStringContainsString('Test Member Name', $rendered);
}
/**
* Test welcome email contains dashboard link
*/
public function test_welcome_email_contains_dashboard_link(): void
{
$member = $this->createPendingMember();
$mail = new WelcomeMemberMail($member);
$rendered = $mail->render();
$this->assertStringContainsString(route('member.dashboard'), $rendered);
}
/**
* Test welcome email sent to correct recipient
*/
public function test_welcome_email_sent_to_correct_recipient(): void
{
Mail::fake();
$data = $this->getValidMemberRegistrationData(['email' => 'newmember@test.com']);
$this->post(route('register.member.store'), $data);
Mail::assertQueued(WelcomeMemberMail::class, function ($mail) {
return $mail->hasTo('newmember@test.com');
});
}
/**
* Test payment submitted email to member
*/
public function test_payment_submitted_email_to_member(): void
{
$data = $this->createMemberWithPendingPayment();
$member = $data['member'];
$payment = $data['payment'];
$mail = new PaymentSubmittedMail($payment, $member->user, 'member');
$rendered = $mail->render();
$this->assertStringContainsString((string) $payment->amount, $rendered);
}
/**
* Test payment submitted email to cashier
*/
public function test_payment_submitted_email_to_cashier(): void
{
$data = $this->createMemberWithPendingPayment();
$member = $data['member'];
$payment = $data['payment'];
$cashier = $this->createCashier();
$mail = new PaymentSubmittedMail($payment, $cashier, 'cashier');
$rendered = $mail->render();
$this->assertStringContainsString($member->full_name, $rendered);
}
/**
* Test payment approved by cashier email
*/
public function test_payment_approved_by_cashier_email(): void
{
$data = $this->createMemberWithPaymentAtStage('cashier_approved');
$payment = $data['payment'];
$mail = new PaymentApprovedByCashierMail($payment);
$this->assertNotNull($mail->envelope()->subject);
$rendered = $mail->render();
$this->assertNotEmpty($rendered);
}
/**
* Test payment approved by accountant email
*/
public function test_payment_approved_by_accountant_email(): void
{
$data = $this->createMemberWithPaymentAtStage('accountant_approved');
$payment = $data['payment'];
$mail = new PaymentApprovedByAccountantMail($payment);
$this->assertNotNull($mail->envelope()->subject);
}
/**
* Test payment fully approved email
*/
public function test_payment_fully_approved_email(): void
{
$data = $this->createMemberWithPaymentAtStage('fully_approved');
$payment = $data['payment'];
$mail = new PaymentFullyApprovedMail($payment);
$rendered = $mail->render();
$this->assertNotEmpty($rendered);
}
/**
* Test payment rejected email contains reason
*/
public function test_payment_rejected_email_contains_reason(): void
{
$data = $this->createMemberWithPendingPayment();
$payment = $data['payment'];
$payment->update([
'status' => MembershipPayment::STATUS_REJECTED,
'rejection_reason' => 'Receipt is not clear',
]);
$mail = new PaymentRejectedMail($payment);
$rendered = $mail->render();
$this->assertStringContainsString('Receipt is not clear', $rendered);
}
/**
* Test membership activated email
*/
public function test_membership_activated_email(): void
{
$member = $this->createActiveMember();
$mail = new MembershipActivatedMail($member);
$rendered = $mail->render();
$this->assertStringContainsString($member->full_name, $rendered);
$this->assertStringContainsString('啟用', $rendered);
}
/**
* Test membership expiry reminder email
* Note: This test is for if the system has expiry reminder functionality
*/
public function test_membership_expiry_reminder_email(): void
{
$member = $this->createActiveMember([
'membership_expires_at' => now()->addDays(30),
]);
// If MembershipExpiryReminderMail exists
// $mail = new MembershipExpiryReminderMail($member);
// $this->assertStringContainsString('到期', $mail->render());
// For now, just verify member expiry date is set
$this->assertTrue($member->membership_expires_at->diffInDays(now()) <= 30);
}
}