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:
451
tests/Feature/EndToEnd/FinanceWorkflowEndToEndTest.php
Normal file
451
tests/Feature/EndToEnd/FinanceWorkflowEndToEndTest.php
Normal file
@@ -0,0 +1,451 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\EndToEnd;
|
||||
|
||||
use App\Models\BankReconciliation;
|
||||
use App\Models\CashierLedgerEntry;
|
||||
use App\Models\FinanceDocument;
|
||||
use App\Models\PaymentOrder;
|
||||
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\CreatesFinanceData;
|
||||
use Tests\Traits\SeedsRolesAndPermissions;
|
||||
|
||||
/**
|
||||
* End-to-End Finance Workflow Tests
|
||||
*
|
||||
* Tests the complete 4-stage financial workflow:
|
||||
* Stage 1: Finance Document Approval (Cashier → Accountant → Chair → Board)
|
||||
* Stage 2: Payment Order (Creation → Verification → Execution)
|
||||
* Stage 3: Cashier Ledger Entry (Recording)
|
||||
* Stage 4: Bank Reconciliation (Preparation → Review → Approval)
|
||||
*/
|
||||
class FinanceWorkflowEndToEndTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase, SeedsRolesAndPermissions, CreatesFinanceData;
|
||||
|
||||
protected User $cashier;
|
||||
protected User $accountant;
|
||||
protected User $chair;
|
||||
protected User $boardMember;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Storage::fake('local');
|
||||
Mail::fake();
|
||||
$this->seedRolesAndPermissions();
|
||||
|
||||
$this->cashier = $this->createCashier(['email' => 'cashier@test.com']);
|
||||
$this->accountant = $this->createAccountant(['email' => 'accountant@test.com']);
|
||||
$this->chair = $this->createChair(['email' => 'chair@test.com']);
|
||||
$this->boardMember = $this->createBoardMember(['email' => 'board@test.com']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test small amount (< 5000) complete workflow
|
||||
* Small amounts only require Cashier + Accountant approval
|
||||
*/
|
||||
public function test_small_amount_complete_workflow(): void
|
||||
{
|
||||
// Create small amount document
|
||||
$document = $this->createSmallAmountDocument([
|
||||
'status' => FinanceDocument::STATUS_PENDING,
|
||||
'submitted_by_user_id' => User::factory()->create()->id,
|
||||
]);
|
||||
|
||||
$this->assertEquals(FinanceDocument::AMOUNT_TIER_SMALL, $document->determineAmountTier());
|
||||
|
||||
// Cashier approves
|
||||
$this->actingAs($this->cashier)->post(
|
||||
route('admin.finance-documents.approve', $document)
|
||||
);
|
||||
$document->refresh();
|
||||
$this->assertEquals(FinanceDocument::STATUS_APPROVED_CASHIER, $document->status);
|
||||
|
||||
// Accountant approves - should be fully approved for small amounts
|
||||
$this->actingAs($this->accountant)->post(
|
||||
route('admin.finance-documents.approve', $document)
|
||||
);
|
||||
$document->refresh();
|
||||
|
||||
// Small amounts may be fully approved after accountant
|
||||
$this->assertTrue(
|
||||
$document->status === FinanceDocument::STATUS_APPROVED_ACCOUNTANT ||
|
||||
$document->status === FinanceDocument::STATUS_APPROVED_CHAIR
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test medium amount (5000 - 50000) complete workflow
|
||||
* Medium amounts require Cashier + Accountant + Chair approval
|
||||
*/
|
||||
public function test_medium_amount_complete_workflow(): void
|
||||
{
|
||||
$document = $this->createMediumAmountDocument([
|
||||
'status' => FinanceDocument::STATUS_PENDING,
|
||||
'submitted_by_user_id' => User::factory()->create()->id,
|
||||
]);
|
||||
|
||||
$this->assertEquals(FinanceDocument::AMOUNT_TIER_MEDIUM, $document->determineAmountTier());
|
||||
|
||||
// Stage 1: Cashier approves
|
||||
$this->actingAs($this->cashier)->post(
|
||||
route('admin.finance-documents.approve', $document)
|
||||
);
|
||||
$document->refresh();
|
||||
$this->assertEquals(FinanceDocument::STATUS_APPROVED_CASHIER, $document->status);
|
||||
|
||||
// Stage 2: Accountant approves
|
||||
$this->actingAs($this->accountant)->post(
|
||||
route('admin.finance-documents.approve', $document)
|
||||
);
|
||||
$document->refresh();
|
||||
$this->assertEquals(FinanceDocument::STATUS_APPROVED_ACCOUNTANT, $document->status);
|
||||
|
||||
// Stage 3: Chair approves - final approval
|
||||
$this->actingAs($this->chair)->post(
|
||||
route('admin.finance-documents.approve', $document)
|
||||
);
|
||||
$document->refresh();
|
||||
$this->assertEquals(FinanceDocument::STATUS_APPROVED_CHAIR, $document->status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test large amount (> 50000) complete workflow with board approval
|
||||
*/
|
||||
public function test_large_amount_complete_workflow_with_board_approval(): void
|
||||
{
|
||||
$document = $this->createLargeAmountDocument([
|
||||
'status' => FinanceDocument::STATUS_PENDING,
|
||||
'submitted_by_user_id' => User::factory()->create()->id,
|
||||
]);
|
||||
|
||||
$this->assertEquals(FinanceDocument::AMOUNT_TIER_LARGE, $document->determineAmountTier());
|
||||
|
||||
// Approval sequence: Cashier → Accountant → Chair → Board
|
||||
$this->actingAs($this->cashier)->post(
|
||||
route('admin.finance-documents.approve', $document)
|
||||
);
|
||||
$document->refresh();
|
||||
|
||||
$this->actingAs($this->accountant)->post(
|
||||
route('admin.finance-documents.approve', $document)
|
||||
);
|
||||
$document->refresh();
|
||||
|
||||
$this->actingAs($this->chair)->post(
|
||||
route('admin.finance-documents.approve', $document)
|
||||
);
|
||||
$document->refresh();
|
||||
|
||||
// For large amounts, may need board approval
|
||||
if ($document->requiresBoardApproval()) {
|
||||
$this->actingAs($this->boardMember)->post(
|
||||
route('admin.finance-documents.approve', $document)
|
||||
);
|
||||
$document->refresh();
|
||||
$this->assertEquals(FinanceDocument::STATUS_APPROVED_BOARD, $document->status);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test finance document to payment order to execution flow
|
||||
*/
|
||||
public function test_finance_document_to_payment_order_to_execution(): void
|
||||
{
|
||||
// Create approved document
|
||||
$document = $this->createDocumentAtStage('chair_approved', [
|
||||
'amount' => 10000,
|
||||
'payee_name' => 'Test Vendor',
|
||||
]);
|
||||
|
||||
// Stage 2: Accountant creates payment order
|
||||
$response = $this->actingAs($this->accountant)->post(
|
||||
route('admin.payment-orders.store'),
|
||||
[
|
||||
'finance_document_id' => $document->id,
|
||||
'payment_method' => 'bank_transfer',
|
||||
'bank_name' => 'Test Bank',
|
||||
'account_number' => '1234567890',
|
||||
'account_name' => 'Test Vendor',
|
||||
'notes' => 'Payment for approved document',
|
||||
]
|
||||
);
|
||||
|
||||
$paymentOrder = PaymentOrder::where('finance_document_id', $document->id)->first();
|
||||
$this->assertNotNull($paymentOrder);
|
||||
$this->assertEquals(PaymentOrder::STATUS_PENDING_VERIFICATION, $paymentOrder->status);
|
||||
|
||||
// Cashier verifies payment order
|
||||
$this->actingAs($this->cashier)->post(
|
||||
route('admin.payment-orders.verify', $paymentOrder)
|
||||
);
|
||||
$paymentOrder->refresh();
|
||||
$this->assertEquals(PaymentOrder::STATUS_VERIFIED, $paymentOrder->status);
|
||||
|
||||
// Cashier executes payment
|
||||
$this->actingAs($this->cashier)->post(
|
||||
route('admin.payment-orders.execute', $paymentOrder),
|
||||
['execution_notes' => 'Payment executed via bank transfer']
|
||||
);
|
||||
$paymentOrder->refresh();
|
||||
$this->assertEquals(PaymentOrder::STATUS_EXECUTED, $paymentOrder->status);
|
||||
$this->assertNotNull($paymentOrder->executed_at);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test payment order to cashier ledger entry flow
|
||||
*/
|
||||
public function test_payment_order_to_cashier_ledger_entry(): void
|
||||
{
|
||||
// Create executed payment order
|
||||
$paymentOrder = $this->createPaymentOrderAtStage('executed', [
|
||||
'amount' => 5000,
|
||||
]);
|
||||
|
||||
// Cashier records ledger entry
|
||||
$response = $this->actingAs($this->cashier)->post(
|
||||
route('admin.cashier-ledger.store'),
|
||||
[
|
||||
'finance_document_id' => $paymentOrder->finance_document_id,
|
||||
'entry_type' => 'payment',
|
||||
'entry_date' => now()->format('Y-m-d'),
|
||||
'amount' => 5000,
|
||||
'payment_method' => 'bank_transfer',
|
||||
'bank_account' => 'Main Operating Account',
|
||||
'notes' => 'Payment for invoice #123',
|
||||
]
|
||||
);
|
||||
|
||||
$response->assertRedirect();
|
||||
|
||||
$ledgerEntry = CashierLedgerEntry::where('finance_document_id', $paymentOrder->finance_document_id)->first();
|
||||
$this->assertNotNull($ledgerEntry);
|
||||
$this->assertEquals('payment', $ledgerEntry->entry_type);
|
||||
$this->assertEquals(5000, $ledgerEntry->amount);
|
||||
$this->assertEquals($this->cashier->id, $ledgerEntry->recorded_by_cashier_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test cashier ledger to bank reconciliation flow
|
||||
*/
|
||||
public function test_cashier_ledger_to_bank_reconciliation(): void
|
||||
{
|
||||
// Create ledger entries
|
||||
$this->createReceiptEntry(100000, 'Main Account', [
|
||||
'recorded_by_cashier_id' => $this->cashier->id,
|
||||
]);
|
||||
$this->createPaymentEntry(30000, 'Main Account', [
|
||||
'recorded_by_cashier_id' => $this->cashier->id,
|
||||
]);
|
||||
|
||||
// Current balance should be 70000
|
||||
$balance = CashierLedgerEntry::getLatestBalance('Main Account');
|
||||
$this->assertEquals(70000, $balance);
|
||||
|
||||
// Create bank reconciliation
|
||||
$response = $this->actingAs($this->cashier)->post(
|
||||
route('admin.bank-reconciliations.store'),
|
||||
[
|
||||
'reconciliation_month' => now()->format('Y-m'),
|
||||
'bank_statement_date' => now()->format('Y-m-d'),
|
||||
'bank_statement_balance' => 70000,
|
||||
'system_book_balance' => 70000,
|
||||
'outstanding_checks' => [],
|
||||
'deposits_in_transit' => [],
|
||||
'bank_charges' => [],
|
||||
'notes' => 'Monthly reconciliation',
|
||||
]
|
||||
);
|
||||
|
||||
$reconciliation = BankReconciliation::latest()->first();
|
||||
$this->assertNotNull($reconciliation);
|
||||
$this->assertEquals(0, $reconciliation->discrepancy_amount);
|
||||
$this->assertFalse($reconciliation->hasDiscrepancy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test complete 4-stage financial workflow
|
||||
*/
|
||||
public function test_complete_4_stage_financial_workflow(): void
|
||||
{
|
||||
$submitter = User::factory()->create();
|
||||
|
||||
// Stage 1: Create and approve finance document
|
||||
$document = FinanceDocument::factory()->create([
|
||||
'title' => 'Complete Workflow Test',
|
||||
'amount' => 25000,
|
||||
'status' => FinanceDocument::STATUS_PENDING,
|
||||
'submitted_by_user_id' => $submitter->id,
|
||||
'request_type' => FinanceDocument::TYPE_EXPENSE_REIMBURSEMENT,
|
||||
]);
|
||||
|
||||
// Approve through all stages
|
||||
$this->actingAs($this->cashier)->post(route('admin.finance-documents.approve', $document));
|
||||
$document->refresh();
|
||||
|
||||
$this->actingAs($this->accountant)->post(route('admin.finance-documents.approve', $document));
|
||||
$document->refresh();
|
||||
|
||||
$this->actingAs($this->chair)->post(route('admin.finance-documents.approve', $document));
|
||||
$document->refresh();
|
||||
|
||||
$this->assertEquals(FinanceDocument::STATUS_APPROVED_CHAIR, $document->status);
|
||||
|
||||
// Stage 2: Create and execute payment order
|
||||
$this->actingAs($this->accountant)->post(route('admin.payment-orders.store'), [
|
||||
'finance_document_id' => $document->id,
|
||||
'payment_method' => 'bank_transfer',
|
||||
'bank_name' => 'Test Bank',
|
||||
'account_number' => '9876543210',
|
||||
'account_name' => 'Submitter Name',
|
||||
]);
|
||||
|
||||
$paymentOrder = PaymentOrder::where('finance_document_id', $document->id)->first();
|
||||
$this->assertNotNull($paymentOrder);
|
||||
|
||||
$this->actingAs($this->cashier)->post(route('admin.payment-orders.verify', $paymentOrder));
|
||||
$paymentOrder->refresh();
|
||||
|
||||
$this->actingAs($this->cashier)->post(route('admin.payment-orders.execute', $paymentOrder));
|
||||
$paymentOrder->refresh();
|
||||
|
||||
$this->assertEquals(PaymentOrder::STATUS_EXECUTED, $paymentOrder->status);
|
||||
|
||||
// Stage 3: Record ledger entry
|
||||
$this->actingAs($this->cashier)->post(route('admin.cashier-ledger.store'), [
|
||||
'finance_document_id' => $document->id,
|
||||
'entry_type' => 'payment',
|
||||
'entry_date' => now()->format('Y-m-d'),
|
||||
'amount' => 25000,
|
||||
'payment_method' => 'bank_transfer',
|
||||
'bank_account' => 'Operating Account',
|
||||
]);
|
||||
|
||||
$ledgerEntry = CashierLedgerEntry::where('finance_document_id', $document->id)->first();
|
||||
$this->assertNotNull($ledgerEntry);
|
||||
|
||||
// Stage 4: Bank reconciliation
|
||||
$this->actingAs($this->cashier)->post(route('admin.bank-reconciliations.store'), [
|
||||
'reconciliation_month' => now()->format('Y-m'),
|
||||
'bank_statement_date' => now()->format('Y-m-d'),
|
||||
'bank_statement_balance' => 75000,
|
||||
'system_book_balance' => 75000,
|
||||
'outstanding_checks' => [],
|
||||
'deposits_in_transit' => [],
|
||||
'bank_charges' => [],
|
||||
]);
|
||||
|
||||
$reconciliation = BankReconciliation::latest()->first();
|
||||
|
||||
// Accountant reviews
|
||||
$this->actingAs($this->accountant)->post(
|
||||
route('admin.bank-reconciliations.review', $reconciliation),
|
||||
['review_notes' => 'Reviewed and verified']
|
||||
);
|
||||
$reconciliation->refresh();
|
||||
$this->assertNotNull($reconciliation->reviewed_at);
|
||||
|
||||
// Manager/Chair approves
|
||||
$this->actingAs($this->chair)->post(
|
||||
route('admin.bank-reconciliations.approve', $reconciliation),
|
||||
['approval_notes' => 'Approved']
|
||||
);
|
||||
$reconciliation->refresh();
|
||||
|
||||
$this->assertEquals('completed', $reconciliation->reconciliation_status);
|
||||
$this->assertTrue($reconciliation->isCompleted());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test rejection at each approval stage
|
||||
*/
|
||||
public function test_rejection_at_each_approval_stage(): void
|
||||
{
|
||||
// Test rejection at cashier stage
|
||||
$doc1 = $this->createFinanceDocument(['status' => FinanceDocument::STATUS_PENDING]);
|
||||
$this->actingAs($this->cashier)->post(
|
||||
route('admin.finance-documents.reject', $doc1),
|
||||
['rejection_reason' => 'Missing documentation']
|
||||
);
|
||||
$doc1->refresh();
|
||||
$this->assertEquals(FinanceDocument::STATUS_REJECTED, $doc1->status);
|
||||
|
||||
// Test rejection at accountant stage
|
||||
$doc2 = $this->createDocumentAtStage('cashier_approved');
|
||||
$this->actingAs($this->accountant)->post(
|
||||
route('admin.finance-documents.reject', $doc2),
|
||||
['rejection_reason' => 'Amount exceeds policy limit']
|
||||
);
|
||||
$doc2->refresh();
|
||||
$this->assertEquals(FinanceDocument::STATUS_REJECTED, $doc2->status);
|
||||
|
||||
// Test rejection at chair stage
|
||||
$doc3 = $this->createDocumentAtStage('accountant_approved');
|
||||
$this->actingAs($this->chair)->post(
|
||||
route('admin.finance-documents.reject', $doc3),
|
||||
['rejection_reason' => 'Not within budget allocation']
|
||||
);
|
||||
$doc3->refresh();
|
||||
$this->assertEquals(FinanceDocument::STATUS_REJECTED, $doc3->status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test workflow with different payment methods
|
||||
*/
|
||||
public function test_workflow_with_different_payment_methods(): void
|
||||
{
|
||||
$paymentMethods = ['cash', 'bank_transfer', 'check'];
|
||||
|
||||
foreach ($paymentMethods as $method) {
|
||||
$document = $this->createDocumentAtStage('chair_approved', [
|
||||
'amount' => 5000,
|
||||
]);
|
||||
|
||||
$this->actingAs($this->accountant)->post(route('admin.payment-orders.store'), [
|
||||
'finance_document_id' => $document->id,
|
||||
'payment_method' => $method,
|
||||
'bank_name' => $method === 'bank_transfer' ? 'Test Bank' : null,
|
||||
'account_number' => $method === 'bank_transfer' ? '1234567890' : null,
|
||||
'check_number' => $method === 'check' ? 'CHK001' : null,
|
||||
]);
|
||||
|
||||
$paymentOrder = PaymentOrder::where('finance_document_id', $document->id)->first();
|
||||
$this->assertNotNull($paymentOrder);
|
||||
$this->assertEquals($method, $paymentOrder->payment_method);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test budget integration with finance documents
|
||||
*/
|
||||
public function test_budget_integration_with_finance_documents(): void
|
||||
{
|
||||
$budget = $this->createBudgetWithItems(3, [
|
||||
'status' => 'active',
|
||||
'fiscal_year' => now()->year,
|
||||
]);
|
||||
|
||||
$budgetItem = $budget->items->first();
|
||||
|
||||
$document = FinanceDocument::factory()->create([
|
||||
'amount' => 10000,
|
||||
'budget_item_id' => $budgetItem->id,
|
||||
'status' => FinanceDocument::STATUS_PENDING,
|
||||
]);
|
||||
|
||||
$this->assertEquals($budgetItem->id, $document->budget_item_id);
|
||||
|
||||
// Approve through workflow
|
||||
$this->actingAs($this->cashier)->post(route('admin.finance-documents.approve', $document));
|
||||
$this->actingAs($this->accountant)->post(route('admin.finance-documents.approve', $document->fresh()));
|
||||
$this->actingAs($this->chair)->post(route('admin.finance-documents.approve', $document->fresh()));
|
||||
|
||||
$document->refresh();
|
||||
$this->assertEquals(FinanceDocument::STATUS_APPROVED_CHAIR, $document->status);
|
||||
}
|
||||
}
|
||||
373
tests/Feature/EndToEnd/MembershipWorkflowEndToEndTest.php
Normal file
373
tests/Feature/EndToEnd/MembershipWorkflowEndToEndTest.php
Normal file
@@ -0,0 +1,373 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\EndToEnd;
|
||||
|
||||
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 Tests\TestCase;
|
||||
use Tests\Traits\CreatesMemberData;
|
||||
use Tests\Traits\SeedsRolesAndPermissions;
|
||||
|
||||
/**
|
||||
* End-to-End Membership Workflow Tests
|
||||
*
|
||||
* Tests the complete membership registration and payment verification workflow
|
||||
* from member registration through three-tier approval to membership activation.
|
||||
*/
|
||||
class MembershipWorkflowEndToEndTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase, SeedsRolesAndPermissions, CreatesMemberData;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Storage::fake('private');
|
||||
$this->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()));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user