Files
usher-manage-stack/tests/Unit/FinanceDocumentTest.php
gbanyan 42099759e8 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>
2026-01-25 03:08:06 +08:00

300 lines
8.9 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace Tests\Unit;
use App\Models\FinanceDocument;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
/**
* Finance Document Model Unit Tests
*
* Tests business logic methods in FinanceDocument model
* Using new workflow: Secretary → Chair → Board
*/
class FinanceDocumentTest extends TestCase
{
use RefreshDatabase;
/** @test */
public function it_determines_small_amount_tier_correctly()
{
$document = new FinanceDocument(['amount' => 4999]);
$this->assertEquals('small', $document->determineAmountTier());
$document->amount = 3000;
$this->assertEquals('small', $document->determineAmountTier());
$document->amount = 1;
$this->assertEquals('small', $document->determineAmountTier());
}
/** @test */
public function it_determines_medium_amount_tier_correctly()
{
$document = new FinanceDocument(['amount' => 5000]);
$this->assertEquals('medium', $document->determineAmountTier());
$document->amount = 25000;
$this->assertEquals('medium', $document->determineAmountTier());
$document->amount = 50000;
$this->assertEquals('medium', $document->determineAmountTier());
}
/** @test */
public function it_determines_large_amount_tier_correctly()
{
$document = new FinanceDocument(['amount' => 50001]);
$this->assertEquals('large', $document->determineAmountTier());
$document->amount = 100000;
$this->assertEquals('large', $document->determineAmountTier());
$document->amount = 1000000;
$this->assertEquals('large', $document->determineAmountTier());
}
/** @test */
public function small_amount_does_not_need_board_meeting()
{
$document = new FinanceDocument(['amount' => 4999]);
$this->assertFalse($document->needsBoardMeetingApproval());
}
/** @test */
public function medium_amount_does_not_need_board_meeting()
{
$document = new FinanceDocument(['amount' => 50000]);
$this->assertFalse($document->needsBoardMeetingApproval());
}
/** @test */
public function large_amount_needs_board_meeting()
{
$document = new FinanceDocument(['amount' => 50001]);
$this->assertTrue($document->needsBoardMeetingApproval());
}
/** @test */
public function small_amount_approval_stage_is_complete_after_secretary()
{
$document = new FinanceDocument([
'amount' => 3000,
'amount_tier' => 'small',
'status' => FinanceDocument::STATUS_APPROVED_SECRETARY,
]);
$this->assertTrue($document->isApprovalComplete());
}
/** @test */
public function medium_amount_approval_stage_needs_chair()
{
$document = new FinanceDocument([
'amount' => 25000,
'amount_tier' => 'medium',
'status' => FinanceDocument::STATUS_APPROVED_SECRETARY,
]);
$this->assertFalse($document->isApprovalComplete());
$document->status = FinanceDocument::STATUS_APPROVED_CHAIR;
$this->assertTrue($document->isApprovalComplete());
}
/** @test */
public function large_amount_approval_stage_needs_chair_and_board()
{
$document = new FinanceDocument([
'amount' => 75000,
'amount_tier' => 'large',
'status' => FinanceDocument::STATUS_APPROVED_CHAIR,
]);
$this->assertFalse($document->isApprovalComplete());
$document->status = FinanceDocument::STATUS_APPROVED_BOARD;
$this->assertTrue($document->isApprovalComplete());
}
/** @test */
public function secretary_cannot_approve_own_submission()
{
$user = User::factory()->create();
$document = new FinanceDocument([
'submitted_by_user_id' => $user->id,
'status' => FinanceDocument::STATUS_PENDING,
]);
$this->assertFalse($document->canBeApprovedBySecretary($user));
}
/** @test */
public function secretary_can_approve_others_submission()
{
$submitter = User::factory()->create();
$secretary = User::factory()->create();
$document = new FinanceDocument([
'submitted_by_user_id' => $submitter->id,
'status' => FinanceDocument::STATUS_PENDING,
]);
$this->assertTrue($document->canBeApprovedBySecretary($secretary));
}
/** @test */
public function chair_cannot_approve_before_secretary()
{
$document = new FinanceDocument([
'status' => FinanceDocument::STATUS_PENDING,
'amount_tier' => 'medium',
]);
$this->assertFalse($document->canBeApprovedByChair());
}
/** @test */
public function chair_can_approve_after_secretary()
{
$document = new FinanceDocument([
'status' => FinanceDocument::STATUS_APPROVED_SECRETARY,
'amount_tier' => 'medium',
]);
$this->assertTrue($document->canBeApprovedByChair());
}
/** @test */
public function chair_can_approve_after_secretary_for_medium_amounts()
{
$document = new FinanceDocument([
'status' => FinanceDocument::STATUS_APPROVED_SECRETARY,
'amount_tier' => 'medium',
]);
$this->assertTrue($document->canBeApprovedByChair());
}
/** @test */
public function payment_order_can_be_created_after_approval_stage()
{
$document = new FinanceDocument([
'amount' => 3000,
'amount_tier' => 'small',
'status' => FinanceDocument::STATUS_APPROVED_SECRETARY,
]);
$this->assertTrue($document->canCreatePaymentOrder());
}
/** @test */
public function payment_order_cannot_be_created_before_approval_complete()
{
$document = new FinanceDocument([
'status' => FinanceDocument::STATUS_PENDING,
'amount_tier' => 'small',
]);
$this->assertFalse($document->canCreatePaymentOrder());
}
/** @test */
public function disbursement_requires_dual_confirmation()
{
$document = new FinanceDocument([
'amount' => 3000,
'amount_tier' => 'small',
'status' => FinanceDocument::STATUS_APPROVED_SECRETARY,
]);
$this->assertFalse($document->isDisbursementComplete());
// Only requester confirmed
$document->requester_confirmed_at = now();
$this->assertFalse($document->isDisbursementComplete());
// Both confirmed
$document->cashier_confirmed_at = now();
$this->assertTrue($document->isDisbursementComplete());
}
/** @test */
public function payment_completed_check_works()
{
$document = new FinanceDocument([
'payment_order_created_at' => now(),
'payment_verified_at' => now(),
'payment_executed_at' => null,
]);
$this->assertFalse($document->isPaymentCompleted());
$document->payment_executed_at = now();
$this->assertTrue($document->isPaymentCompleted());
}
/** @test */
public function recording_complete_check_works()
{
$document = new FinanceDocument([
'accountant_recorded_at' => null,
]);
$this->assertFalse($document->isRecordingComplete());
$document->accountant_recorded_at = now();
$this->assertTrue($document->isRecordingComplete());
}
/** @test */
public function reconciled_check_works()
{
$document = new FinanceDocument([
'bank_reconciliation_id' => null,
]);
$this->assertFalse($document->isReconciled());
$document->bank_reconciliation_id = 1;
$this->assertTrue($document->isReconciled());
}
/** @test */
public function amount_tier_text_is_correct()
{
$small = new FinanceDocument(['amount_tier' => 'small']);
$this->assertEquals('小額(< 5000', $small->getAmountTierText());
$medium = new FinanceDocument(['amount_tier' => 'medium']);
$this->assertEquals('中額5000-50000', $medium->getAmountTierText());
$large = new FinanceDocument(['amount_tier' => 'large']);
$this->assertEquals('大額(> 50000', $large->getAmountTierText());
}
/** @test */
public function fully_processed_check_works()
{
$document = new FinanceDocument([
'amount' => 3000,
'amount_tier' => 'small',
'status' => FinanceDocument::STATUS_APPROVED_SECRETARY,
]);
$this->assertFalse($document->isFullyProcessed());
// Complete disbursement
$document->requester_confirmed_at = now();
$document->cashier_confirmed_at = now();
$this->assertFalse($document->isFullyProcessed());
// Complete recording
$document->accountant_recorded_at = now();
$this->assertTrue($document->isFullyProcessed());
}
}