Initial commit
This commit is contained in:
302
tests/Unit/BankReconciliationTest.php
Normal file
302
tests/Unit/BankReconciliationTest.php
Normal file
@@ -0,0 +1,302 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\BankReconciliation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* Bank Reconciliation Model Unit Tests
|
||||
*
|
||||
* Tests calculation and validation methods in BankReconciliation model
|
||||
*/
|
||||
class BankReconciliationTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_adjusted_balance_with_all_items()
|
||||
{
|
||||
$reconciliation = new BankReconciliation([
|
||||
'system_book_balance' => 100000,
|
||||
'outstanding_checks' => [
|
||||
['check_number' => 'CHK001', 'amount' => 3000],
|
||||
['check_number' => 'CHK002', 'amount' => 2000],
|
||||
],
|
||||
'deposits_in_transit' => [
|
||||
['date' => '2024-01-15', 'amount' => 5000],
|
||||
['date' => '2024-01-16', 'amount' => 3000],
|
||||
],
|
||||
'bank_charges' => [
|
||||
['amount' => 500],
|
||||
['amount' => 200],
|
||||
],
|
||||
]);
|
||||
|
||||
// Adjusted = 100000 + (5000 + 3000) - (3000 + 2000) - (500 + 200) = 102300
|
||||
$adjusted = $reconciliation->calculateAdjustedBalance();
|
||||
$this->assertEquals(102300, $adjusted);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_adjusted_balance_with_no_items()
|
||||
{
|
||||
$reconciliation = new BankReconciliation([
|
||||
'system_book_balance' => 50000,
|
||||
'outstanding_checks' => null,
|
||||
'deposits_in_transit' => null,
|
||||
'bank_charges' => null,
|
||||
]);
|
||||
|
||||
$adjusted = $reconciliation->calculateAdjustedBalance();
|
||||
$this->assertEquals(50000, $adjusted);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_adjusted_balance_with_empty_arrays()
|
||||
{
|
||||
$reconciliation = new BankReconciliation([
|
||||
'system_book_balance' => 50000,
|
||||
'outstanding_checks' => [],
|
||||
'deposits_in_transit' => [],
|
||||
'bank_charges' => [],
|
||||
]);
|
||||
|
||||
$adjusted = $reconciliation->calculateAdjustedBalance();
|
||||
$this->assertEquals(50000, $adjusted);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_calculates_discrepancy_correctly()
|
||||
{
|
||||
$reconciliation = new BankReconciliation([
|
||||
'bank_statement_balance' => 100000,
|
||||
'system_book_balance' => 95000,
|
||||
'outstanding_checks' => [
|
||||
['amount' => 3000],
|
||||
],
|
||||
'deposits_in_transit' => [
|
||||
['amount' => 5000],
|
||||
],
|
||||
'bank_charges' => [
|
||||
['amount' => 500],
|
||||
],
|
||||
]);
|
||||
|
||||
// Adjusted = 95000 + 5000 - 3000 - 500 = 96500
|
||||
// Discrepancy = |100000 - 96500| = 3500
|
||||
$discrepancy = $reconciliation->calculateDiscrepancy();
|
||||
$this->assertEquals(3500, $discrepancy);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_detects_discrepancy_above_tolerance()
|
||||
{
|
||||
$reconciliation = new BankReconciliation([
|
||||
'bank_statement_balance' => 100000,
|
||||
'system_book_balance' => 95000,
|
||||
'outstanding_checks' => [],
|
||||
'deposits_in_transit' => [],
|
||||
'bank_charges' => [],
|
||||
]);
|
||||
|
||||
// Discrepancy = 5000, which is > 0.01
|
||||
$this->assertTrue($reconciliation->hasDiscrepancy());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_allows_small_discrepancy_within_tolerance()
|
||||
{
|
||||
$reconciliation = new BankReconciliation([
|
||||
'bank_statement_balance' => 100000.00,
|
||||
'system_book_balance' => 100000.00,
|
||||
'outstanding_checks' => [],
|
||||
'deposits_in_transit' => [],
|
||||
'bank_charges' => [],
|
||||
]);
|
||||
|
||||
// Discrepancy = 0, which is <= 0.01
|
||||
$this->assertFalse($reconciliation->hasDiscrepancy());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_generates_outstanding_items_summary_correctly()
|
||||
{
|
||||
$reconciliation = new BankReconciliation([
|
||||
'outstanding_checks' => [
|
||||
['check_number' => 'CHK001', 'amount' => 3000],
|
||||
['check_number' => 'CHK002', 'amount' => 2000],
|
||||
['check_number' => 'CHK003', 'amount' => 1500],
|
||||
],
|
||||
'deposits_in_transit' => [
|
||||
['date' => '2024-01-15', 'amount' => 5000],
|
||||
['date' => '2024-01-16', 'amount' => 3000],
|
||||
],
|
||||
'bank_charges' => [
|
||||
['amount' => 500],
|
||||
['amount' => 200],
|
||||
['amount' => 100],
|
||||
],
|
||||
]);
|
||||
|
||||
$summary = $reconciliation->getOutstandingItemsSummary();
|
||||
|
||||
$this->assertEquals(6500, $summary['total_outstanding_checks']);
|
||||
$this->assertEquals(3, $summary['outstanding_checks_count']);
|
||||
$this->assertEquals(8000, $summary['total_deposits_in_transit']);
|
||||
$this->assertEquals(2, $summary['deposits_in_transit_count']);
|
||||
$this->assertEquals(800, $summary['total_bank_charges']);
|
||||
$this->assertEquals(3, $summary['bank_charges_count']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_null_outstanding_items_in_summary()
|
||||
{
|
||||
$reconciliation = new BankReconciliation([
|
||||
'outstanding_checks' => null,
|
||||
'deposits_in_transit' => null,
|
||||
'bank_charges' => null,
|
||||
]);
|
||||
|
||||
$summary = $reconciliation->getOutstandingItemsSummary();
|
||||
|
||||
$this->assertEquals(0, $summary['total_outstanding_checks']);
|
||||
$this->assertEquals(0, $summary['outstanding_checks_count']);
|
||||
$this->assertEquals(0, $summary['total_deposits_in_transit']);
|
||||
$this->assertEquals(0, $summary['deposits_in_transit_count']);
|
||||
$this->assertEquals(0, $summary['total_bank_charges']);
|
||||
$this->assertEquals(0, $summary['bank_charges_count']);
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_be_reviewed_when_pending()
|
||||
{
|
||||
$reconciliation = new BankReconciliation([
|
||||
'reconciliation_status' => 'pending',
|
||||
'reviewed_at' => null,
|
||||
]);
|
||||
|
||||
$this->assertTrue($reconciliation->canBeReviewed());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_cannot_be_reviewed_when_already_reviewed()
|
||||
{
|
||||
$reconciliation = new BankReconciliation([
|
||||
'reconciliation_status' => 'pending',
|
||||
'reviewed_at' => now(),
|
||||
]);
|
||||
|
||||
$this->assertFalse($reconciliation->canBeReviewed());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_can_be_approved_when_reviewed()
|
||||
{
|
||||
$reconciliation = new BankReconciliation([
|
||||
'reconciliation_status' => 'pending',
|
||||
'reviewed_at' => now(),
|
||||
'approved_at' => null,
|
||||
]);
|
||||
|
||||
$this->assertTrue($reconciliation->canBeApproved());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_cannot_be_approved_when_not_reviewed()
|
||||
{
|
||||
$reconciliation = new BankReconciliation([
|
||||
'reconciliation_status' => 'pending',
|
||||
'reviewed_at' => null,
|
||||
'approved_at' => null,
|
||||
]);
|
||||
|
||||
$this->assertFalse($reconciliation->canBeApproved());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_cannot_be_approved_when_already_approved()
|
||||
{
|
||||
$reconciliation = new BankReconciliation([
|
||||
'reconciliation_status' => 'completed',
|
||||
'reviewed_at' => now(),
|
||||
'approved_at' => now(),
|
||||
]);
|
||||
|
||||
$this->assertFalse($reconciliation->canBeApproved());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_detects_pending_status()
|
||||
{
|
||||
$pending = new BankReconciliation(['reconciliation_status' => 'pending']);
|
||||
$this->assertTrue($pending->isPending());
|
||||
|
||||
$completed = new BankReconciliation(['reconciliation_status' => 'completed']);
|
||||
$this->assertFalse($completed->isPending());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_detects_completed_status()
|
||||
{
|
||||
$completed = new BankReconciliation(['reconciliation_status' => 'completed']);
|
||||
$this->assertTrue($completed->isCompleted());
|
||||
|
||||
$pending = new BankReconciliation(['reconciliation_status' => 'pending']);
|
||||
$this->assertFalse($pending->isCompleted());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_detects_unresolved_discrepancy()
|
||||
{
|
||||
$withDiscrepancy = new BankReconciliation([
|
||||
'reconciliation_status' => 'discrepancy',
|
||||
]);
|
||||
$this->assertTrue($withDiscrepancy->hasUnresolvedDiscrepancy());
|
||||
|
||||
$completed = new BankReconciliation([
|
||||
'reconciliation_status' => 'completed',
|
||||
]);
|
||||
$this->assertFalse($completed->hasUnresolvedDiscrepancy());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function status_text_is_correct()
|
||||
{
|
||||
$pending = new BankReconciliation(['reconciliation_status' => 'pending']);
|
||||
$this->assertEquals('待覆核', $pending->getStatusText());
|
||||
|
||||
$completed = new BankReconciliation(['reconciliation_status' => 'completed']);
|
||||
$this->assertEquals('已完成', $completed->getStatusText());
|
||||
|
||||
$discrepancy = new BankReconciliation(['reconciliation_status' => 'discrepancy']);
|
||||
$this->assertEquals('有差異', $discrepancy->getStatusText());
|
||||
}
|
||||
|
||||
/** @test */
|
||||
public function it_handles_missing_amounts_in_outstanding_items()
|
||||
{
|
||||
$reconciliation = new BankReconciliation([
|
||||
'system_book_balance' => 100000,
|
||||
'outstanding_checks' => [
|
||||
['check_number' => 'CHK001'], // Missing amount
|
||||
['check_number' => 'CHK002', 'amount' => 2000],
|
||||
],
|
||||
'deposits_in_transit' => [
|
||||
['date' => '2024-01-15'], // Missing amount
|
||||
['date' => '2024-01-16', 'amount' => 3000],
|
||||
],
|
||||
'bank_charges' => [
|
||||
['description' => 'Fee'], // Missing amount
|
||||
['amount' => 200],
|
||||
],
|
||||
]);
|
||||
|
||||
// Should handle missing amounts gracefully (treat as 0)
|
||||
$adjusted = $reconciliation->calculateAdjustedBalance();
|
||||
// 100000 + 3000 - 2000 - 200 = 100800
|
||||
$this->assertEquals(100800, $adjusted);
|
||||
}
|
||||
}
|
||||
300
tests/Unit/BudgetTest.php
Normal file
300
tests/Unit/BudgetTest.php
Normal 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());
|
||||
}
|
||||
}
|
||||
16
tests/Unit/ExampleTest.php
Normal file
16
tests/Unit/ExampleTest.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ExampleTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* A basic test example.
|
||||
*/
|
||||
public function test_that_true_is_true(): void
|
||||
{
|
||||
$this->assertTrue(true);
|
||||
}
|
||||
}
|
||||
312
tests/Unit/FinanceDocumentTest.php
Normal file
312
tests/Unit/FinanceDocumentTest.php
Normal 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());
|
||||
}
|
||||
}
|
||||
328
tests/Unit/IssueTest.php
Normal file
328
tests/Unit/IssueTest.php
Normal file
@@ -0,0 +1,328 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Issue;
|
||||
use App\Models\IssueComment;
|
||||
use App\Models\IssueAttachment;
|
||||
use App\Models\IssueLabel;
|
||||
use App\Models\IssueTimeLog;
|
||||
use App\Models\User;
|
||||
use App\Models\Member;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class IssueTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->artisan('db:seed', ['--class' => 'RoleSeeder']);
|
||||
}
|
||||
|
||||
public function test_issue_number_auto_generation(): void
|
||||
{
|
||||
$issue1 = Issue::factory()->create();
|
||||
$issue2 = Issue::factory()->create();
|
||||
|
||||
$this->assertMatchesRegularExpression('/^ISS-\d{4}-\d{3}$/', $issue1->issue_number);
|
||||
$this->assertMatchesRegularExpression('/^ISS-\d{4}-\d{3}$/', $issue2->issue_number);
|
||||
$this->assertNotEquals($issue1->issue_number, $issue2->issue_number);
|
||||
}
|
||||
|
||||
public function test_issue_belongs_to_creator(): void
|
||||
{
|
||||
$creator = User::factory()->create();
|
||||
$issue = Issue::factory()->create(['created_by_user_id' => $creator->id]);
|
||||
|
||||
$this->assertInstanceOf(User::class, $issue->creator);
|
||||
$this->assertEquals($creator->id, $issue->creator->id);
|
||||
}
|
||||
|
||||
public function test_issue_belongs_to_assignee(): void
|
||||
{
|
||||
$assignee = User::factory()->create();
|
||||
$issue = Issue::factory()->create(['assigned_to_user_id' => $assignee->id]);
|
||||
|
||||
$this->assertInstanceOf(User::class, $issue->assignee);
|
||||
$this->assertEquals($assignee->id, $issue->assignee->id);
|
||||
}
|
||||
|
||||
public function test_issue_belongs_to_reviewer(): void
|
||||
{
|
||||
$reviewer = User::factory()->create();
|
||||
$issue = Issue::factory()->create(['reviewer_id' => $reviewer->id]);
|
||||
|
||||
$this->assertInstanceOf(User::class, $issue->reviewer);
|
||||
$this->assertEquals($reviewer->id, $issue->reviewer->id);
|
||||
}
|
||||
|
||||
public function test_issue_has_many_comments(): void
|
||||
{
|
||||
$issue = Issue::factory()->create();
|
||||
$comment1 = IssueComment::factory()->create(['issue_id' => $issue->id]);
|
||||
$comment2 = IssueComment::factory()->create(['issue_id' => $issue->id]);
|
||||
|
||||
$this->assertCount(2, $issue->comments);
|
||||
$this->assertTrue($issue->comments->contains($comment1));
|
||||
}
|
||||
|
||||
public function test_issue_has_many_attachments(): void
|
||||
{
|
||||
$issue = Issue::factory()->create();
|
||||
$attachment1 = IssueAttachment::factory()->create(['issue_id' => $issue->id]);
|
||||
$attachment2 = IssueAttachment::factory()->create(['issue_id' => $issue->id]);
|
||||
|
||||
$this->assertCount(2, $issue->attachments);
|
||||
}
|
||||
|
||||
public function test_issue_has_many_time_logs(): void
|
||||
{
|
||||
$issue = Issue::factory()->create();
|
||||
$log1 = IssueTimeLog::factory()->create(['issue_id' => $issue->id, 'hours' => 2.5]);
|
||||
$log2 = IssueTimeLog::factory()->create(['issue_id' => $issue->id, 'hours' => 3.5]);
|
||||
|
||||
$this->assertCount(2, $issue->timeLogs);
|
||||
}
|
||||
|
||||
public function test_issue_has_many_labels(): void
|
||||
{
|
||||
$issue = Issue::factory()->create();
|
||||
$label1 = IssueLabel::factory()->create();
|
||||
$label2 = IssueLabel::factory()->create();
|
||||
|
||||
$issue->labels()->attach([$label1->id, $label2->id]);
|
||||
|
||||
$this->assertCount(2, $issue->labels);
|
||||
$this->assertTrue($issue->labels->contains($label1));
|
||||
}
|
||||
|
||||
public function test_issue_has_many_watchers(): void
|
||||
{
|
||||
$issue = Issue::factory()->create();
|
||||
$watcher1 = User::factory()->create();
|
||||
$watcher2 = User::factory()->create();
|
||||
|
||||
$issue->watchers()->attach([$watcher1->id, $watcher2->id]);
|
||||
|
||||
$this->assertCount(2, $issue->watchers);
|
||||
}
|
||||
|
||||
public function test_status_check_methods_work(): void
|
||||
{
|
||||
$issue = Issue::factory()->create(['status' => Issue::STATUS_NEW]);
|
||||
$this->assertTrue($issue->isNew());
|
||||
$this->assertFalse($issue->isAssigned());
|
||||
|
||||
$issue->status = Issue::STATUS_ASSIGNED;
|
||||
$this->assertTrue($issue->isAssigned());
|
||||
$this->assertFalse($issue->isNew());
|
||||
|
||||
$issue->status = Issue::STATUS_IN_PROGRESS;
|
||||
$this->assertTrue($issue->isInProgress());
|
||||
|
||||
$issue->status = Issue::STATUS_REVIEW;
|
||||
$this->assertTrue($issue->inReview());
|
||||
|
||||
$issue->status = Issue::STATUS_CLOSED;
|
||||
$this->assertTrue($issue->isClosed());
|
||||
$this->assertFalse($issue->isOpen());
|
||||
}
|
||||
|
||||
public function test_can_be_assigned_validates_correctly(): void
|
||||
{
|
||||
$newIssue = Issue::factory()->create(['status' => Issue::STATUS_NEW]);
|
||||
$this->assertTrue($newIssue->canBeAssigned());
|
||||
|
||||
$closedIssue = Issue::factory()->create(['status' => Issue::STATUS_CLOSED]);
|
||||
$this->assertFalse($closedIssue->canBeAssigned());
|
||||
}
|
||||
|
||||
public function test_can_move_to_in_progress_validates_correctly(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$assignedIssue = Issue::factory()->create([
|
||||
'status' => Issue::STATUS_ASSIGNED,
|
||||
'assigned_to_user_id' => $user->id,
|
||||
]);
|
||||
$this->assertTrue($assignedIssue->canMoveToInProgress());
|
||||
|
||||
$newIssue = Issue::factory()->create(['status' => Issue::STATUS_NEW]);
|
||||
$this->assertFalse($newIssue->canMoveToInProgress());
|
||||
|
||||
$assignedWithoutUser = Issue::factory()->create([
|
||||
'status' => Issue::STATUS_ASSIGNED,
|
||||
'assigned_to_user_id' => null,
|
||||
]);
|
||||
$this->assertFalse($assignedWithoutUser->canMoveToInProgress());
|
||||
}
|
||||
|
||||
public function test_can_move_to_review_validates_correctly(): void
|
||||
{
|
||||
$inProgressIssue = Issue::factory()->create(['status' => Issue::STATUS_IN_PROGRESS]);
|
||||
$this->assertTrue($inProgressIssue->canMoveToReview());
|
||||
|
||||
$newIssue = Issue::factory()->create(['status' => Issue::STATUS_NEW]);
|
||||
$this->assertFalse($newIssue->canMoveToReview());
|
||||
}
|
||||
|
||||
public function test_can_be_closed_validates_correctly(): void
|
||||
{
|
||||
$reviewIssue = Issue::factory()->create(['status' => Issue::STATUS_REVIEW]);
|
||||
$this->assertTrue($reviewIssue->canBeClosed());
|
||||
|
||||
$inProgressIssue = Issue::factory()->create(['status' => Issue::STATUS_IN_PROGRESS]);
|
||||
$this->assertTrue($inProgressIssue->canBeClosed());
|
||||
|
||||
$newIssue = Issue::factory()->create(['status' => Issue::STATUS_NEW]);
|
||||
$this->assertFalse($newIssue->canBeClosed());
|
||||
}
|
||||
|
||||
public function test_can_be_reopened_validates_correctly(): void
|
||||
{
|
||||
$closedIssue = Issue::factory()->create(['status' => Issue::STATUS_CLOSED]);
|
||||
$this->assertTrue($closedIssue->canBeReopened());
|
||||
|
||||
$openIssue = Issue::factory()->create(['status' => Issue::STATUS_NEW]);
|
||||
$this->assertFalse($openIssue->canBeReopened());
|
||||
}
|
||||
|
||||
public function test_progress_percentage_calculation(): void
|
||||
{
|
||||
$newIssue = Issue::factory()->create(['status' => Issue::STATUS_NEW]);
|
||||
$this->assertEquals(0, $newIssue->progress_percentage);
|
||||
|
||||
$assignedIssue = Issue::factory()->create(['status' => Issue::STATUS_ASSIGNED]);
|
||||
$this->assertEquals(25, $assignedIssue->progress_percentage);
|
||||
|
||||
$inProgressIssue = Issue::factory()->create(['status' => Issue::STATUS_IN_PROGRESS]);
|
||||
$this->assertEquals(50, $inProgressIssue->progress_percentage);
|
||||
|
||||
$reviewIssue = Issue::factory()->create(['status' => Issue::STATUS_REVIEW]);
|
||||
$this->assertEquals(75, $reviewIssue->progress_percentage);
|
||||
|
||||
$closedIssue = Issue::factory()->create(['status' => Issue::STATUS_CLOSED]);
|
||||
$this->assertEquals(100, $closedIssue->progress_percentage);
|
||||
}
|
||||
|
||||
public function test_overdue_detection_works(): void
|
||||
{
|
||||
$overdueIssue = Issue::factory()->create([
|
||||
'due_date' => now()->subDays(5),
|
||||
'status' => Issue::STATUS_IN_PROGRESS,
|
||||
]);
|
||||
$this->assertTrue($overdueIssue->is_overdue);
|
||||
|
||||
$upcomingIssue = Issue::factory()->create([
|
||||
'due_date' => now()->addDays(5),
|
||||
'status' => Issue::STATUS_IN_PROGRESS,
|
||||
]);
|
||||
$this->assertFalse($upcomingIssue->is_overdue);
|
||||
|
||||
$closedOverdueIssue = Issue::factory()->create([
|
||||
'due_date' => now()->subDays(5),
|
||||
'status' => Issue::STATUS_CLOSED,
|
||||
]);
|
||||
$this->assertFalse($closedOverdueIssue->is_overdue);
|
||||
}
|
||||
|
||||
public function test_days_until_due_calculation(): void
|
||||
{
|
||||
$issue = Issue::factory()->create([
|
||||
'due_date' => now()->addDays(5),
|
||||
]);
|
||||
$this->assertEquals(5, $issue->days_until_due);
|
||||
|
||||
$overdueIssue = Issue::factory()->create([
|
||||
'due_date' => now()->subDays(3),
|
||||
]);
|
||||
$this->assertEquals(-3, $overdueIssue->days_until_due);
|
||||
}
|
||||
|
||||
public function test_total_time_logged_calculation(): void
|
||||
{
|
||||
$issue = Issue::factory()->create();
|
||||
IssueTimeLog::factory()->create(['issue_id' => $issue->id, 'hours' => 2.5]);
|
||||
IssueTimeLog::factory()->create(['issue_id' => $issue->id, 'hours' => 3.5]);
|
||||
IssueTimeLog::factory()->create(['issue_id' => $issue->id, 'hours' => 1.0]);
|
||||
|
||||
$this->assertEquals(7.0, $issue->total_time_logged);
|
||||
}
|
||||
|
||||
public function test_status_label_returns_correct_text(): void
|
||||
{
|
||||
$issue = Issue::factory()->create(['status' => Issue::STATUS_NEW]);
|
||||
$this->assertEquals('New', $issue->status_label);
|
||||
|
||||
$issue->status = Issue::STATUS_CLOSED;
|
||||
$this->assertEquals('Closed', $issue->status_label);
|
||||
}
|
||||
|
||||
public function test_priority_label_returns_correct_text(): void
|
||||
{
|
||||
$issue = Issue::factory()->create(['priority' => Issue::PRIORITY_LOW]);
|
||||
$this->assertEquals('Low', $issue->priority_label);
|
||||
|
||||
$issue->priority = Issue::PRIORITY_URGENT;
|
||||
$this->assertEquals('Urgent', $issue->priority_label);
|
||||
}
|
||||
|
||||
public function test_badge_color_methods_work(): void
|
||||
{
|
||||
$urgentIssue = Issue::factory()->create(['priority' => Issue::PRIORITY_URGENT]);
|
||||
$this->assertStringContainsString('red', $urgentIssue->priority_badge_color);
|
||||
|
||||
$closedIssue = Issue::factory()->create(['status' => Issue::STATUS_CLOSED]);
|
||||
$this->assertStringContainsString('gray', $closedIssue->status_badge_color);
|
||||
}
|
||||
|
||||
public function test_scopes_work(): void
|
||||
{
|
||||
Issue::factory()->create(['status' => Issue::STATUS_NEW]);
|
||||
Issue::factory()->create(['status' => Issue::STATUS_IN_PROGRESS]);
|
||||
Issue::factory()->create(['status' => Issue::STATUS_CLOSED]);
|
||||
|
||||
$openIssues = Issue::open()->get();
|
||||
$this->assertCount(2, $openIssues);
|
||||
|
||||
$closedIssues = Issue::closed()->get();
|
||||
$this->assertCount(1, $closedIssues);
|
||||
}
|
||||
|
||||
public function test_overdue_scope_works(): void
|
||||
{
|
||||
Issue::factory()->create([
|
||||
'due_date' => now()->subDays(5),
|
||||
'status' => Issue::STATUS_IN_PROGRESS,
|
||||
]);
|
||||
Issue::factory()->create([
|
||||
'due_date' => now()->addDays(5),
|
||||
'status' => Issue::STATUS_IN_PROGRESS,
|
||||
]);
|
||||
|
||||
$overdueIssues = Issue::overdue()->get();
|
||||
$this->assertCount(1, $overdueIssues);
|
||||
}
|
||||
|
||||
public function test_parent_child_relationships_work(): void
|
||||
{
|
||||
$parentIssue = Issue::factory()->create();
|
||||
$childIssue1 = Issue::factory()->create(['parent_issue_id' => $parentIssue->id]);
|
||||
$childIssue2 = Issue::factory()->create(['parent_issue_id' => $parentIssue->id]);
|
||||
|
||||
$this->assertCount(2, $parentIssue->subTasks);
|
||||
$this->assertInstanceOf(Issue::class, $childIssue1->parentIssue);
|
||||
$this->assertEquals($parentIssue->id, $childIssue1->parentIssue->id);
|
||||
}
|
||||
|
||||
public function test_issue_type_label_returns_correct_text(): void
|
||||
{
|
||||
$issue = Issue::factory()->create(['issue_type' => Issue::TYPE_WORK_ITEM]);
|
||||
$this->assertEquals('Work Item', $issue->issue_type_label);
|
||||
|
||||
$issue->issue_type = Issue::TYPE_MEMBER_REQUEST;
|
||||
$this->assertEquals('Member Request', $issue->issue_type_label);
|
||||
}
|
||||
}
|
||||
266
tests/Unit/MemberTest.php
Normal file
266
tests/Unit/MemberTest.php
Normal file
@@ -0,0 +1,266 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\User;
|
||||
use App\Models\MembershipPayment;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MemberTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->artisan('db:seed', ['--class' => 'RoleSeeder']);
|
||||
}
|
||||
|
||||
public function test_member_has_required_fillable_fields(): void
|
||||
{
|
||||
$member = Member::factory()->create([
|
||||
'full_name' => 'Test Member',
|
||||
'email' => 'test@example.com',
|
||||
'phone' => '0912345678',
|
||||
]);
|
||||
|
||||
$this->assertEquals('Test Member', $member->full_name);
|
||||
$this->assertEquals('test@example.com', $member->email);
|
||||
$this->assertEquals('0912345678', $member->phone);
|
||||
}
|
||||
|
||||
public function test_member_belongs_to_user(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$member = Member::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
$this->assertInstanceOf(User::class, $member->user);
|
||||
$this->assertEquals($user->id, $member->user->id);
|
||||
}
|
||||
|
||||
public function test_member_has_many_payments(): void
|
||||
{
|
||||
$member = Member::factory()->create();
|
||||
$payment1 = MembershipPayment::factory()->create(['member_id' => $member->id]);
|
||||
$payment2 = MembershipPayment::factory()->create(['member_id' => $member->id]);
|
||||
|
||||
$this->assertCount(2, $member->payments);
|
||||
$this->assertTrue($member->payments->contains($payment1));
|
||||
$this->assertTrue($member->payments->contains($payment2));
|
||||
}
|
||||
|
||||
public function test_has_paid_membership_returns_true_when_active_with_future_expiry(): void
|
||||
{
|
||||
$member = Member::factory()->create([
|
||||
'membership_status' => Member::STATUS_ACTIVE,
|
||||
'membership_started_at' => now()->subMonth(),
|
||||
'membership_expires_at' => now()->addYear(),
|
||||
]);
|
||||
|
||||
$this->assertTrue($member->hasPaidMembership());
|
||||
}
|
||||
|
||||
public function test_has_paid_membership_returns_false_when_pending(): void
|
||||
{
|
||||
$member = Member::factory()->create([
|
||||
'membership_status' => Member::STATUS_PENDING,
|
||||
'membership_started_at' => null,
|
||||
'membership_expires_at' => null,
|
||||
]);
|
||||
|
||||
$this->assertFalse($member->hasPaidMembership());
|
||||
}
|
||||
|
||||
public function test_has_paid_membership_returns_false_when_expired(): void
|
||||
{
|
||||
$member = Member::factory()->create([
|
||||
'membership_status' => Member::STATUS_ACTIVE,
|
||||
'membership_started_at' => now()->subYear()->subMonth(),
|
||||
'membership_expires_at' => now()->subMonth(),
|
||||
]);
|
||||
|
||||
$this->assertFalse($member->hasPaidMembership());
|
||||
}
|
||||
|
||||
public function test_has_paid_membership_returns_false_when_suspended(): void
|
||||
{
|
||||
$member = Member::factory()->create([
|
||||
'membership_status' => Member::STATUS_SUSPENDED,
|
||||
'membership_started_at' => now()->subMonth(),
|
||||
'membership_expires_at' => now()->addYear(),
|
||||
]);
|
||||
|
||||
$this->assertFalse($member->hasPaidMembership());
|
||||
}
|
||||
|
||||
public function test_can_submit_payment_returns_true_when_pending_and_no_pending_payment(): void
|
||||
{
|
||||
$member = Member::factory()->create([
|
||||
'membership_status' => Member::STATUS_PENDING,
|
||||
]);
|
||||
|
||||
$this->assertTrue($member->canSubmitPayment());
|
||||
}
|
||||
|
||||
public function test_can_submit_payment_returns_false_when_already_has_pending_payment(): void
|
||||
{
|
||||
$member = Member::factory()->create([
|
||||
'membership_status' => Member::STATUS_PENDING,
|
||||
]);
|
||||
|
||||
MembershipPayment::factory()->create([
|
||||
'member_id' => $member->id,
|
||||
'status' => MembershipPayment::STATUS_PENDING,
|
||||
]);
|
||||
|
||||
$this->assertFalse($member->canSubmitPayment());
|
||||
}
|
||||
|
||||
public function test_can_submit_payment_returns_false_when_active(): void
|
||||
{
|
||||
$member = Member::factory()->create([
|
||||
'membership_status' => Member::STATUS_ACTIVE,
|
||||
]);
|
||||
|
||||
$this->assertFalse($member->canSubmitPayment());
|
||||
}
|
||||
|
||||
public function test_get_pending_payment_returns_pending_payment(): void
|
||||
{
|
||||
$member = Member::factory()->create();
|
||||
|
||||
$pendingPayment = MembershipPayment::factory()->create([
|
||||
'member_id' => $member->id,
|
||||
'status' => MembershipPayment::STATUS_PENDING,
|
||||
]);
|
||||
|
||||
$approvedPayment = MembershipPayment::factory()->create([
|
||||
'member_id' => $member->id,
|
||||
'status' => MembershipPayment::STATUS_APPROVED_CHAIR,
|
||||
]);
|
||||
|
||||
$result = $member->getPendingPayment();
|
||||
|
||||
$this->assertNotNull($result);
|
||||
$this->assertEquals($pendingPayment->id, $result->id);
|
||||
}
|
||||
|
||||
public function test_get_pending_payment_returns_null_when_no_pending_payments(): void
|
||||
{
|
||||
$member = Member::factory()->create();
|
||||
|
||||
MembershipPayment::factory()->create([
|
||||
'member_id' => $member->id,
|
||||
'status' => MembershipPayment::STATUS_APPROVED_CHAIR,
|
||||
]);
|
||||
|
||||
$this->assertNull($member->getPendingPayment());
|
||||
}
|
||||
|
||||
public function test_national_id_encryption_and_decryption(): void
|
||||
{
|
||||
$member = Member::factory()->create([
|
||||
'full_name' => 'Test Member',
|
||||
]);
|
||||
|
||||
$nationalId = 'A123456789';
|
||||
$member->national_id = $nationalId;
|
||||
$member->save();
|
||||
|
||||
// Refresh from database
|
||||
$member->refresh();
|
||||
|
||||
// Check encrypted value is different from plain text
|
||||
$this->assertNotEquals($nationalId, $member->national_id_encrypted);
|
||||
|
||||
// Check decryption works
|
||||
$this->assertEquals($nationalId, $member->national_id);
|
||||
}
|
||||
|
||||
public function test_national_id_hash_created_for_search(): void
|
||||
{
|
||||
$member = Member::factory()->create();
|
||||
$nationalId = 'A123456789';
|
||||
|
||||
$member->national_id = $nationalId;
|
||||
$member->save();
|
||||
|
||||
$member->refresh();
|
||||
|
||||
// Check hash was created
|
||||
$this->assertNotNull($member->national_id_hash);
|
||||
|
||||
// Check hash matches SHA256
|
||||
$expectedHash = hash('sha256', $nationalId);
|
||||
$this->assertEquals($expectedHash, $member->national_id_hash);
|
||||
}
|
||||
|
||||
public function test_is_pending_returns_true_when_status_is_pending(): void
|
||||
{
|
||||
$member = Member::factory()->create(['membership_status' => Member::STATUS_PENDING]);
|
||||
$this->assertTrue($member->isPending());
|
||||
}
|
||||
|
||||
public function test_is_active_returns_true_when_status_is_active(): void
|
||||
{
|
||||
$member = Member::factory()->create(['membership_status' => Member::STATUS_ACTIVE]);
|
||||
$this->assertTrue($member->isActive());
|
||||
}
|
||||
|
||||
public function test_is_expired_returns_true_when_status_is_expired(): void
|
||||
{
|
||||
$member = Member::factory()->create(['membership_status' => Member::STATUS_EXPIRED]);
|
||||
$this->assertTrue($member->isExpired());
|
||||
}
|
||||
|
||||
public function test_is_suspended_returns_true_when_status_is_suspended(): void
|
||||
{
|
||||
$member = Member::factory()->create(['membership_status' => Member::STATUS_SUSPENDED]);
|
||||
$this->assertTrue($member->isSuspended());
|
||||
}
|
||||
|
||||
public function test_membership_status_label_returns_chinese_text(): void
|
||||
{
|
||||
$member = Member::factory()->create(['membership_status' => Member::STATUS_PENDING]);
|
||||
$this->assertEquals('待繳費', $member->membership_status_label);
|
||||
|
||||
$member->membership_status = Member::STATUS_ACTIVE;
|
||||
$this->assertEquals('已啟用', $member->membership_status_label);
|
||||
|
||||
$member->membership_status = Member::STATUS_EXPIRED;
|
||||
$this->assertEquals('已過期', $member->membership_status_label);
|
||||
|
||||
$member->membership_status = Member::STATUS_SUSPENDED;
|
||||
$this->assertEquals('已暫停', $member->membership_status_label);
|
||||
}
|
||||
|
||||
public function test_membership_type_label_returns_chinese_text(): void
|
||||
{
|
||||
$member = Member::factory()->create(['membership_type' => Member::TYPE_REGULAR]);
|
||||
$this->assertEquals('一般會員', $member->membership_type_label);
|
||||
|
||||
$member->membership_type = Member::TYPE_STUDENT;
|
||||
$this->assertEquals('學生會員', $member->membership_type_label);
|
||||
|
||||
$member->membership_type = Member::TYPE_HONORARY;
|
||||
$this->assertEquals('榮譽會員', $member->membership_type_label);
|
||||
|
||||
$member->membership_type = Member::TYPE_LIFETIME;
|
||||
$this->assertEquals('終身會員', $member->membership_type_label);
|
||||
}
|
||||
|
||||
public function test_membership_status_badge_returns_correct_css_class(): void
|
||||
{
|
||||
$member = Member::factory()->create(['membership_status' => Member::STATUS_PENDING]);
|
||||
$badge = $member->membership_status_badge;
|
||||
|
||||
$this->assertStringContainsString('待繳費', $badge);
|
||||
$this->assertStringContainsString('bg-yellow', $badge);
|
||||
|
||||
$member->membership_status = Member::STATUS_ACTIVE;
|
||||
$badge = $member->membership_status_badge;
|
||||
$this->assertStringContainsString('bg-green', $badge);
|
||||
}
|
||||
}
|
||||
230
tests/Unit/MembershipPaymentTest.php
Normal file
230
tests/Unit/MembershipPaymentTest.php
Normal file
@@ -0,0 +1,230 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Models\Member;
|
||||
use App\Models\MembershipPayment;
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MembershipPaymentTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
Storage::fake('private');
|
||||
$this->artisan('db:seed', ['--class' => 'RoleSeeder']);
|
||||
}
|
||||
|
||||
public function test_payment_belongs_to_member(): void
|
||||
{
|
||||
$member = Member::factory()->create();
|
||||
$payment = MembershipPayment::factory()->create(['member_id' => $member->id]);
|
||||
|
||||
$this->assertInstanceOf(Member::class, $payment->member);
|
||||
$this->assertEquals($member->id, $payment->member->id);
|
||||
}
|
||||
|
||||
public function test_payment_belongs_to_submitted_by_user(): void
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$payment = MembershipPayment::factory()->create(['submitted_by_user_id' => $user->id]);
|
||||
|
||||
$this->assertInstanceOf(User::class, $payment->submittedBy);
|
||||
$this->assertEquals($user->id, $payment->submittedBy->id);
|
||||
}
|
||||
|
||||
public function test_payment_has_verifier_relationships(): void
|
||||
{
|
||||
$cashier = User::factory()->create();
|
||||
$accountant = User::factory()->create();
|
||||
$chair = User::factory()->create();
|
||||
|
||||
$payment = MembershipPayment::factory()->create([
|
||||
'verified_by_cashier_id' => $cashier->id,
|
||||
'verified_by_accountant_id' => $accountant->id,
|
||||
'verified_by_chair_id' => $chair->id,
|
||||
]);
|
||||
|
||||
$this->assertInstanceOf(User::class, $payment->verifiedByCashier);
|
||||
$this->assertInstanceOf(User::class, $payment->verifiedByAccountant);
|
||||
$this->assertInstanceOf(User::class, $payment->verifiedByChair);
|
||||
|
||||
$this->assertEquals($cashier->id, $payment->verifiedByCashier->id);
|
||||
$this->assertEquals($accountant->id, $payment->verifiedByAccountant->id);
|
||||
$this->assertEquals($chair->id, $payment->verifiedByChair->id);
|
||||
}
|
||||
|
||||
public function test_is_pending_returns_true_when_status_is_pending(): void
|
||||
{
|
||||
$payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_PENDING]);
|
||||
$this->assertTrue($payment->isPending());
|
||||
}
|
||||
|
||||
public function test_is_approved_by_cashier_returns_true_when_status_is_approved_cashier(): void
|
||||
{
|
||||
$payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_CASHIER]);
|
||||
$this->assertTrue($payment->isApprovedByCashier());
|
||||
}
|
||||
|
||||
public function test_is_approved_by_accountant_returns_true_when_status_is_approved_accountant(): void
|
||||
{
|
||||
$payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_ACCOUNTANT]);
|
||||
$this->assertTrue($payment->isApprovedByAccountant());
|
||||
}
|
||||
|
||||
public function test_is_fully_approved_returns_true_when_status_is_approved_chair(): void
|
||||
{
|
||||
$payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_CHAIR]);
|
||||
$this->assertTrue($payment->isFullyApproved());
|
||||
}
|
||||
|
||||
public function test_is_rejected_returns_true_when_status_is_rejected(): void
|
||||
{
|
||||
$payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_REJECTED]);
|
||||
$this->assertTrue($payment->isRejected());
|
||||
}
|
||||
|
||||
public function test_can_be_approved_by_cashier_validates_correctly(): void
|
||||
{
|
||||
$pendingPayment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_PENDING]);
|
||||
$this->assertTrue($pendingPayment->canBeApprovedByCashier());
|
||||
|
||||
$approvedPayment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_CASHIER]);
|
||||
$this->assertFalse($approvedPayment->canBeApprovedByCashier());
|
||||
}
|
||||
|
||||
public function test_can_be_approved_by_accountant_validates_correctly(): void
|
||||
{
|
||||
$cashierApprovedPayment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_CASHIER]);
|
||||
$this->assertTrue($cashierApprovedPayment->canBeApprovedByAccountant());
|
||||
|
||||
$pendingPayment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_PENDING]);
|
||||
$this->assertFalse($pendingPayment->canBeApprovedByAccountant());
|
||||
}
|
||||
|
||||
public function test_can_be_approved_by_chair_validates_correctly(): void
|
||||
{
|
||||
$accountantApprovedPayment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_ACCOUNTANT]);
|
||||
$this->assertTrue($accountantApprovedPayment->canBeApprovedByChair());
|
||||
|
||||
$cashierApprovedPayment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_APPROVED_CASHIER]);
|
||||
$this->assertFalse($cashierApprovedPayment->canBeApprovedByChair());
|
||||
}
|
||||
|
||||
public function test_workflow_validation_prevents_skipping_tiers(): void
|
||||
{
|
||||
$payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_PENDING]);
|
||||
|
||||
// Cannot skip to accountant approval
|
||||
$this->assertFalse($payment->canBeApprovedByAccountant());
|
||||
|
||||
// Cannot skip to chair approval
|
||||
$this->assertFalse($payment->canBeApprovedByChair());
|
||||
|
||||
// Must go through cashier first
|
||||
$this->assertTrue($payment->canBeApprovedByCashier());
|
||||
}
|
||||
|
||||
public function test_status_label_returns_chinese_text(): void
|
||||
{
|
||||
$payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_PENDING]);
|
||||
$this->assertEquals('待審核', $payment->status_label);
|
||||
|
||||
$payment->status = MembershipPayment::STATUS_APPROVED_CASHIER;
|
||||
$this->assertEquals('出納已審', $payment->status_label);
|
||||
|
||||
$payment->status = MembershipPayment::STATUS_APPROVED_ACCOUNTANT;
|
||||
$this->assertEquals('會計已審', $payment->status_label);
|
||||
|
||||
$payment->status = MembershipPayment::STATUS_APPROVED_CHAIR;
|
||||
$this->assertEquals('主席已審', $payment->status_label);
|
||||
|
||||
$payment->status = MembershipPayment::STATUS_REJECTED;
|
||||
$this->assertEquals('已拒絕', $payment->status_label);
|
||||
}
|
||||
|
||||
public function test_payment_method_label_returns_chinese_text(): void
|
||||
{
|
||||
$payment = MembershipPayment::factory()->create(['payment_method' => MembershipPayment::METHOD_BANK_TRANSFER]);
|
||||
$this->assertEquals('銀行轉帳', $payment->payment_method_label);
|
||||
|
||||
$payment->payment_method = MembershipPayment::METHOD_CONVENIENCE_STORE;
|
||||
$this->assertEquals('超商繳費', $payment->payment_method_label);
|
||||
|
||||
$payment->payment_method = MembershipPayment::METHOD_CASH;
|
||||
$this->assertEquals('現金', $payment->payment_method_label);
|
||||
|
||||
$payment->payment_method = MembershipPayment::METHOD_CREDIT_CARD;
|
||||
$this->assertEquals('信用卡', $payment->payment_method_label);
|
||||
}
|
||||
|
||||
public function test_receipt_file_cleanup_on_deletion(): void
|
||||
{
|
||||
Storage::fake('private');
|
||||
|
||||
$payment = MembershipPayment::factory()->create([
|
||||
'receipt_path' => 'payment-receipts/test-receipt.pdf'
|
||||
]);
|
||||
|
||||
// Create fake file
|
||||
Storage::disk('private')->put('payment-receipts/test-receipt.pdf', 'test content');
|
||||
|
||||
$this->assertTrue(Storage::disk('private')->exists('payment-receipts/test-receipt.pdf'));
|
||||
|
||||
// Delete payment should delete file
|
||||
$payment->delete();
|
||||
|
||||
$this->assertFalse(Storage::disk('private')->exists('payment-receipts/test-receipt.pdf'));
|
||||
}
|
||||
|
||||
public function test_rejection_tracking_works(): void
|
||||
{
|
||||
$rejector = User::factory()->create();
|
||||
$payment = MembershipPayment::factory()->create([
|
||||
'status' => MembershipPayment::STATUS_REJECTED,
|
||||
'rejected_by_user_id' => $rejector->id,
|
||||
'rejected_at' => now(),
|
||||
'rejection_reason' => 'Invalid receipt',
|
||||
]);
|
||||
|
||||
$this->assertTrue($payment->isRejected());
|
||||
$this->assertInstanceOf(User::class, $payment->rejectedBy);
|
||||
$this->assertEquals($rejector->id, $payment->rejectedBy->id);
|
||||
$this->assertEquals('Invalid receipt', $payment->rejection_reason);
|
||||
$this->assertNotNull($payment->rejected_at);
|
||||
}
|
||||
|
||||
public function test_payment_workflow_complete_sequence(): void
|
||||
{
|
||||
$payment = MembershipPayment::factory()->create(['status' => MembershipPayment::STATUS_PENDING]);
|
||||
|
||||
// Step 1: Pending - can only be approved by cashier
|
||||
$this->assertTrue($payment->canBeApprovedByCashier());
|
||||
$this->assertFalse($payment->canBeApprovedByAccountant());
|
||||
$this->assertFalse($payment->canBeApprovedByChair());
|
||||
|
||||
// Step 2: Cashier approved - can only be approved by accountant
|
||||
$payment->status = MembershipPayment::STATUS_APPROVED_CASHIER;
|
||||
$this->assertFalse($payment->canBeApprovedByCashier());
|
||||
$this->assertTrue($payment->canBeApprovedByAccountant());
|
||||
$this->assertFalse($payment->canBeApprovedByChair());
|
||||
|
||||
// Step 3: Accountant approved - can only be approved by chair
|
||||
$payment->status = MembershipPayment::STATUS_APPROVED_ACCOUNTANT;
|
||||
$this->assertFalse($payment->canBeApprovedByCashier());
|
||||
$this->assertFalse($payment->canBeApprovedByAccountant());
|
||||
$this->assertTrue($payment->canBeApprovedByChair());
|
||||
|
||||
// Step 4: Chair approved - workflow complete
|
||||
$payment->status = MembershipPayment::STATUS_APPROVED_CHAIR;
|
||||
$this->assertFalse($payment->canBeApprovedByCashier());
|
||||
$this->assertFalse($payment->canBeApprovedByAccountant());
|
||||
$this->assertFalse($payment->canBeApprovedByChair());
|
||||
$this->assertTrue($payment->isFullyApproved());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user