artisan('db:seed', ['--class' => 'RoleSeeder']); $this->artisan('db:seed', ['--class' => 'PaymentVerificationRolesSeeder']); } public function test_member_can_submit_payment_with_receipt(): void { Mail::fake(); $user = User::factory()->create(); $member = Member::factory()->create([ 'user_id' => $user->id, 'membership_status' => Member::STATUS_PENDING, ]); $file = UploadedFile::fake()->image('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' => 'ATM123456', 'receipt' => $file, 'notes' => 'Annual membership fee', ]); $response->assertRedirect(route('member.dashboard')); $response->assertSessionHas('status'); $this->assertDatabaseHas('membership_payments', [ 'member_id' => $member->id, 'amount' => 1000, 'status' => MembershipPayment::STATUS_PENDING, 'payment_method' => MembershipPayment::METHOD_BANK_TRANSFER, ]); } public function test_receipt_is_stored_in_private_storage(): void { Mail::fake(); Storage::fake('private'); $user = User::factory()->create(); $member = Member::factory()->create([ 'user_id' => $user->id, 'membership_status' => Member::STATUS_PENDING, ]); $file = UploadedFile::fake()->image('receipt.jpg'); $this->actingAs($user)->post(route('member.payments.store'), [ 'amount' => 1000, 'paid_at' => now()->format('Y-m-d'), 'payment_method' => MembershipPayment::METHOD_BANK_TRANSFER, 'receipt' => $file, ]); $payment = MembershipPayment::first(); $this->assertNotNull($payment->receipt_path); Storage::disk('private')->assertExists($payment->receipt_path); } public function test_payment_starts_with_pending_status(): void { Mail::fake(); $user = User::factory()->create(); $member = Member::factory()->create([ 'user_id' => $user->id, 'membership_status' => Member::STATUS_PENDING, ]); $file = UploadedFile::fake()->image('receipt.jpg'); $this->actingAs($user)->post(route('member.payments.store'), [ 'amount' => 1000, 'paid_at' => now()->format('Y-m-d'), 'payment_method' => MembershipPayment::METHOD_BANK_TRANSFER, 'receipt' => $file, ]); $payment = MembershipPayment::first(); $this->assertEquals(MembershipPayment::STATUS_PENDING, $payment->status); $this->assertTrue($payment->isPending()); } public function test_submission_emails_sent_to_member_and_cashiers(): void { Mail::fake(); $cashier = User::factory()->create(); $cashier->givePermissionTo('verify_payments_cashier'); $user = User::factory()->create(); $member = Member::factory()->create([ 'user_id' => $user->id, 'membership_status' => Member::STATUS_PENDING, ]); $file = UploadedFile::fake()->image('receipt.jpg'); $this->actingAs($user)->post(route('member.payments.store'), [ 'amount' => 1000, 'paid_at' => now()->format('Y-m-d'), 'payment_method' => MembershipPayment::METHOD_BANK_TRANSFER, 'receipt' => $file, ]); Mail::assertQueued(PaymentSubmittedMail::class, 2); // Member + Cashier } public function test_cashier_can_approve_tier_1(): void { Mail::fake(); $cashier = User::factory()->create(); $cashier->givePermissionTo('verify_payments_cashier'); $member = Member::factory()->create(); $payment = MembershipPayment::factory()->create([ 'member_id' => $member->id, 'status' => MembershipPayment::STATUS_PENDING, ]); $response = $this->actingAs($cashier)->post( route('admin.payment-verifications.approve-cashier', $payment), ['notes' => 'Receipt verified'] ); $response->assertRedirect(route('admin.payment-verifications.index')); $payment->refresh(); $this->assertEquals(MembershipPayment::STATUS_APPROVED_CASHIER, $payment->status); $this->assertEquals($cashier->id, $payment->verified_by_cashier_id); $this->assertNotNull($payment->cashier_verified_at); } public function test_cashier_approval_sends_email_to_accountants(): void { Mail::fake(); $cashier = User::factory()->create(); $cashier->givePermissionTo('verify_payments_cashier'); $accountant = User::factory()->create(); $accountant->givePermissionTo('verify_payments_accountant'); $member = Member::factory()->create(); $payment = MembershipPayment::factory()->create([ 'member_id' => $member->id, 'status' => MembershipPayment::STATUS_PENDING, ]); $this->actingAs($cashier)->post( route('admin.payment-verifications.approve-cashier', $payment) ); Mail::assertQueued(PaymentApprovedByCashierMail::class); } public function test_accountant_can_approve_tier_2(): void { Mail::fake(); $accountant = User::factory()->create(); $accountant->givePermissionTo('verify_payments_accountant'); $member = Member::factory()->create(); $payment = MembershipPayment::factory()->create([ 'member_id' => $member->id, 'status' => MembershipPayment::STATUS_APPROVED_CASHIER, ]); $response = $this->actingAs($accountant)->post( route('admin.payment-verifications.approve-accountant', $payment), ['notes' => 'Amount verified'] ); $response->assertRedirect(route('admin.payment-verifications.index')); $payment->refresh(); $this->assertEquals(MembershipPayment::STATUS_APPROVED_ACCOUNTANT, $payment->status); $this->assertEquals($accountant->id, $payment->verified_by_accountant_id); $this->assertNotNull($payment->accountant_verified_at); } public function test_accountant_approval_sends_email_to_chairs(): void { Mail::fake(); $accountant = User::factory()->create(); $accountant->givePermissionTo('verify_payments_accountant'); $chair = User::factory()->create(); $chair->givePermissionTo('verify_payments_chair'); $member = Member::factory()->create(); $payment = MembershipPayment::factory()->create([ 'member_id' => $member->id, 'status' => MembershipPayment::STATUS_APPROVED_CASHIER, ]); $this->actingAs($accountant)->post( route('admin.payment-verifications.approve-accountant', $payment) ); Mail::assertQueued(PaymentApprovedByAccountantMail::class); } public function test_chair_can_approve_tier_3(): void { Mail::fake(); $chair = User::factory()->create(); $chair->givePermissionTo('verify_payments_chair'); $member = Member::factory()->create([ 'membership_status' => Member::STATUS_PENDING, ]); $payment = MembershipPayment::factory()->create([ 'member_id' => $member->id, 'status' => MembershipPayment::STATUS_APPROVED_ACCOUNTANT, ]); $response = $this->actingAs($chair)->post( route('admin.payment-verifications.approve-chair', $payment), ['notes' => 'Final approval'] ); $response->assertRedirect(route('admin.payment-verifications.index')); $payment->refresh(); $this->assertEquals(MembershipPayment::STATUS_APPROVED_CHAIR, $payment->status); $this->assertEquals($chair->id, $payment->verified_by_chair_id); $this->assertNotNull($payment->chair_verified_at); $this->assertTrue($payment->isFullyApproved()); } public function test_chair_approval_activates_membership_automatically(): void { Mail::fake(); $chair = User::factory()->create(); $chair->givePermissionTo('verify_payments_chair'); $member = Member::factory()->create([ 'membership_status' => Member::STATUS_PENDING, 'membership_started_at' => null, 'membership_expires_at' => null, ]); $payment = MembershipPayment::factory()->create([ 'member_id' => $member->id, 'status' => MembershipPayment::STATUS_APPROVED_ACCOUNTANT, ]); $this->actingAs($chair)->post( route('admin.payment-verifications.approve-chair', $payment) ); $member->refresh(); $this->assertEquals(Member::STATUS_ACTIVE, $member->membership_status); $this->assertNotNull($member->membership_started_at); $this->assertNotNull($member->membership_expires_at); $this->assertTrue($member->hasPaidMembership()); } public function test_activation_email_sent_to_member(): void { Mail::fake(); $chair = User::factory()->create(); $chair->givePermissionTo('verify_payments_chair'); $member = Member::factory()->create([ 'membership_status' => Member::STATUS_PENDING, ]); $payment = MembershipPayment::factory()->create([ 'member_id' => $member->id, 'status' => MembershipPayment::STATUS_APPROVED_ACCOUNTANT, ]); $this->actingAs($chair)->post( route('admin.payment-verifications.approve-chair', $payment) ); Mail::assertQueued(PaymentFullyApprovedMail::class); Mail::assertQueued(MembershipActivatedMail::class); } public function test_cannot_skip_tiers_accountant_cant_approve_pending(): void { $accountant = User::factory()->create(); $accountant->givePermissionTo('verify_payments_accountant'); $member = Member::factory()->create(); $payment = MembershipPayment::factory()->create([ 'member_id' => $member->id, 'status' => MembershipPayment::STATUS_PENDING, ]); $response = $this->actingAs($accountant)->post( route('admin.payment-verifications.approve-accountant', $payment) ); $response->assertSessionHas('error'); $payment->refresh(); $this->assertEquals(MembershipPayment::STATUS_PENDING, $payment->status); } public function test_can_reject_at_any_tier_with_reason(): void { Mail::fake(); $cashier = User::factory()->create(); $cashier->givePermissionTo('verify_payments_cashier'); $member = Member::factory()->create(); $payment = MembershipPayment::factory()->create([ 'member_id' => $member->id, 'status' => MembershipPayment::STATUS_PENDING, ]); $response = $this->actingAs($cashier)->post( route('admin.payment-verifications.reject', $payment), ['rejection_reason' => 'Invalid receipt'] ); $response->assertRedirect(route('admin.payment-verifications.index')); $payment->refresh(); $this->assertEquals(MembershipPayment::STATUS_REJECTED, $payment->status); $this->assertEquals('Invalid receipt', $payment->rejection_reason); $this->assertEquals($cashier->id, $payment->rejected_by_user_id); $this->assertNotNull($payment->rejected_at); } public function test_rejection_email_sent_with_reason(): void { Mail::fake(); $cashier = User::factory()->create(); $cashier->givePermissionTo('verify_payments_cashier'); $member = Member::factory()->create(); $payment = MembershipPayment::factory()->create([ 'member_id' => $member->id, 'status' => MembershipPayment::STATUS_PENDING, ]); $this->actingAs($cashier)->post( route('admin.payment-verifications.reject', $payment), ['rejection_reason' => 'Invalid receipt'] ); Mail::assertQueued(PaymentRejectedMail::class, function ($mail) use ($payment) { return $mail->payment->id === $payment->id; }); } public function test_dashboard_shows_correct_queues_based_on_permissions(): void { $admin = User::factory()->create(['is_admin' => true]); $admin->assignRole('admin'); $admin->givePermissionTo('view_payment_verifications'); // Create payments in different states $pending = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_PENDING]); $cashierApproved = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_CASHIER]); $accountantApproved = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_ACCOUNTANT]); $response = $this->actingAs($admin)->get(route('admin.payment-verifications.index')); $response->assertStatus(200); $response->assertSee('Cashier Queue'); $response->assertSee('Accountant Queue'); $response->assertSee('Chair Queue'); } public function test_user_without_permission_cannot_access_dashboard(): void { $user = User::factory()->create(); // non-admin $response = $this->actingAs($user)->get(route('admin.payment-verifications.index')); $response->assertStatus(403); } public function test_audit_log_created_for_each_approval(): void { Mail::fake(); $cashier = User::factory()->create(); $cashier->givePermissionTo('verify_payments_cashier'); $member = Member::factory()->create(); $payment = MembershipPayment::factory()->create([ 'member_id' => $member->id, 'status' => MembershipPayment::STATUS_PENDING, ]); $this->actingAs($cashier)->post( route('admin.payment-verifications.approve-cashier', $payment) ); $this->assertDatabaseHas('audit_logs', [ 'action' => 'payment.approved_by_cashier', 'user_id' => $cashier->id, ]); } public function test_complete_workflow_sequence(): void { Mail::fake(); // Setup users with permissions $cashier = User::factory()->create(); $cashier->givePermissionTo('verify_payments_cashier'); $accountant = User::factory()->create(); $accountant->givePermissionTo('verify_payments_accountant'); $chair = User::factory()->create(); $chair->givePermissionTo('verify_payments_chair'); // Create member and payment $member = Member::factory()->create([ 'membership_status' => Member::STATUS_PENDING, ]); $payment = MembershipPayment::factory()->create([ 'member_id' => $member->id, 'status' => MembershipPayment::STATUS_PENDING, ]); // Step 1: Cashier approves $this->actingAs($cashier)->post( route('admin.payment-verifications.approve-cashier', $payment) ); $payment->refresh(); $this->assertTrue($payment->isApprovedByCashier()); // Step 2: Accountant approves $this->actingAs($accountant)->post( route('admin.payment-verifications.approve-accountant', $payment) ); $payment->refresh(); $this->assertTrue($payment->isApprovedByAccountant()); // Step 3: Chair approves $this->actingAs($chair)->post( route('admin.payment-verifications.approve-chair', $payment) ); $payment->refresh(); $this->assertTrue($payment->isFullyApproved()); // Verify member is activated $member->refresh(); $this->assertEquals(Member::STATUS_ACTIVE, $member->membership_status); $this->assertTrue($member->hasPaidMembership()); } }