Features: - Support login via phone number or email (LoginRequest) - Add members:import-roster command for Excel roster import - Merge survey emails with roster data Code Quality (Phase 1-4): - Add database locking for balance calculation - Add self-approval checks for finance workflow - Create service layer (FinanceDocumentApprovalService, PaymentVerificationService) - Add HasAccountingEntries and HasApprovalWorkflow traits - Create FormRequest classes for validation - Add status-badge component - Define authorization gates in AuthServiceProvider - Add accounting config file Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
227 lines
8.4 KiB
PHP
227 lines
8.4 KiB
PHP
<?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 financial workflow:
|
|
* Stage 1: Finance Document Approval (Secretary → Chair → Board based on amount)
|
|
* Stage 2: Disbursement (Requester + Cashier confirmation)
|
|
* Stage 3: Recording (Accountant records to ledger)
|
|
* Stage 4: Bank Reconciliation (Preparation → Review → Approval)
|
|
*/
|
|
class FinanceWorkflowEndToEndTest extends TestCase
|
|
{
|
|
use RefreshDatabase, SeedsRolesAndPermissions, CreatesFinanceData;
|
|
|
|
protected User $secretary;
|
|
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->secretary = $this->createSecretary(['email' => 'secretary@test.com']);
|
|
$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 Secretary 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());
|
|
|
|
// Secretary approves - should be fully approved for small amounts
|
|
$this->actingAs($this->secretary)->post(
|
|
route('admin.finance.approve', $document)
|
|
);
|
|
$document->refresh();
|
|
$this->assertEquals(FinanceDocument::STATUS_APPROVED_SECRETARY, $document->status);
|
|
|
|
// Small amounts are complete after secretary approval
|
|
$this->assertTrue($document->isApprovalComplete());
|
|
}
|
|
|
|
/**
|
|
* Test medium amount (5000 - 50000) complete workflow
|
|
* Medium amounts require Secretary + 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: Secretary approves
|
|
$this->actingAs($this->secretary)->post(
|
|
route('admin.finance.approve', $document)
|
|
);
|
|
$document->refresh();
|
|
$this->assertEquals(FinanceDocument::STATUS_APPROVED_SECRETARY, $document->status);
|
|
|
|
// Stage 2: Chair approves - final approval for medium amounts
|
|
$this->actingAs($this->chair)->post(
|
|
route('admin.finance.approve', $document)
|
|
);
|
|
$document->refresh();
|
|
$this->assertEquals(FinanceDocument::STATUS_APPROVED_CHAIR, $document->status);
|
|
|
|
$this->assertTrue($document->isApprovalComplete());
|
|
}
|
|
|
|
/**
|
|
* 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: Secretary → Chair → Board
|
|
$this->actingAs($this->secretary)->post(
|
|
route('admin.finance.approve', $document)
|
|
);
|
|
$document->refresh();
|
|
$this->assertEquals(FinanceDocument::STATUS_APPROVED_SECRETARY, $document->status);
|
|
|
|
$this->actingAs($this->chair)->post(
|
|
route('admin.finance.approve', $document)
|
|
);
|
|
$document->refresh();
|
|
$this->assertEquals(FinanceDocument::STATUS_APPROVED_CHAIR, $document->status);
|
|
|
|
// Board approval for large amounts
|
|
$this->actingAs($this->boardMember)->post(
|
|
route('admin.finance.approve', $document)
|
|
);
|
|
$document->refresh();
|
|
$this->assertEquals(FinanceDocument::STATUS_APPROVED_BOARD, $document->status);
|
|
|
|
$this->assertTrue($document->isApprovalComplete());
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
$reconciliation = $this->createBankReconciliation([
|
|
'bank_statement_balance' => 70000,
|
|
'system_book_balance' => 70000,
|
|
'discrepancy_amount' => 0,
|
|
]);
|
|
|
|
$this->assertEquals(0, $reconciliation->discrepancy_amount);
|
|
}
|
|
|
|
/**
|
|
* Test rejection at each approval stage
|
|
*/
|
|
public function test_rejection_at_each_approval_stage(): void
|
|
{
|
|
// Test rejection at secretary stage
|
|
$doc1 = $this->createFinanceDocument(['status' => FinanceDocument::STATUS_PENDING]);
|
|
$this->actingAs($this->secretary)->post(
|
|
route('admin.finance.reject', $doc1),
|
|
['rejection_reason' => 'Missing documentation']
|
|
);
|
|
$doc1->refresh();
|
|
$this->assertEquals(FinanceDocument::STATUS_REJECTED, $doc1->status);
|
|
|
|
// Test rejection at chair stage
|
|
$doc2 = $this->createDocumentAtStage('secretary_approved', ['amount' => 25000]);
|
|
$this->actingAs($this->chair)->post(
|
|
route('admin.finance.reject', $doc2),
|
|
['rejection_reason' => 'Amount exceeds policy limit']
|
|
);
|
|
$doc2->refresh();
|
|
$this->assertEquals(FinanceDocument::STATUS_REJECTED, $doc2->status);
|
|
|
|
// Test rejection at board stage
|
|
$doc3 = $this->createDocumentAtStage('chair_approved', ['amount' => 75000]);
|
|
$this->actingAs($this->boardMember)->post(
|
|
route('admin.finance.reject', $doc3),
|
|
['rejection_reason' => 'Not within budget allocation']
|
|
);
|
|
$doc3->refresh();
|
|
$this->assertEquals(FinanceDocument::STATUS_REJECTED, $doc3->status);
|
|
}
|
|
|
|
/**
|
|
* Test amount tier determination
|
|
*/
|
|
public function test_amount_tier_determination(): void
|
|
{
|
|
$smallDoc = $this->createFinanceDocument(['amount' => 3000]);
|
|
$this->assertEquals(FinanceDocument::AMOUNT_TIER_SMALL, $smallDoc->determineAmountTier());
|
|
|
|
$mediumDoc = $this->createFinanceDocument(['amount' => 25000]);
|
|
$this->assertEquals(FinanceDocument::AMOUNT_TIER_MEDIUM, $mediumDoc->determineAmountTier());
|
|
|
|
$largeDoc = $this->createFinanceDocument(['amount' => 75000]);
|
|
$this->assertEquals(FinanceDocument::AMOUNT_TIER_LARGE, $largeDoc->determineAmountTier());
|
|
}
|
|
|
|
/**
|
|
* Test document status constants match workflow
|
|
*/
|
|
public function test_document_status_constants(): void
|
|
{
|
|
$this->assertEquals('pending', FinanceDocument::STATUS_PENDING);
|
|
$this->assertEquals('approved_secretary', FinanceDocument::STATUS_APPROVED_SECRETARY);
|
|
$this->assertEquals('approved_chair', FinanceDocument::STATUS_APPROVED_CHAIR);
|
|
$this->assertEquals('approved_board', FinanceDocument::STATUS_APPROVED_BOARD);
|
|
$this->assertEquals('rejected', FinanceDocument::STATUS_REJECTED);
|
|
}
|
|
}
|