Initial commit

This commit is contained in:
2025-11-20 23:21:05 +08:00
commit 13bc6db529
378 changed files with 54527 additions and 0 deletions

View File

@@ -0,0 +1,312 @@
<?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
*/
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_accountant()
{
$document = new FinanceDocument([
'amount' => 3000,
'amount_tier' => 'small',
'status' => FinanceDocument::STATUS_APPROVED_ACCOUNTANT,
]);
$this->assertTrue($document->isApprovalStageComplete());
}
/** @test */
public function medium_amount_approval_stage_needs_chair()
{
$document = new FinanceDocument([
'amount' => 25000,
'amount_tier' => 'medium',
'status' => FinanceDocument::STATUS_APPROVED_ACCOUNTANT,
]);
$this->assertFalse($document->isApprovalStageComplete());
$document->status = FinanceDocument::STATUS_APPROVED_CHAIR;
$this->assertTrue($document->isApprovalStageComplete());
}
/** @test */
public function large_amount_approval_stage_needs_chair_and_board()
{
$document = new FinanceDocument([
'amount' => 75000,
'amount_tier' => 'large',
'status' => FinanceDocument::STATUS_APPROVED_CHAIR,
'board_meeting_approved_at' => null,
]);
$this->assertFalse($document->isApprovalStageComplete());
$document->board_meeting_approved_at = now();
$this->assertTrue($document->isApprovalStageComplete());
}
/** @test */
public function cashier_cannot_approve_own_submission()
{
$user = User::factory()->create();
$document = new FinanceDocument([
'submitted_by_id' => $user->id,
'status' => 'pending',
]);
$this->assertFalse($document->canBeApprovedByCashier($user));
}
/** @test */
public function cashier_can_approve_others_submission()
{
$submitter = User::factory()->create();
$cashier = User::factory()->create();
$document = new FinanceDocument([
'submitted_by_id' => $submitter->id,
'status' => 'pending',
]);
$this->assertTrue($document->canBeApprovedByCashier($cashier));
}
/** @test */
public function accountant_cannot_approve_before_cashier()
{
$document = new FinanceDocument([
'status' => 'pending',
]);
$this->assertFalse($document->canBeApprovedByAccountant());
}
/** @test */
public function accountant_can_approve_after_cashier()
{
$document = new FinanceDocument([
'status' => FinanceDocument::STATUS_APPROVED_CASHIER,
]);
$this->assertTrue($document->canBeApprovedByAccountant());
}
/** @test */
public function chair_cannot_approve_before_accountant()
{
$document = new FinanceDocument([
'status' => FinanceDocument::STATUS_APPROVED_CASHIER,
'amount_tier' => 'medium',
]);
$this->assertFalse($document->canBeApprovedByChair());
}
/** @test */
public function chair_can_approve_after_accountant_for_medium_amounts()
{
$document = new FinanceDocument([
'status' => FinanceDocument::STATUS_APPROVED_ACCOUNTANT,
'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_ACCOUNTANT,
]);
$this->assertTrue($document->canCreatePaymentOrder());
}
/** @test */
public function payment_order_cannot_be_created_before_approval_complete()
{
$document = new FinanceDocument([
'status' => FinanceDocument::STATUS_APPROVED_CASHIER,
]);
$this->assertFalse($document->canCreatePaymentOrder());
}
/** @test */
public function workflow_stages_are_correctly_identified()
{
$document = new FinanceDocument([
'status' => 'pending',
'amount_tier' => 'small',
]);
// Stage 1: Approval
$this->assertEquals('approval', $document->getCurrentWorkflowStage());
// Stage 2: Payment
$document->status = FinanceDocument::STATUS_APPROVED_ACCOUNTANT;
$document->cashier_approved_at = now();
$document->accountant_approved_at = now();
$document->payment_order_created_at = now();
$this->assertEquals('payment', $document->getCurrentWorkflowStage());
// Stage 3: Recording
$document->payment_executed_at = now();
$this->assertEquals('payment', $document->getCurrentWorkflowStage());
$document->cashier_recorded_at = now();
$this->assertEquals('recording', $document->getCurrentWorkflowStage());
// Stage 4: Reconciliation
$document->bank_reconciliation_id = 1;
$this->assertEquals('completed', $document->getCurrentWorkflowStage());
}
/** @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([
'cashier_recorded_at' => null,
]);
$this->assertFalse($document->isRecordingComplete());
$document->cashier_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 request_type_text_is_correct()
{
$doc1 = new FinanceDocument(['request_type' => 'expense_reimbursement']);
$this->assertEquals('費用報銷', $doc1->getRequestTypeText());
$doc2 = new FinanceDocument(['request_type' => 'advance_payment']);
$this->assertEquals('預支款項', $doc2->getRequestTypeText());
$doc3 = new FinanceDocument(['request_type' => 'purchase_request']);
$this->assertEquals('採購申請', $doc3->getRequestTypeText());
$doc4 = new FinanceDocument(['request_type' => 'petty_cash']);
$this->assertEquals('零用金', $doc4->getRequestTypeText());
}
/** @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());
}
}