seedRolesAndPermissions(); } /** * Test complete member registration to activation workflow */ public function test_complete_member_registration_to_activation_workflow(): void { Mail::fake(); // Create approval team $team = $this->createFinanceApprovalTeam(); // Step 1: Member registration $registrationData = $this->getValidMemberRegistrationData(); $response = $this->post(route('register.member.store'), $registrationData); $user = User::where('email', $registrationData['email'])->first(); $this->assertNotNull($user); $member = $user->member; $this->assertNotNull($member); $this->assertEquals(Member::STATUS_PENDING, $member->membership_status); // Step 2: Member submits payment $file = $this->createFakeReceipt(); $this->actingAs($user)->post(route('member.payments.store'), [ 'amount' => 1000, 'paid_at' => now()->format('Y-m-d'), 'payment_method' => MembershipPayment::METHOD_BANK_TRANSFER, 'reference' => 'REF123456', 'receipt' => $file, ]); $payment = MembershipPayment::where('member_id', $member->id)->first(); $this->assertNotNull($payment); $this->assertEquals(MembershipPayment::STATUS_PENDING, $payment->status); // Step 3: Cashier approves $this->actingAs($team['cashier'])->post( route('admin.payment-verifications.approve-cashier', $payment), ['notes' => 'Receipt verified'] ); $payment->refresh(); $this->assertEquals(MembershipPayment::STATUS_APPROVED_CASHIER, $payment->status); // Step 4: Accountant approves $this->actingAs($team['accountant'])->post( route('admin.payment-verifications.approve-accountant', $payment), ['notes' => 'Amount verified'] ); $payment->refresh(); $this->assertEquals(MembershipPayment::STATUS_APPROVED_ACCOUNTANT, $payment->status); // Step 5: Chair approves and activates membership $this->actingAs($team['chair'])->post( route('admin.payment-verifications.approve-chair', $payment), ['notes' => 'Final approval'] ); $payment->refresh(); $member->refresh(); $this->assertEquals(MembershipPayment::STATUS_APPROVED_CHAIR, $payment->status); $this->assertTrue($payment->isFullyApproved()); $this->assertEquals(Member::STATUS_ACTIVE, $member->membership_status); $this->assertNotNull($member->membership_started_at); $this->assertNotNull($member->membership_expires_at); $this->assertTrue($member->hasPaidMembership()); // Verify emails were sent Mail::assertQueued(MembershipActivatedMail::class); Mail::assertQueued(PaymentFullyApprovedMail::class); } /** * Test cashier can approve first tier */ public function test_member_registration_payment_cashier_approval(): void { Mail::fake(); $cashier = $this->createCashier(); $data = $this->createMemberWithPendingPayment(); $response = $this->actingAs($cashier)->post( route('admin.payment-verifications.approve-cashier', $data['payment']), ['notes' => 'Verified'] ); $response->assertRedirect(); $data['payment']->refresh(); $this->assertEquals(MembershipPayment::STATUS_APPROVED_CASHIER, $data['payment']->status); $this->assertEquals($cashier->id, $data['payment']->verified_by_cashier_id); $this->assertNotNull($data['payment']->cashier_verified_at); Mail::assertQueued(PaymentApprovedByCashierMail::class); } /** * Test accountant can approve second tier */ public function test_member_registration_payment_accountant_approval(): void { Mail::fake(); $accountant = $this->createAccountant(); $data = $this->createMemberWithPaymentAtStage('cashier_approved'); $response = $this->actingAs($accountant)->post( route('admin.payment-verifications.approve-accountant', $data['payment']), ['notes' => 'Amount verified'] ); $response->assertRedirect(); $data['payment']->refresh(); $this->assertEquals(MembershipPayment::STATUS_APPROVED_ACCOUNTANT, $data['payment']->status); $this->assertEquals($accountant->id, $data['payment']->verified_by_accountant_id); $this->assertNotNull($data['payment']->accountant_verified_at); Mail::assertQueued(PaymentApprovedByAccountantMail::class); } /** * Test chair can approve third tier and activate membership */ public function test_member_registration_payment_chair_approval_and_activation(): void { Mail::fake(); $chair = $this->createChair(); $data = $this->createMemberWithPaymentAtStage('accountant_approved'); $response = $this->actingAs($chair)->post( route('admin.payment-verifications.approve-chair', $data['payment']), ['notes' => 'Final approval'] ); $response->assertRedirect(); $data['payment']->refresh(); $data['member']->refresh(); $this->assertEquals(MembershipPayment::STATUS_APPROVED_CHAIR, $data['payment']->status); $this->assertTrue($data['payment']->isFullyApproved()); $this->assertEquals(Member::STATUS_ACTIVE, $data['member']->membership_status); $this->assertTrue($data['member']->hasPaidMembership()); Mail::assertQueued(PaymentFullyApprovedMail::class); Mail::assertQueued(MembershipActivatedMail::class); } /** * Test payment rejection at cashier level */ public function test_payment_rejection_at_cashier_level(): void { Mail::fake(); $cashier = $this->createCashier(); $data = $this->createMemberWithPendingPayment(); $response = $this->actingAs($cashier)->post( route('admin.payment-verifications.reject', $data['payment']), ['rejection_reason' => 'Invalid receipt - image is blurry'] ); $response->assertRedirect(); $data['payment']->refresh(); $this->assertEquals(MembershipPayment::STATUS_REJECTED, $data['payment']->status); $this->assertEquals('Invalid receipt - image is blurry', $data['payment']->rejection_reason); $this->assertEquals($cashier->id, $data['payment']->rejected_by_user_id); $this->assertNotNull($data['payment']->rejected_at); Mail::assertQueued(PaymentRejectedMail::class); } /** * Test payment rejection at accountant level */ public function test_payment_rejection_at_accountant_level(): void { Mail::fake(); $accountant = $this->createAccountant(); $data = $this->createMemberWithPaymentAtStage('cashier_approved'); $response = $this->actingAs($accountant)->post( route('admin.payment-verifications.reject', $data['payment']), ['rejection_reason' => 'Amount does not match receipt'] ); $response->assertRedirect(); $data['payment']->refresh(); $this->assertEquals(MembershipPayment::STATUS_REJECTED, $data['payment']->status); $this->assertEquals('Amount does not match receipt', $data['payment']->rejection_reason); $this->assertEquals($accountant->id, $data['payment']->rejected_by_user_id); Mail::assertQueued(PaymentRejectedMail::class); } /** * Test payment rejection at chair level */ public function test_payment_rejection_at_chair_level(): void { Mail::fake(); $chair = $this->createChair(); $data = $this->createMemberWithPaymentAtStage('accountant_approved'); $response = $this->actingAs($chair)->post( route('admin.payment-verifications.reject', $data['payment']), ['rejection_reason' => 'Membership application incomplete'] ); $response->assertRedirect(); $data['payment']->refresh(); $this->assertEquals(MembershipPayment::STATUS_REJECTED, $data['payment']->status); $this->assertEquals($chair->id, $data['payment']->rejected_by_user_id); } /** * Test member can resubmit payment after rejection */ public function test_member_resubmit_payment_after_rejection(): void { Mail::fake(); $data = $this->createMemberWithPendingPayment(); $member = $data['member']; $user = $member->user; // Simulate rejection $data['payment']->update([ 'status' => MembershipPayment::STATUS_REJECTED, 'rejection_reason' => 'Invalid receipt', ]); // Member submits new payment $newReceipt = $this->createFakeReceipt('new_receipt.jpg'); $response = $this->actingAs($user)->post(route('member.payments.store'), [ 'amount' => 1000, 'paid_at' => now()->format('Y-m-d'), 'payment_method' => MembershipPayment::METHOD_BANK_TRANSFER, 'reference' => 'NEWREF123', 'receipt' => $newReceipt, ]); $response->assertRedirect(); // Verify new payment was created $newPayment = MembershipPayment::where('member_id', $member->id) ->where('status', MembershipPayment::STATUS_PENDING) ->latest() ->first(); $this->assertNotNull($newPayment); $this->assertNotEquals($data['payment']->id, $newPayment->id); $this->assertEquals(MembershipPayment::STATUS_PENDING, $newPayment->status); } /** * Test multiple members can register concurrently */ public function test_multiple_members_concurrent_registration(): void { Mail::fake(); $members = []; for ($i = 1; $i <= 3; $i++) { $registrationData = $this->getValidMemberRegistrationData([ 'email' => "member{$i}@example.com", 'full_name' => "Test Member {$i}", ]); $this->post(route('register.member.store'), $registrationData); $members[$i] = User::where('email', "member{$i}@example.com")->first(); } // Verify all members were created foreach ($members as $i => $user) { $this->assertNotNull($user); $this->assertNotNull($user->member); $this->assertEquals(Member::STATUS_PENDING, $user->member->membership_status); $this->assertEquals("Test Member {$i}", $user->member->full_name); } $this->assertCount(3, Member::where('membership_status', Member::STATUS_PENDING)->get()); } /** * Test member status transitions through workflow */ public function test_member_status_transitions_through_workflow(): void { Mail::fake(); $team = $this->createFinanceApprovalTeam(); $data = $this->createMemberWithPendingPayment(); $member = $data['member']; $payment = $data['payment']; // Initial status $this->assertEquals(Member::STATUS_PENDING, $member->membership_status); $this->assertFalse($member->hasPaidMembership()); // After cashier approval - member still pending $this->actingAs($team['cashier'])->post( route('admin.payment-verifications.approve-cashier', $payment) ); $member->refresh(); $this->assertEquals(Member::STATUS_PENDING, $member->membership_status); // After accountant approval - member still pending $payment->refresh(); $this->actingAs($team['accountant'])->post( route('admin.payment-verifications.approve-accountant', $payment) ); $member->refresh(); $this->assertEquals(Member::STATUS_PENDING, $member->membership_status); // After chair approval - member becomes active $payment->refresh(); $this->actingAs($team['chair'])->post( route('admin.payment-verifications.approve-chair', $payment) ); $member->refresh(); $this->assertEquals(Member::STATUS_ACTIVE, $member->membership_status); $this->assertTrue($member->hasPaidMembership()); $this->assertNotNull($member->membership_started_at); $this->assertNotNull($member->membership_expires_at); $this->assertTrue($member->membership_expires_at->isAfter(now())); } }