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

300
tests/Unit/BudgetTest.php Normal file
View File

@@ -0,0 +1,300 @@
<?php
namespace Tests\Unit;
use App\Models\Budget;
use App\Models\BudgetItem;
use App\Models\ChartOfAccount;
use App\Models\Transaction;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class BudgetTest extends TestCase
{
use RefreshDatabase;
protected function setUp(): void
{
parent::setUp();
$this->artisan('db:seed', ['--class' => 'RoleSeeder']);
$this->artisan('db:seed', ['--class' => 'ChartOfAccountSeeder']);
}
public function test_budget_belongs_to_created_by_user(): void
{
$user = User::factory()->create();
$budget = Budget::factory()->create(['created_by_user_id' => $user->id]);
$this->assertInstanceOf(User::class, $budget->createdBy);
$this->assertEquals($user->id, $budget->createdBy->id);
}
public function test_budget_belongs_to_approved_by_user(): void
{
$user = User::factory()->create();
$budget = Budget::factory()->create(['approved_by_user_id' => $user->id]);
$this->assertInstanceOf(User::class, $budget->approvedBy);
$this->assertEquals($user->id, $budget->approvedBy->id);
}
public function test_budget_has_many_budget_items(): void
{
$budget = Budget::factory()->create();
$account1 = ChartOfAccount::first();
$account2 = ChartOfAccount::skip(1)->first();
$item1 = BudgetItem::factory()->create([
'budget_id' => $budget->id,
'chart_of_account_id' => $account1->id,
]);
$item2 = BudgetItem::factory()->create([
'budget_id' => $budget->id,
'chart_of_account_id' => $account2->id,
]);
$this->assertCount(2, $budget->budgetItems);
}
public function test_is_draft_returns_true_when_status_is_draft(): void
{
$budget = Budget::factory()->create(['status' => Budget::STATUS_DRAFT]);
$this->assertTrue($budget->isDraft());
}
public function test_is_approved_returns_true_when_status_is_approved(): void
{
$budget = Budget::factory()->create(['status' => Budget::STATUS_APPROVED]);
$this->assertTrue($budget->isApproved());
}
public function test_is_active_returns_true_when_status_is_active(): void
{
$budget = Budget::factory()->create(['status' => Budget::STATUS_ACTIVE]);
$this->assertTrue($budget->isActive());
}
public function test_is_closed_returns_true_when_status_is_closed(): void
{
$budget = Budget::factory()->create(['status' => Budget::STATUS_CLOSED]);
$this->assertTrue($budget->isClosed());
}
public function test_can_be_edited_validates_correctly(): void
{
$draftBudget = Budget::factory()->create(['status' => Budget::STATUS_DRAFT]);
$this->assertTrue($draftBudget->canBeEdited());
$submittedBudget = Budget::factory()->create(['status' => Budget::STATUS_SUBMITTED]);
$this->assertTrue($submittedBudget->canBeEdited());
$activeBudget = Budget::factory()->create(['status' => Budget::STATUS_ACTIVE]);
$this->assertFalse($activeBudget->canBeEdited());
$closedBudget = Budget::factory()->create(['status' => Budget::STATUS_CLOSED]);
$this->assertFalse($closedBudget->canBeEdited());
}
public function test_can_be_approved_validates_correctly(): void
{
$submittedBudget = Budget::factory()->create(['status' => Budget::STATUS_SUBMITTED]);
$this->assertTrue($submittedBudget->canBeApproved());
$draftBudget = Budget::factory()->create(['status' => Budget::STATUS_DRAFT]);
$this->assertFalse($draftBudget->canBeApproved());
$approvedBudget = Budget::factory()->create(['status' => Budget::STATUS_APPROVED]);
$this->assertFalse($approvedBudget->canBeApproved());
}
public function test_total_budgeted_income_calculation(): void
{
$budget = Budget::factory()->create();
$incomeAccount = ChartOfAccount::where('account_type', ChartOfAccount::TYPE_INCOME)->first();
if ($incomeAccount) {
BudgetItem::factory()->create([
'budget_id' => $budget->id,
'chart_of_account_id' => $incomeAccount->id,
'budgeted_amount' => 10000,
]);
BudgetItem::factory()->create([
'budget_id' => $budget->id,
'chart_of_account_id' => $incomeAccount->id,
'budgeted_amount' => 5000,
]);
$this->assertEquals(15000, $budget->total_budgeted_income);
} else {
$this->assertTrue(true, 'No income account available for test');
}
}
public function test_total_budgeted_expense_calculation(): void
{
$budget = Budget::factory()->create();
$expenseAccount = ChartOfAccount::where('account_type', ChartOfAccount::TYPE_EXPENSE)->first();
if ($expenseAccount) {
BudgetItem::factory()->create([
'budget_id' => $budget->id,
'chart_of_account_id' => $expenseAccount->id,
'budgeted_amount' => 8000,
]);
BudgetItem::factory()->create([
'budget_id' => $budget->id,
'chart_of_account_id' => $expenseAccount->id,
'budgeted_amount' => 3000,
]);
$this->assertEquals(11000, $budget->total_budgeted_expense);
} else {
$this->assertTrue(true, 'No expense account available for test');
}
}
public function test_total_actual_income_calculation(): void
{
$budget = Budget::factory()->create();
$incomeAccount = ChartOfAccount::where('account_type', ChartOfAccount::TYPE_INCOME)->first();
if ($incomeAccount) {
BudgetItem::factory()->create([
'budget_id' => $budget->id,
'chart_of_account_id' => $incomeAccount->id,
'budgeted_amount' => 10000,
'actual_amount' => 12000,
]);
BudgetItem::factory()->create([
'budget_id' => $budget->id,
'chart_of_account_id' => $incomeAccount->id,
'budgeted_amount' => 5000,
'actual_amount' => 4500,
]);
$this->assertEquals(16500, $budget->total_actual_income);
} else {
$this->assertTrue(true, 'No income account available for test');
}
}
public function test_total_actual_expense_calculation(): void
{
$budget = Budget::factory()->create();
$expenseAccount = ChartOfAccount::where('account_type', ChartOfAccount::TYPE_EXPENSE)->first();
if ($expenseAccount) {
BudgetItem::factory()->create([
'budget_id' => $budget->id,
'chart_of_account_id' => $expenseAccount->id,
'budgeted_amount' => 8000,
'actual_amount' => 7500,
]);
BudgetItem::factory()->create([
'budget_id' => $budget->id,
'chart_of_account_id' => $expenseAccount->id,
'budgeted_amount' => 3000,
'actual_amount' => 3200,
]);
$this->assertEquals(10700, $budget->total_actual_expense);
} else {
$this->assertTrue(true, 'No expense account available for test');
}
}
public function test_budget_item_variance_calculation(): void
{
$account = ChartOfAccount::first();
$budgetItem = BudgetItem::factory()->create([
'chart_of_account_id' => $account->id,
'budgeted_amount' => 10000,
'actual_amount' => 8500,
]);
$this->assertEquals(-1500, $budgetItem->variance);
}
public function test_budget_item_variance_percentage_calculation(): void
{
$account = ChartOfAccount::first();
$budgetItem = BudgetItem::factory()->create([
'chart_of_account_id' => $account->id,
'budgeted_amount' => 10000,
'actual_amount' => 8500,
]);
$this->assertEquals(-15.0, $budgetItem->variance_percentage);
}
public function test_budget_item_remaining_budget_calculation(): void
{
$account = ChartOfAccount::first();
$budgetItem = BudgetItem::factory()->create([
'chart_of_account_id' => $account->id,
'budgeted_amount' => 10000,
'actual_amount' => 6000,
]);
$this->assertEquals(4000, $budgetItem->remaining_budget);
}
public function test_budget_item_is_over_budget_detection(): void
{
$account = ChartOfAccount::first();
$overBudgetItem = BudgetItem::factory()->create([
'chart_of_account_id' => $account->id,
'budgeted_amount' => 10000,
'actual_amount' => 12000,
]);
$this->assertTrue($overBudgetItem->isOverBudget());
$underBudgetItem = BudgetItem::factory()->create([
'chart_of_account_id' => $account->id,
'budgeted_amount' => 10000,
'actual_amount' => 8000,
]);
$this->assertFalse($underBudgetItem->isOverBudget());
}
public function test_budget_item_utilization_percentage_calculation(): void
{
$account = ChartOfAccount::first();
$budgetItem = BudgetItem::factory()->create([
'chart_of_account_id' => $account->id,
'budgeted_amount' => 10000,
'actual_amount' => 7500,
]);
$this->assertEquals(75.0, $budgetItem->utilization_percentage);
}
public function test_budget_workflow_sequence(): void
{
$budget = Budget::factory()->create(['status' => Budget::STATUS_DRAFT]);
// Draft can be edited
$this->assertTrue($budget->canBeEdited());
$this->assertFalse($budget->canBeApproved());
// Submitted can be edited and approved
$budget->status = Budget::STATUS_SUBMITTED;
$this->assertTrue($budget->canBeEdited());
$this->assertTrue($budget->canBeApproved());
// Approved cannot be edited
$budget->status = Budget::STATUS_APPROVED;
$this->assertFalse($budget->canBeEdited());
$this->assertFalse($budget->canBeApproved());
// Active cannot be edited
$budget->status = Budget::STATUS_ACTIVE;
$this->assertFalse($budget->canBeEdited());
// Closed cannot be edited
$budget->status = Budget::STATUS_CLOSED;
$this->assertFalse($budget->canBeEdited());
}
}