Initial commit
This commit is contained in:
488
tests/Feature/PaymentVerificationTest.php
Normal file
488
tests/Feature/PaymentVerificationTest.php
Normal file
@@ -0,0 +1,488 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature;
|
||||
|
||||
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\Models\Member;
|
||||
use App\Models\MembershipPayment;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Spatie\Permission\Models\Permission;
|
||||
use Spatie\Permission\Models\Role;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PaymentVerificationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Storage::fake('private');
|
||||
$this->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();
|
||||
$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();
|
||||
|
||||
$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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user