Add phone login support and member import functionality
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>
This commit is contained in:
@@ -16,7 +16,7 @@ use Tests\TestCase;
|
||||
* Financial Document Workflow Feature Tests
|
||||
*
|
||||
* Tests the complete financial document workflow including:
|
||||
* - Amount-based routing
|
||||
* - Amount-based routing (secretary → chair → board)
|
||||
* - Multi-stage approval
|
||||
* - Permission-based access control
|
||||
*/
|
||||
@@ -25,11 +25,17 @@ class FinanceDocumentWorkflowTest extends TestCase
|
||||
use RefreshDatabase, WithFaker;
|
||||
|
||||
protected User $requester;
|
||||
protected User $cashier;
|
||||
protected User $accountant;
|
||||
|
||||
protected User $secretary;
|
||||
|
||||
protected User $chair;
|
||||
|
||||
protected User $boardMember;
|
||||
|
||||
protected User $cashier;
|
||||
|
||||
protected User $accountant;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
@@ -38,42 +44,45 @@ class FinanceDocumentWorkflowTest extends TestCase
|
||||
|
||||
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_secretary', 'web');
|
||||
Permission::findOrCreate('approve_as_chair', 'web');
|
||||
Permission::findOrCreate('approve_board_meeting', 'web');
|
||||
|
||||
Role::firstOrCreate(['name' => 'admin']);
|
||||
// Create roles
|
||||
// Create roles for new workflow
|
||||
Role::create(['name' => 'finance_requester']);
|
||||
Role::create(['name' => 'finance_cashier']);
|
||||
Role::create(['name' => 'finance_accountant']);
|
||||
Role::create(['name' => 'secretary_general']);
|
||||
Role::create(['name' => 'finance_chair']);
|
||||
Role::create(['name' => 'finance_board_member']);
|
||||
Role::create(['name' => 'finance_cashier']);
|
||||
Role::create(['name' => 'finance_accountant']);
|
||||
|
||||
// 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->secretary = User::factory()->create(['email' => 'secretary@test.com']);
|
||||
$this->chair = User::factory()->create(['email' => 'chair@test.com']);
|
||||
$this->boardMember = User::factory()->create(['email' => 'board@test.com']);
|
||||
$this->cashier = User::factory()->create(['email' => 'cashier@test.com']);
|
||||
$this->accountant = User::factory()->create(['email' => 'accountant@test.com']);
|
||||
|
||||
// Assign roles
|
||||
$this->requester->assignRole('admin');
|
||||
$this->cashier->assignRole('admin');
|
||||
$this->accountant->assignRole('admin');
|
||||
$this->secretary->assignRole('admin');
|
||||
$this->chair->assignRole('admin');
|
||||
$this->boardMember->assignRole('admin');
|
||||
$this->cashier->assignRole('admin');
|
||||
$this->accountant->assignRole('admin');
|
||||
|
||||
$this->requester->assignRole('finance_requester');
|
||||
$this->cashier->assignRole('finance_cashier');
|
||||
$this->accountant->assignRole('finance_accountant');
|
||||
$this->secretary->assignRole('secretary_general');
|
||||
$this->chair->assignRole('finance_chair');
|
||||
$this->boardMember->assignRole('finance_board_member');
|
||||
$this->cashier->assignRole('finance_cashier');
|
||||
$this->accountant->assignRole('finance_accountant');
|
||||
|
||||
// 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->secretary->givePermissionTo(['view_finance_documents', 'approve_as_secretary']);
|
||||
$this->chair->givePermissionTo(['view_finance_documents', 'approve_as_chair']);
|
||||
$this->boardMember->givePermissionTo('approve_board_meeting');
|
||||
}
|
||||
@@ -86,9 +95,8 @@ class FinanceDocumentWorkflowTest extends TestCase
|
||||
'title' => 'Small Expense Reimbursement',
|
||||
'description' => 'Test small expense',
|
||||
'amount' => 3000,
|
||||
'request_type' => 'expense_reimbursement',
|
||||
'status' => 'pending',
|
||||
'submitted_by_id' => $this->requester->id,
|
||||
'status' => FinanceDocument::STATUS_PENDING,
|
||||
'submitted_by_user_id' => $this->requester->id,
|
||||
'submitted_at' => now(),
|
||||
]);
|
||||
|
||||
@@ -97,24 +105,14 @@ class FinanceDocumentWorkflowTest extends TestCase
|
||||
|
||||
$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();
|
||||
// Secretary approves (should complete workflow for small amounts)
|
||||
$this->actingAs($this->secretary);
|
||||
$document->status = FinanceDocument::STATUS_APPROVED_SECRETARY;
|
||||
$document->approved_by_secretary_id = $this->secretary->id;
|
||||
$document->secretary_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
|
||||
$this->assertTrue($document->isApprovalComplete());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@@ -125,9 +123,8 @@ class FinanceDocumentWorkflowTest extends TestCase
|
||||
'title' => 'Medium Purchase Request',
|
||||
'description' => 'Test medium purchase',
|
||||
'amount' => 25000,
|
||||
'request_type' => 'purchase_request',
|
||||
'status' => 'pending',
|
||||
'submitted_by_id' => $this->requester->id,
|
||||
'status' => FinanceDocument::STATUS_PENDING,
|
||||
'submitted_by_user_id' => $this->requester->id,
|
||||
'submitted_at' => now(),
|
||||
]);
|
||||
|
||||
@@ -137,29 +134,21 @@ class FinanceDocumentWorkflowTest extends TestCase
|
||||
$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();
|
||||
// Secretary approves
|
||||
$document->status = FinanceDocument::STATUS_APPROVED_SECRETARY;
|
||||
$document->approved_by_secretary_id = $this->secretary->id;
|
||||
$document->secretary_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
|
||||
$this->assertFalse($document->isApprovalComplete()); // 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->approved_by_chair_id = $this->chair->id;
|
||||
$document->chair_approved_at = now();
|
||||
$document->save();
|
||||
|
||||
$this->assertTrue($document->isApprovalStageComplete());
|
||||
$this->assertTrue($document->isApprovalComplete());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@@ -170,9 +159,8 @@ class FinanceDocumentWorkflowTest extends TestCase
|
||||
'title' => 'Large Capital Expenditure',
|
||||
'description' => 'Test large expenditure',
|
||||
'amount' => 75000,
|
||||
'request_type' => 'purchase_request',
|
||||
'status' => 'pending',
|
||||
'submitted_by_id' => $this->requester->id,
|
||||
'status' => FinanceDocument::STATUS_PENDING,
|
||||
'submitted_by_user_id' => $this->requester->id,
|
||||
'submitted_at' => now(),
|
||||
]);
|
||||
|
||||
@@ -183,32 +171,29 @@ class FinanceDocumentWorkflowTest extends TestCase
|
||||
$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();
|
||||
// Secretary approves
|
||||
$document->status = FinanceDocument::STATUS_APPROVED_SECRETARY;
|
||||
$document->approved_by_secretary_id = $this->secretary->id;
|
||||
$document->secretary_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();
|
||||
$this->assertFalse($document->isApprovalComplete());
|
||||
|
||||
// Chair approves
|
||||
$document->status = FinanceDocument::STATUS_APPROVED_CHAIR;
|
||||
$document->chair_approved_by_id = $this->chair->id;
|
||||
$document->approved_by_chair_id = $this->chair->id;
|
||||
$document->chair_approved_at = now();
|
||||
$document->save();
|
||||
|
||||
$this->assertFalse($document->isApprovalStageComplete()); // Still needs board meeting
|
||||
$this->assertFalse($document->isApprovalComplete()); // Still needs board
|
||||
|
||||
// Board meeting approval
|
||||
// Board approval
|
||||
$document->status = FinanceDocument::STATUS_APPROVED_BOARD;
|
||||
$document->board_meeting_approved_at = now();
|
||||
$document->board_meeting_approved_by_id = $this->boardMember->id;
|
||||
$document->save();
|
||||
|
||||
$this->assertTrue($document->isApprovalStageComplete());
|
||||
$this->assertTrue($document->isApprovalComplete());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@@ -224,7 +209,6 @@ class FinanceDocumentWorkflowTest extends TestCase
|
||||
'title' => 'Test Expense',
|
||||
'description' => 'Test description',
|
||||
'amount' => 5000,
|
||||
'request_type' => 'expense_reimbursement',
|
||||
'attachment' => $file,
|
||||
]);
|
||||
|
||||
@@ -232,40 +216,43 @@ class FinanceDocumentWorkflowTest extends TestCase
|
||||
$this->assertDatabaseHas('finance_documents', [
|
||||
'title' => 'Test Expense',
|
||||
'amount' => 5000,
|
||||
'submitted_by_id' => $this->requester->id,
|
||||
'submitted_by_user_id' => $this->requester->id,
|
||||
]);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function cashier_cannot_approve_own_submission()
|
||||
{
|
||||
// Test using canBeApprovedBySecretary (secretary is first approval in new workflow)
|
||||
$document = FinanceDocument::create([
|
||||
'title' => 'Self Submitted',
|
||||
'description' => 'Test',
|
||||
'amount' => 1000,
|
||||
'request_type' => 'petty_cash',
|
||||
'status' => 'pending',
|
||||
'submitted_by_id' => $this->cashier->id,
|
||||
'status' => FinanceDocument::STATUS_PENDING,
|
||||
'submitted_by_user_id' => $this->secretary->id,
|
||||
'submitted_at' => now(),
|
||||
]);
|
||||
|
||||
$this->assertFalse($document->canBeApprovedByCashier($this->cashier));
|
||||
// Secretary cannot approve their own submission
|
||||
$this->assertFalse($document->canBeApprovedBySecretary($this->secretary));
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function accountant_cannot_approve_before_cashier()
|
||||
{
|
||||
// In new workflow: chair cannot approve before secretary
|
||||
$document = FinanceDocument::create([
|
||||
'title' => 'Pending Document',
|
||||
'description' => 'Test',
|
||||
'amount' => 1000,
|
||||
'request_type' => 'petty_cash',
|
||||
'status' => 'pending',
|
||||
'submitted_by_id' => $this->requester->id,
|
||||
'amount' => 25000, // Medium amount needs chair
|
||||
'amount_tier' => FinanceDocument::AMOUNT_TIER_MEDIUM,
|
||||
'status' => FinanceDocument::STATUS_PENDING,
|
||||
'submitted_by_user_id' => $this->requester->id,
|
||||
'submitted_at' => now(),
|
||||
]);
|
||||
|
||||
$this->assertFalse($document->canBeApprovedByAccountant());
|
||||
// Chair cannot approve before secretary
|
||||
$this->assertFalse($document->canBeApprovedByChair());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@@ -275,15 +262,14 @@ class FinanceDocumentWorkflowTest extends TestCase
|
||||
'title' => 'Rejected Document',
|
||||
'description' => 'Test',
|
||||
'amount' => 1000,
|
||||
'request_type' => 'petty_cash',
|
||||
'status' => FinanceDocument::STATUS_REJECTED,
|
||||
'submitted_by_id' => $this->requester->id,
|
||||
'submitted_by_user_id' => $this->requester->id,
|
||||
'submitted_at' => now(),
|
||||
]);
|
||||
|
||||
$this->assertFalse($document->canBeApprovedByCashier($this->cashier));
|
||||
$this->assertFalse($document->canBeApprovedByAccountant());
|
||||
$this->assertFalse($document->canBeApprovedBySecretary($this->secretary));
|
||||
$this->assertFalse($document->canBeApprovedByChair());
|
||||
$this->assertFalse($document->canBeApprovedByBoard());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
@@ -293,9 +279,8 @@ class FinanceDocumentWorkflowTest extends TestCase
|
||||
'title' => 'Test Document',
|
||||
'description' => 'Test',
|
||||
'amount' => 1000,
|
||||
'request_type' => 'petty_cash',
|
||||
'status' => 'pending',
|
||||
'submitted_by_id' => $this->requester->id,
|
||||
'status' => FinanceDocument::STATUS_PENDING,
|
||||
'submitted_by_user_id' => $this->requester->id,
|
||||
'submitted_at' => now(),
|
||||
]);
|
||||
|
||||
@@ -303,41 +288,34 @@ class FinanceDocumentWorkflowTest extends TestCase
|
||||
$document->save();
|
||||
|
||||
// Stage 1: Approval
|
||||
$this->assertEquals('approval', $document->getCurrentWorkflowStage());
|
||||
$this->assertFalse($document->isPaymentCompleted());
|
||||
$this->assertFalse($document->isApprovalComplete());
|
||||
$this->assertFalse($document->isDisbursementComplete());
|
||||
$this->assertFalse($document->isRecordingComplete());
|
||||
|
||||
// Complete approval
|
||||
$document->status = FinanceDocument::STATUS_APPROVED_ACCOUNTANT;
|
||||
$document->cashier_approved_at = now();
|
||||
$document->accountant_approved_at = now();
|
||||
// Complete approval (secretary only for small)
|
||||
$document->status = FinanceDocument::STATUS_APPROVED_SECRETARY;
|
||||
$document->approved_by_secretary_id = $this->secretary->id;
|
||||
$document->secretary_approved_at = now();
|
||||
$document->save();
|
||||
|
||||
$this->assertTrue($document->isApprovalStageComplete());
|
||||
$this->assertTrue($document->isApprovalComplete());
|
||||
|
||||
// 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();
|
||||
// Stage 2: Disbursement (dual confirmation)
|
||||
$document->requester_confirmed_at = now();
|
||||
$document->requester_confirmed_by_id = $this->requester->id;
|
||||
$document->cashier_confirmed_at = now();
|
||||
$document->cashier_confirmed_by_id = $this->cashier->id;
|
||||
$document->save();
|
||||
|
||||
$this->assertTrue($document->isPaymentCompleted());
|
||||
$this->assertEquals('payment', $document->getCurrentWorkflowStage());
|
||||
$this->assertTrue($document->isDisbursementComplete());
|
||||
|
||||
// Stage 3: Recording
|
||||
$document->cashier_recorded_at = now();
|
||||
$document->accountant_recorded_at = now();
|
||||
$document->accountant_recorded_by_id = $this->accountant->id;
|
||||
$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());
|
||||
$this->assertTrue($document->isFullyProcessed());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
|
||||
Reference in New Issue
Block a user