Files
usher-manage-stack/tests/Feature/FinanceDocumentWorkflowTest.php

369 lines
13 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\FinanceDocument;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
use Tests\TestCase;
/**
* Financial Document Workflow Feature Tests
*
* Tests the complete financial document workflow including:
* - Amount-based routing
* - Multi-stage approval
* - Permission-based access control
*/
class FinanceDocumentWorkflowTest extends TestCase
{
use RefreshDatabase, WithFaker;
protected User $requester;
protected User $cashier;
protected User $accountant;
protected User $chair;
protected User $boardMember;
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware([\App\Http\Middleware\EnsureUserIsAdmin::class]);
Permission::findOrCreate('create_finance_document', 'web');
Permission::findOrCreate('view_finance_documents', 'web');
Permission::findOrCreate('approve_as_cashier', 'web');
Permission::findOrCreate('approve_as_accountant', 'web');
Permission::findOrCreate('approve_as_chair', 'web');
Permission::findOrCreate('approve_board_meeting', 'web');
Role::firstOrCreate(['name' => 'admin']);
// Create roles
Role::create(['name' => 'finance_requester']);
Role::create(['name' => 'finance_cashier']);
Role::create(['name' => 'finance_accountant']);
Role::create(['name' => 'finance_chair']);
Role::create(['name' => 'finance_board_member']);
// Create test users
$this->requester = User::factory()->create(['email' => 'requester@test.com']);
$this->cashier = User::factory()->create(['email' => 'cashier@test.com']);
$this->accountant = User::factory()->create(['email' => 'accountant@test.com']);
$this->chair = User::factory()->create(['email' => 'chair@test.com']);
$this->boardMember = User::factory()->create(['email' => 'board@test.com']);
// Assign roles
$this->requester->assignRole('admin');
$this->cashier->assignRole('admin');
$this->accountant->assignRole('admin');
$this->chair->assignRole('admin');
$this->boardMember->assignRole('admin');
$this->requester->assignRole('finance_requester');
$this->cashier->assignRole('finance_cashier');
$this->accountant->assignRole('finance_accountant');
$this->chair->assignRole('finance_chair');
$this->boardMember->assignRole('finance_board_member');
// Give permissions
$this->requester->givePermissionTo('create_finance_document');
$this->cashier->givePermissionTo(['view_finance_documents', 'approve_as_cashier']);
$this->accountant->givePermissionTo(['view_finance_documents', 'approve_as_accountant']);
$this->chair->givePermissionTo(['view_finance_documents', 'approve_as_chair']);
$this->boardMember->givePermissionTo('approve_board_meeting');
}
/** @test */
public function small_amount_workflow_completes_without_chair()
{
// Create a small amount document (< 5000)
$document = FinanceDocument::create([
'title' => 'Small Expense Reimbursement',
'description' => 'Test small expense',
'amount' => 3000,
'request_type' => 'expense_reimbursement',
'status' => 'pending',
'submitted_by_id' => $this->requester->id,
'submitted_at' => now(),
]);
$document->amount_tier = $document->determineAmountTier();
$document->save();
$this->assertEquals('small', $document->amount_tier);
// Cashier approves
$this->actingAs($this->cashier);
$document->status = FinanceDocument::STATUS_APPROVED_CASHIER;
$document->cashier_approved_by_id = $this->cashier->id;
$document->cashier_approved_at = now();
$document->save();
$this->assertFalse($document->isApprovalStageComplete());
// Accountant approves (should complete workflow for small amounts)
$this->actingAs($this->accountant);
$document->status = FinanceDocument::STATUS_APPROVED_ACCOUNTANT;
$document->accountant_approved_by_id = $this->accountant->id;
$document->accountant_approved_at = now();
$document->save();
$this->assertTrue($document->isApprovalStageComplete());
$this->assertEquals('approval', $document->getCurrentWorkflowStage()); // Ready for payment stage
}
/** @test */
public function medium_amount_workflow_requires_chair_approval()
{
// Create a medium amount document (5000-50000)
$document = FinanceDocument::create([
'title' => 'Medium Purchase Request',
'description' => 'Test medium purchase',
'amount' => 25000,
'request_type' => 'purchase_request',
'status' => 'pending',
'submitted_by_id' => $this->requester->id,
'submitted_at' => now(),
]);
$document->amount_tier = $document->determineAmountTier();
$document->save();
$this->assertEquals('medium', $document->amount_tier);
$this->assertFalse($document->needsBoardMeetingApproval());
// Cashier approves
$document->status = FinanceDocument::STATUS_APPROVED_CASHIER;
$document->cashier_approved_by_id = $this->cashier->id;
$document->cashier_approved_at = now();
$document->save();
$this->assertFalse($document->isApprovalStageComplete());
// Accountant approves
$document->status = FinanceDocument::STATUS_APPROVED_ACCOUNTANT;
$document->accountant_approved_by_id = $this->accountant->id;
$document->accountant_approved_at = now();
$document->save();
$this->assertFalse($document->isApprovalStageComplete()); // Still needs chair
// Chair approves (should complete workflow for medium amounts)
$document->status = FinanceDocument::STATUS_APPROVED_CHAIR;
$document->chair_approved_by_id = $this->chair->id;
$document->chair_approved_at = now();
$document->save();
$this->assertTrue($document->isApprovalStageComplete());
}
/** @test */
public function large_amount_workflow_requires_board_meeting_approval()
{
// Create a large amount document (> 50000)
$document = FinanceDocument::create([
'title' => 'Large Capital Expenditure',
'description' => 'Test large expenditure',
'amount' => 75000,
'request_type' => 'purchase_request',
'status' => 'pending',
'submitted_by_id' => $this->requester->id,
'submitted_at' => now(),
]);
$document->amount_tier = $document->determineAmountTier();
$document->requires_board_meeting = $document->needsBoardMeetingApproval();
$document->save();
$this->assertEquals('large', $document->amount_tier);
$this->assertTrue($document->requires_board_meeting);
// Cashier approves
$document->status = FinanceDocument::STATUS_APPROVED_CASHIER;
$document->cashier_approved_by_id = $this->cashier->id;
$document->cashier_approved_at = now();
$document->save();
// Accountant approves
$document->status = FinanceDocument::STATUS_APPROVED_ACCOUNTANT;
$document->accountant_approved_by_id = $this->accountant->id;
$document->accountant_approved_at = now();
$document->save();
// Chair approves
$document->status = FinanceDocument::STATUS_APPROVED_CHAIR;
$document->chair_approved_by_id = $this->chair->id;
$document->chair_approved_at = now();
$document->save();
$this->assertFalse($document->isApprovalStageComplete()); // Still needs board meeting
// Board meeting approval
$document->board_meeting_approved_at = now();
$document->board_meeting_approved_by_id = $this->boardMember->id;
$document->save();
$this->assertTrue($document->isApprovalStageComplete());
}
/** @test */
public function requester_can_create_finance_document()
{
Storage::fake('local');
$this->actingAs($this->requester);
$file = UploadedFile::fake()->create('receipt.pdf', 100);
$response = $this->post(route('admin.finance.store'), [
'title' => 'Test Expense',
'description' => 'Test description',
'amount' => 5000,
'request_type' => 'expense_reimbursement',
'attachment' => $file,
]);
$response->assertRedirect();
$this->assertDatabaseHas('finance_documents', [
'title' => 'Test Expense',
'amount' => 5000,
'submitted_by_id' => $this->requester->id,
]);
}
/** @test */
public function cashier_cannot_approve_own_submission()
{
$document = FinanceDocument::create([
'title' => 'Self Submitted',
'description' => 'Test',
'amount' => 1000,
'request_type' => 'petty_cash',
'status' => 'pending',
'submitted_by_id' => $this->cashier->id,
'submitted_at' => now(),
]);
$this->assertFalse($document->canBeApprovedByCashier($this->cashier));
}
/** @test */
public function accountant_cannot_approve_before_cashier()
{
$document = FinanceDocument::create([
'title' => 'Pending Document',
'description' => 'Test',
'amount' => 1000,
'request_type' => 'petty_cash',
'status' => 'pending',
'submitted_by_id' => $this->requester->id,
'submitted_at' => now(),
]);
$this->assertFalse($document->canBeApprovedByAccountant());
}
/** @test */
public function rejected_document_cannot_proceed()
{
$document = FinanceDocument::create([
'title' => 'Rejected Document',
'description' => 'Test',
'amount' => 1000,
'request_type' => 'petty_cash',
'status' => FinanceDocument::STATUS_REJECTED,
'submitted_by_id' => $this->requester->id,
'submitted_at' => now(),
]);
$this->assertFalse($document->canBeApprovedByCashier($this->cashier));
$this->assertFalse($document->canBeApprovedByAccountant());
$this->assertFalse($document->canBeApprovedByChair());
}
/** @test */
public function workflow_stages_are_correctly_identified()
{
$document = FinanceDocument::create([
'title' => 'Test Document',
'description' => 'Test',
'amount' => 1000,
'request_type' => 'petty_cash',
'status' => 'pending',
'submitted_by_id' => $this->requester->id,
'submitted_at' => now(),
]);
$document->amount_tier = 'small';
$document->save();
// Stage 1: Approval
$this->assertEquals('approval', $document->getCurrentWorkflowStage());
$this->assertFalse($document->isPaymentCompleted());
$this->assertFalse($document->isRecordingComplete());
// Complete approval
$document->status = FinanceDocument::STATUS_APPROVED_ACCOUNTANT;
$document->cashier_approved_at = now();
$document->accountant_approved_at = now();
$document->save();
$this->assertTrue($document->isApprovalStageComplete());
// Stage 2: Payment (simulate payment order created and executed)
$document->payment_order_created_at = now();
$document->payment_verified_at = now();
$document->payment_executed_at = now();
$document->save();
$this->assertTrue($document->isPaymentCompleted());
$this->assertEquals('payment', $document->getCurrentWorkflowStage());
// Stage 3: Recording
$document->cashier_recorded_at = now();
$document->save();
$this->assertTrue($document->isRecordingComplete());
// Stage 4: Reconciliation
$this->assertEquals('reconciliation', $document->getCurrentWorkflowStage());
$document->bank_reconciliation_id = 1; // Simulate reconciliation
$document->save();
$this->assertTrue($document->isReconciled());
$this->assertEquals('completed', $document->getCurrentWorkflowStage());
}
/** @test */
public function amount_tier_is_automatically_determined()
{
$smallDoc = FinanceDocument::factory()->make(['amount' => 3000]);
$this->assertEquals('small', $smallDoc->determineAmountTier());
$mediumDoc = FinanceDocument::factory()->make(['amount' => 25000]);
$this->assertEquals('medium', $mediumDoc->determineAmountTier());
$largeDoc = FinanceDocument::factory()->make(['amount' => 75000]);
$this->assertEquals('large', $largeDoc->determineAmountTier());
}
/** @test */
public function board_meeting_requirement_is_correctly_identified()
{
$smallDoc = FinanceDocument::factory()->make(['amount' => 3000]);
$this->assertFalse($smallDoc->needsBoardMeetingApproval());
$mediumDoc = FinanceDocument::factory()->make(['amount' => 25000]);
$this->assertFalse($mediumDoc->needsBoardMeetingApproval());
$largeDoc = FinanceDocument::factory()->make(['amount' => 75000]);
$this->assertTrue($largeDoc->needsBoardMeetingApproval());
}
}