artisan('db:seed', ['--class' => 'RoleSeeder']); } public function test_payment_belongs_to_member(): void { $member = Member::factory()->create(); $payment = MembershipPayment::factory()->create(['member_id' => $member->id]); $this->assertInstanceOf(Member::class, $payment->member); $this->assertEquals($member->id, $payment->member->id); } public function test_payment_belongs_to_submitted_by_user(): void { $user = User::factory()->create(); $payment = MembershipPayment::factory()->create(['submitted_by_user_id' => $user->id]); $this->assertInstanceOf(User::class, $payment->submittedBy); $this->assertEquals($user->id, $payment->submittedBy->id); } public function test_payment_has_verifier_relationships(): void { $cashier = User::factory()->create(); $accountant = User::factory()->create(); $chair = User::factory()->create(); $payment = MembershipPayment::factory()->create([ 'verified_by_cashier_id' => $cashier->id, 'verified_by_accountant_id' => $accountant->id, 'verified_by_chair_id' => $chair->id, ]); $this->assertInstanceOf(User::class, $payment->verifiedByCashier); $this->assertInstanceOf(User::class, $payment->verifiedByAccountant); $this->assertInstanceOf(User::class, $payment->verifiedByChair); $this->assertEquals($cashier->id, $payment->verifiedByCashier->id); $this->assertEquals($accountant->id, $payment->verifiedByAccountant->id); $this->assertEquals($chair->id, $payment->verifiedByChair->id); } public function test_is_pending_returns_true_when_status_is_pending(): void { $payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_PENDING]); $this->assertTrue($payment->isPending()); } public function test_is_approved_by_cashier_returns_true_when_status_is_approved_cashier(): void { $payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_CASHIER]); $this->assertTrue($payment->isApprovedByCashier()); } public function test_is_approved_by_accountant_returns_true_when_status_is_approved_accountant(): void { $payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_ACCOUNTANT]); $this->assertTrue($payment->isApprovedByAccountant()); } public function test_is_fully_approved_returns_true_when_status_is_approved_chair(): void { $payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_CHAIR]); $this->assertTrue($payment->isFullyApproved()); } public function test_is_rejected_returns_true_when_status_is_rejected(): void { $payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_REJECTED]); $this->assertTrue($payment->isRejected()); } public function test_can_be_approved_by_cashier_validates_correctly(): void { $pendingPayment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_PENDING]); $this->assertTrue($pendingPayment->canBeApprovedByCashier()); $approvedPayment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_CASHIER]); $this->assertFalse($approvedPayment->canBeApprovedByCashier()); } public function test_can_be_approved_by_accountant_validates_correctly(): void { $cashierApprovedPayment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_CASHIER]); $this->assertTrue($cashierApprovedPayment->canBeApprovedByAccountant()); $pendingPayment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_PENDING]); $this->assertFalse($pendingPayment->canBeApprovedByAccountant()); } public function test_can_be_approved_by_chair_validates_correctly(): void { $accountantApprovedPayment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_ACCOUNTANT]); $this->assertTrue($accountantApprovedPayment->canBeApprovedByChair()); $cashierApprovedPayment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_CASHIER]); $this->assertFalse($cashierApprovedPayment->canBeApprovedByChair()); } public function test_workflow_validation_prevents_skipping_tiers(): void { $payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_PENDING]); // Cannot skip to accountant approval $this->assertFalse($payment->canBeApprovedByAccountant()); // Cannot skip to chair approval $this->assertFalse($payment->canBeApprovedByChair()); // Must go through cashier first $this->assertTrue($payment->canBeApprovedByCashier()); } public function test_status_label_returns_chinese_text(): void { $payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_PENDING]); $this->assertEquals('待審核', $payment->status_label); $payment->status = MembershipPayment::STATUS_APPROVED_CASHIER; $this->assertEquals('出納已審', $payment->status_label); $payment->status = MembershipPayment::STATUS_APPROVED_ACCOUNTANT; $this->assertEquals('會計已審', $payment->status_label); $payment->status = MembershipPayment::STATUS_APPROVED_CHAIR; $this->assertEquals('主席已審', $payment->status_label); $payment->status = MembershipPayment::STATUS_REJECTED; $this->assertEquals('已拒絕', $payment->status_label); } public function test_payment_method_label_returns_chinese_text(): void { $payment = MembershipPayment::factory()->create(['payment_method' => MembershipPayment::METHOD_BANK_TRANSFER]); $this->assertEquals('銀行轉帳', $payment->payment_method_label); $payment->payment_method = MembershipPayment::METHOD_CONVENIENCE_STORE; $this->assertEquals('超商繳費', $payment->payment_method_label); $payment->payment_method = MembershipPayment::METHOD_CASH; $this->assertEquals('現金', $payment->payment_method_label); $payment->payment_method = MembershipPayment::METHOD_CREDIT_CARD; $this->assertEquals('信用卡', $payment->payment_method_label); } public function test_receipt_file_cleanup_on_deletion(): void { Storage::fake('private'); $payment = MembershipPayment::factory()->create([ 'receipt_path' => 'payment-receipts/test-receipt.pdf' ]); // Create fake file Storage::disk('private')->put('payment-receipts/test-receipt.pdf', 'test content'); $this->assertTrue(Storage::disk('private')->exists('payment-receipts/test-receipt.pdf')); // Delete payment should delete file $payment->delete(); $this->assertFalse(Storage::disk('private')->exists('payment-receipts/test-receipt.pdf')); } public function test_rejection_tracking_works(): void { $rejector = User::factory()->create(); $payment = MembershipPayment::factory()->create([ 'status' => MembershipPayment::STATUS_REJECTED, 'rejected_by_user_id' => $rejector->id, 'rejected_at' => now(), 'rejection_reason' => 'Invalid receipt', ]); $this->assertTrue($payment->isRejected()); $this->assertInstanceOf(User::class, $payment->rejectedBy); $this->assertEquals($rejector->id, $payment->rejectedBy->id); $this->assertEquals('Invalid receipt', $payment->rejection_reason); $this->assertNotNull($payment->rejected_at); } public function test_payment_workflow_complete_sequence(): void { $payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_PENDING]); // Step 1: Pending - can only be approved by cashier $this->assertTrue($payment->canBeApprovedByCashier()); $this->assertFalse($payment->canBeApprovedByAccountant()); $this->assertFalse($payment->canBeApprovedByChair()); // Step 2: Cashier approved - can only be approved by accountant $payment->status = MembershipPayment::STATUS_APPROVED_CASHIER; $this->assertFalse($payment->canBeApprovedByCashier()); $this->assertTrue($payment->canBeApprovedByAccountant()); $this->assertFalse($payment->canBeApprovedByChair()); // Step 3: Accountant approved - can only be approved by chair $payment->status = MembershipPayment::STATUS_APPROVED_ACCOUNTANT; $this->assertFalse($payment->canBeApprovedByCashier()); $this->assertFalse($payment->canBeApprovedByAccountant()); $this->assertTrue($payment->canBeApprovedByChair()); // Step 4: Chair approved - workflow complete $payment->status = MembershipPayment::STATUS_APPROVED_CHAIR; $this->assertFalse($payment->canBeApprovedByCashier()); $this->assertFalse($payment->canBeApprovedByAccountant()); $this->assertFalse($payment->canBeApprovedByChair()); $this->assertTrue($payment->isFullyApproved()); } }