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()); } }