Features: - Implement two fee types: entrance fee and annual fee (both NT$1,000) - Add 50% discount for disability certificate holders - Add disability certificate upload in member profile - Integrate disability verification into cashier approval workflow - Add membership fee settings in system admin Document permissions: - Fix hard-coded role logic in Document model - Use permission-based authorization instead of role checks Additional features: - Add announcements, general ledger, and trial balance modules - Add income management and accounting entries - Add comprehensive test suite with factories - Update UI translations to Traditional Chinese 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
260 lines
6.6 KiB
PHP
260 lines
6.6 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\CashierLedger;
|
|
|
|
use App\Models\CashierLedger;
|
|
use App\Models\PaymentOrder;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Tests\TestCase;
|
|
use Tests\Traits\CreatesFinanceData;
|
|
use Tests\Traits\SeedsRolesAndPermissions;
|
|
|
|
/**
|
|
* Cashier Ledger Tests
|
|
*
|
|
* Tests cashier ledger entries in the 4-stage finance workflow.
|
|
*/
|
|
class CashierLedgerTest extends TestCase
|
|
{
|
|
use RefreshDatabase, SeedsRolesAndPermissions, CreatesFinanceData;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Storage::fake('local');
|
|
$this->seedRolesAndPermissions();
|
|
}
|
|
|
|
/**
|
|
* Test can view cashier ledger
|
|
*/
|
|
public function test_can_view_cashier_ledger(): void
|
|
{
|
|
$cashier = $this->createCashier();
|
|
|
|
$response = $this->actingAs($cashier)->get(
|
|
route('admin.cashier-ledger.index')
|
|
);
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* Test ledger entry created from payment order
|
|
*/
|
|
public function test_ledger_entry_created_from_payment_order(): void
|
|
{
|
|
$cashier = $this->createCashier();
|
|
$order = $this->createPaymentOrder([
|
|
'status' => PaymentOrder::STATUS_COMPLETED,
|
|
]);
|
|
|
|
$response = $this->actingAs($cashier)->post(
|
|
route('admin.cashier-ledger.store'),
|
|
[
|
|
'payment_order_id' => $order->id,
|
|
'entry_type' => 'expense',
|
|
'entry_date' => now()->toDateString(),
|
|
]
|
|
);
|
|
|
|
$response->assertRedirect();
|
|
$this->assertDatabaseHas('cashier_ledgers', [
|
|
'payment_order_id' => $order->id,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Test ledger tracks income entries
|
|
*/
|
|
public function test_ledger_tracks_income_entries(): void
|
|
{
|
|
$cashier = $this->createCashier();
|
|
|
|
$response = $this->actingAs($cashier)->post(
|
|
route('admin.cashier-ledger.store'),
|
|
[
|
|
'entry_type' => 'income',
|
|
'amount' => 50000,
|
|
'description' => '會員繳費收入',
|
|
'entry_date' => now()->toDateString(),
|
|
]
|
|
);
|
|
|
|
$this->assertDatabaseHas('cashier_ledgers', [
|
|
'entry_type' => 'income',
|
|
'amount' => 50000,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Test ledger tracks expense entries
|
|
*/
|
|
public function test_ledger_tracks_expense_entries(): void
|
|
{
|
|
$cashier = $this->createCashier();
|
|
$order = $this->createPaymentOrder([
|
|
'status' => PaymentOrder::STATUS_COMPLETED,
|
|
]);
|
|
|
|
$entry = $this->createCashierLedgerEntry([
|
|
'payment_order_id' => $order->id,
|
|
'entry_type' => 'expense',
|
|
]);
|
|
|
|
$this->assertEquals('expense', $entry->entry_type);
|
|
}
|
|
|
|
/**
|
|
* Test ledger balance calculation
|
|
*/
|
|
public function test_ledger_balance_calculation(): void
|
|
{
|
|
$cashier = $this->createCashier();
|
|
|
|
// Create income
|
|
$this->createCashierLedgerEntry([
|
|
'entry_type' => 'income',
|
|
'amount' => 100000,
|
|
]);
|
|
|
|
// Create expense
|
|
$this->createCashierLedgerEntry([
|
|
'entry_type' => 'expense',
|
|
'amount' => 30000,
|
|
]);
|
|
|
|
$balance = CashierLedger::calculateBalance();
|
|
$this->assertEquals(70000, $balance);
|
|
}
|
|
|
|
/**
|
|
* Test ledger date range filter
|
|
*/
|
|
public function test_ledger_date_range_filter(): void
|
|
{
|
|
$cashier = $this->createCashier();
|
|
|
|
$this->createCashierLedgerEntry([
|
|
'entry_date' => now()->subMonth(),
|
|
]);
|
|
|
|
$this->createCashierLedgerEntry([
|
|
'entry_date' => now(),
|
|
]);
|
|
|
|
$response = $this->actingAs($cashier)->get(
|
|
route('admin.cashier-ledger.index', [
|
|
'start_date' => now()->startOfMonth()->toDateString(),
|
|
'end_date' => now()->endOfMonth()->toDateString(),
|
|
])
|
|
);
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* Test ledger entry validation
|
|
*/
|
|
public function test_ledger_entry_validation(): void
|
|
{
|
|
$cashier = $this->createCashier();
|
|
|
|
$response = $this->actingAs($cashier)->post(
|
|
route('admin.cashier-ledger.store'),
|
|
[
|
|
'entry_type' => 'income',
|
|
'amount' => -1000, // Invalid negative amount
|
|
'entry_date' => now()->toDateString(),
|
|
]
|
|
);
|
|
|
|
$response->assertSessionHasErrors('amount');
|
|
}
|
|
|
|
/**
|
|
* Test ledger entry requires date
|
|
*/
|
|
public function test_ledger_entry_requires_date(): void
|
|
{
|
|
$cashier = $this->createCashier();
|
|
|
|
$response = $this->actingAs($cashier)->post(
|
|
route('admin.cashier-ledger.store'),
|
|
[
|
|
'entry_type' => 'income',
|
|
'amount' => 5000,
|
|
// Missing entry_date
|
|
]
|
|
);
|
|
|
|
$response->assertSessionHasErrors('entry_date');
|
|
}
|
|
|
|
/**
|
|
* Test ledger monthly summary
|
|
*/
|
|
public function test_ledger_monthly_summary(): void
|
|
{
|
|
$cashier = $this->createCashier();
|
|
|
|
$this->createCashierLedgerEntry([
|
|
'entry_type' => 'income',
|
|
'amount' => 100000,
|
|
'entry_date' => now(),
|
|
]);
|
|
|
|
$this->createCashierLedgerEntry([
|
|
'entry_type' => 'expense',
|
|
'amount' => 50000,
|
|
'entry_date' => now(),
|
|
]);
|
|
|
|
$response = $this->actingAs($cashier)->get(
|
|
route('admin.cashier-ledger.summary', [
|
|
'year' => now()->year,
|
|
'month' => now()->month,
|
|
])
|
|
);
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* Test ledger export
|
|
*/
|
|
public function test_ledger_export(): void
|
|
{
|
|
$cashier = $this->createCashier();
|
|
|
|
$this->createCashierLedgerEntry();
|
|
$this->createCashierLedgerEntry();
|
|
|
|
$response = $this->actingAs($cashier)->get(
|
|
route('admin.cashier-ledger.export', ['format' => 'csv'])
|
|
);
|
|
|
|
$response->assertStatus(200);
|
|
}
|
|
|
|
/**
|
|
* Test ledger entry cannot be edited after reconciliation
|
|
*/
|
|
public function test_ledger_entry_cannot_be_edited_after_reconciliation(): void
|
|
{
|
|
$cashier = $this->createCashier();
|
|
$entry = $this->createCashierLedgerEntry([
|
|
'is_reconciled' => true,
|
|
]);
|
|
|
|
$response = $this->actingAs($cashier)->patch(
|
|
route('admin.cashier-ledger.update', $entry),
|
|
['amount' => 99999]
|
|
);
|
|
|
|
$response->assertSessionHasErrors();
|
|
}
|
|
}
|