dstpabuaran.com/tests/Feature/Admin/Finance/CashAccountTest.php

1902 lines
61 KiB
PHP

<?php
use App\Enums\CashTransactionType;
use App\Models\CashAccount;
use App\Models\CashTransaction;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
uses(RefreshDatabase::class);
/*
|--------------------------------------------------------------------------
| AUTHENTICATION
|--------------------------------------------------------------------------
*/
test('guests are redirected to the login page', function () {
$response = $this->get(route('admin.finance.cash-accounts.index'));
$response->assertRedirect(route('login'));
});
test('authenticated users can visit the cash account index page', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->get(route('admin.finance.cash-accounts.index'));
$response->assertOk();
});
/*
|--------------------------------------------------------------------------
| INDEX PAGE
|--------------------------------------------------------------------------
*/
test('cash account index page displays cash account and transactions', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 500000]);
$response = $this->get(route('admin.finance.cash-accounts.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('admin/finance/cash-account/index')
->has('cashAccount')
->has('transactions')
);
});
test('index page works when no cash account exists', function () {
$user = User::factory()->create();
$this->actingAs($user);
$response = $this->get(route('admin.finance.cash-accounts.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('admin/finance/cash-account/index')
->has('transactions', 0)
);
});
test('index page displays correct balance', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 750000]);
$response = $this->get(route('admin.finance.cash-accounts.index'));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('admin/finance/cash-account/index')
->where('cashAccount.balance', 750000)
);
});
/*
|--------------------------------------------------------------------------
| DEPOSIT
|--------------------------------------------------------------------------
*/
test('deposit can be made', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => 'Deposit test',
'receipt_key' => 'cash-transaction/deposit-001.jpg',
]);
$response
->assertSessionHasNoErrors()
->assertRedirect(route('admin.finance.cash-accounts.index'));
expect(CashAccount::first()->balance)->toBe(150000);
});
test('deposit creates a transaction record', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => 'Deposit test',
'receipt_key' => 'cash-transaction/deposit-001.jpg',
]);
$this->assertDatabaseCount('cash_transactions', 1);
$this->assertDatabaseHas('cash_transactions', [
'type' => CashTransactionType::DEPOSIT->value,
'amount' => 50000,
'balance_after' => 150000,
'description' => 'Deposit test',
]);
});
test('deposit sets created_by_id to authenticated user', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => 'Deposit test',
'receipt_key' => 'cash-transaction/deposit-001.jpg',
]);
$this->assertDatabaseHas('cash_transactions', [
'created_by_id' => $user->id,
]);
});
test('deposit amount is required', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => '',
'description' => 'Test',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasErrors('amount');
});
test('deposit amount must be integer', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 'abc',
'description' => 'Test',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasErrors('amount');
});
test('deposit amount must be at least 1', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 0,
'description' => 'Test',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasErrors('amount');
});
test('deposit amount of exactly 1 is accepted', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 1,
'description' => 'Minimum deposit',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasNoErrors();
expect(CashAccount::first()->balance)->toBe(1);
});
test('deposit description is required', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => '',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasErrors('description');
});
test('deposit description must not exceed 100 characters', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => str_repeat('a', 101),
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasErrors('description');
});
test('deposit description exactly 100 characters is accepted', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => str_repeat('a', 100),
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasNoErrors();
});
test('deposit with formatted amount containing dots is accepted', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => '50.000',
'description' => 'Formatted deposit',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasNoErrors();
expect(CashAccount::first()->balance)->toBe(50000);
});
test('deposit with large amount is accepted', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 1000000000,
'description' => 'Large deposit',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasNoErrors();
expect(CashAccount::first()->balance)->toBe(1000000000);
});
test('deposit with negative amount is rejected', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => -50000,
'description' => 'Negative deposit',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasErrors('amount');
});
test('multiple deposits accumulate balance correctly', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 100000,
'description' => 'First deposit',
'receipt_key' => 'cash-transaction/receipt1.jpg',
]);
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 200000,
'description' => 'Second deposit',
'receipt_key' => 'cash-transaction/receipt2.jpg',
]);
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 300000,
'description' => 'Third deposit',
'receipt_key' => 'cash-transaction/receipt3.jpg',
]);
expect(CashAccount::first()->balance)->toBe(600000);
$this->assertDatabaseCount('cash_transactions', 3);
});
test('deposit with zero balance account works', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => 'First deposit',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
expect(CashAccount::first()->balance)->toBe(50000);
});
test('deposit flashes success toast', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => 'Deposit toast test',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertRedirect();
});
test('deposit receipt_key is required', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => 'Test',
]);
$response->assertSessionHasErrors('receipt_key');
});
test('deposit receipt_key max length is 500', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => 'Test',
'receipt_key' => str_repeat('a', 501),
]);
$response->assertSessionHasErrors('receipt_key');
});
test('deposit receipt_key exactly 500 characters is accepted', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => 'Test',
'receipt_key' => str_repeat('a', 500),
]);
$response->assertSessionHasNoErrors();
});
/*
|--------------------------------------------------------------------------
| WITHDRAWAL
|--------------------------------------------------------------------------
*/
test('withdrawal can be made', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 30000,
'description' => 'Withdrawal test',
'receipt_key' => 'cash-transaction/withdrawal-001.jpg',
]);
$response
->assertSessionHasNoErrors()
->assertRedirect(route('admin.finance.cash-accounts.index'));
expect(CashAccount::first()->balance)->toBe(70000);
});
test('withdrawal creates a transaction record', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 30000,
'description' => 'Withdrawal test',
'receipt_key' => 'cash-transaction/withdrawal-001.jpg',
]);
$this->assertDatabaseCount('cash_transactions', 1);
$this->assertDatabaseHas('cash_transactions', [
'type' => CashTransactionType::WITHDRAWAL->value,
'amount' => 30000,
'balance_after' => 70000,
'description' => 'Withdrawal test',
]);
});
test('withdrawal sets created_by_id to authenticated user', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 30000,
'description' => 'Withdrawal test',
'receipt_key' => 'cash-transaction/withdrawal-001.jpg',
]);
$this->assertDatabaseHas('cash_transactions', [
'created_by_id' => $user->id,
]);
});
test('withdrawal fails when balance is insufficient', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 50000]);
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 100000,
'description' => 'Withdrawal test',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
// Controller catches ValidationException and flashes toast error
$response->assertRedirect();
expect(CashAccount::first()->balance)->toBe(50000);
});
test('withdrawal of exact balance succeeds', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 50000]);
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 50000,
'description' => 'Withdrawal exact balance',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
$response->assertSessionHasNoErrors();
expect(CashAccount::first()->balance)->toBe(0);
});
test('withdrawal of entire balance leaves zero', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 100000,
'description' => 'Withdraw all',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
expect(CashAccount::first()->balance)->toBe(0);
});
test('withdrawal amount is required', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => '',
'description' => 'Test',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
$response->assertSessionHasErrors('amount');
});
test('withdrawal amount must be integer', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 'abc',
'description' => 'Test',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
$response->assertSessionHasErrors('amount');
});
test('withdrawal amount must be at least 1', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 0,
'description' => 'Test',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
$response->assertSessionHasErrors('amount');
});
test('withdrawal description is required', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 50000,
'description' => '',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
$response->assertSessionHasErrors('description');
});
test('withdrawal description must not exceed 100 characters', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 50000,
'description' => str_repeat('a', 101),
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
$response->assertSessionHasErrors('description');
});
test('withdrawal with formatted amount containing dots is accepted', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => '50.000',
'description' => 'Formatted withdrawal',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
$response->assertSessionHasNoErrors();
expect(CashAccount::first()->balance)->toBe(50000);
});
test('withdrawal with negative amount is rejected', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => -50000,
'description' => 'Negative withdrawal',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
$response->assertSessionHasErrors('amount');
});
test('multiple withdrawals decrease balance correctly', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 1000000]);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 100000,
'description' => 'First withdrawal',
'receipt_key' => 'cash-transaction/withdrawal1.jpg',
]);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 200000,
'description' => 'Second withdrawal',
'receipt_key' => 'cash-transaction/withdrawal2.jpg',
]);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 300000,
'description' => 'Third withdrawal',
'receipt_key' => 'cash-transaction/withdrawal3.jpg',
]);
expect(CashAccount::first()->balance)->toBe(400000);
$this->assertDatabaseCount('cash_transactions', 3);
});
test('withdrawal with insufficient balance does not create transaction', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 50000]);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 100000,
'description' => 'Should fail',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
$this->assertDatabaseCount('cash_transactions', 0);
});
test('withdrawal receipt_key is required', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 50000,
'description' => 'Test',
]);
$response->assertSessionHasErrors('receipt_key');
});
test('withdrawal receipt_key max length is 500', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 50000,
'description' => 'Test',
'receipt_key' => str_repeat('a', 501),
]);
$response->assertSessionHasErrors('receipt_key');
});
test('withdrawal receipt_key exactly 500 characters is accepted', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 50000,
'description' => 'Test',
'receipt_key' => str_repeat('a', 500),
]);
$response->assertSessionHasNoErrors();
});
/*
|--------------------------------------------------------------------------
| MIXED DEPOSIT AND WITHDRAWAL
|--------------------------------------------------------------------------
*/
test('deposit and withdrawal maintain correct balance sequence', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 500000,
'description' => 'Initial deposit',
'receipt_key' => 'cash-transaction/deposit1.jpg',
]);
expect(CashAccount::first()->balance)->toBe(500000);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 200000,
'description' => 'First withdrawal',
'receipt_key' => 'cash-transaction/withdrawal1.jpg',
]);
expect(CashAccount::first()->balance)->toBe(300000);
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 100000,
'description' => 'Second deposit',
'receipt_key' => 'cash-transaction/deposit2.jpg',
]);
expect(CashAccount::first()->balance)->toBe(400000);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 400000,
'description' => 'Final withdrawal',
'receipt_key' => 'cash-transaction/withdrawal2.jpg',
]);
expect(CashAccount::first()->balance)->toBe(0);
$this->assertDatabaseCount('cash_transactions', 4);
});
test('cannot withdraw more than available balance after mixed transactions', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 100000,
'description' => 'Deposit',
'receipt_key' => 'cash-transaction/deposit.jpg',
]);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 80000,
'description' => 'Withdrawal',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
expect(CashAccount::first()->balance)->toBe(20000);
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 30000,
'description' => 'Should fail',
'receipt_key' => 'cash-transaction/withdrawal2.jpg',
]);
$response->assertRedirect();
expect(CashAccount::first()->balance)->toBe(20000);
$this->assertDatabaseCount('cash_transactions', 2);
});
/*
|--------------------------------------------------------------------------
| UPDATE TRANSACTION
|--------------------------------------------------------------------------
*/
test('deposit transaction can be updated', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::DEPOSIT,
'amount' => 50000,
'balance_after' => 100000,
'description' => 'Original deposit',
]);
// Update deposit from 50,000 to 75,000
// difference = 75,000 - 50,000 = 25,000
// newBalance = 100,000 + 25,000 = 125,000
$response = $this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 75000,
'description' => 'Updated deposit',
'receipt_key' => 'cash-transaction/deposit-updated.jpg',
]);
$response
->assertSessionHasNoErrors()
->assertRedirect(route('admin.finance.cash-accounts.index'));
expect(CashAccount::first()->balance)->toBe(125000);
$transaction->refresh();
expect($transaction->amount)->toBe(75000);
expect($transaction->description)->toBe('Updated deposit');
});
test('withdrawal transaction can be updated', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::WITHDRAWAL,
'amount' => 30000,
'balance_after' => 100000,
'description' => 'Original withdrawal',
]);
// Update withdrawal from 30,000 to 20,000
// difference = 30,000 - 20,000 = 10,000 (positive, reducing withdrawal)
// newBalance = 100,000 + 10,000 = 110,000
$response = $this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 20000,
'description' => 'Updated withdrawal',
'receipt_key' => 'cash-transaction/withdrawal-updated.jpg',
]);
$response->assertSessionHasNoErrors();
expect(CashAccount::first()->balance)->toBe(110000);
$transaction->refresh();
expect($transaction->amount)->toBe(20000);
expect($transaction->description)->toBe('Updated withdrawal');
});
test('expense transaction cannot be updated', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::EXPENSE,
'amount' => 30000,
'balance_after' => 100000,
'description' => 'Expense',
]);
$response = $this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 20000,
'description' => 'Updated expense',
'receipt_key' => 'cash-transaction/expense-updated.jpg',
]);
$response->assertRedirect();
expect(CashAccount::first()->balance)->toBe(100000);
});
test('transfer transaction cannot be updated', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::TRANSFER,
'amount' => 30000,
'balance_after' => 100000,
'description' => 'Transfer',
]);
$response = $this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 20000,
'description' => 'Updated transfer',
'receipt_key' => 'cash-transaction/transfer-updated.jpg',
]);
$response->assertRedirect();
expect(CashAccount::first()->balance)->toBe(100000);
});
test('updating deposit to higher amount increases balance', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::DEPOSIT,
'amount' => 50000,
'balance_after' => 100000,
'description' => 'Original',
]);
// Update from 50,000 to 80,000
// difference = 80,000 - 50,000 = 30,000
// newBalance = 100,000 + 30,000 = 130,000
$this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 80000,
'description' => 'Increased',
'receipt_key' => 'cash-transaction/deposit-increased.jpg',
]);
expect(CashAccount::first()->balance)->toBe(130000);
});
test('updating deposit to lower amount decreases balance', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::DEPOSIT,
'amount' => 50000,
'balance_after' => 100000,
'description' => 'Original',
]);
// Update from 50,000 to 20,000
// difference = 20,000 - 50,000 = -30,000
// newBalance = 100,000 + (-30,000) = 70,000
$this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 20000,
'description' => 'Decreased',
'receipt_key' => 'cash-transaction/deposit-decreased.jpg',
]);
expect(CashAccount::first()->balance)->toBe(70000);
});
test('updating withdrawal to higher amount decreases balance further', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::WITHDRAWAL,
'amount' => 30000,
'balance_after' => 100000,
'description' => 'Original',
]);
// Update from 30,000 to 50,000
// difference = 30,000 - 50,000 = -20,000
// newBalance = 100,000 + (-20,000) = 80,000
$this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 50000,
'description' => 'Increased withdrawal',
'receipt_key' => 'cash-transaction/withdrawal-increased.jpg',
]);
expect(CashAccount::first()->balance)->toBe(80000);
});
test('updating withdrawal to lower amount increases balance', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::WITHDRAWAL,
'amount' => 30000,
'balance_after' => 100000,
'description' => 'Original',
]);
// Update from 30,000 to 10,000
// difference = 30,000 - 10,000 = 20,000
// newBalance = 100,000 + 20,000 = 120,000
$this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 10000,
'description' => 'Decreased withdrawal',
'receipt_key' => 'cash-transaction/withdrawal-decreased.jpg',
]);
expect(CashAccount::first()->balance)->toBe(120000);
});
test('updating deposit to zero amount is rejected', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::DEPOSIT,
'amount' => 50000,
'balance_after' => 100000,
'description' => 'Original',
]);
$response = $this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 0,
'description' => 'Zero amount',
'receipt_key' => 'cash-transaction/deposit.jpg',
]);
$response->assertSessionHasErrors('amount');
expect(CashAccount::first()->balance)->toBe(100000);
});
test('updating transaction with formatted amount is accepted', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::DEPOSIT,
'amount' => 50000,
'balance_after' => 100000,
'description' => 'Original',
]);
// Update from 50,000 to 75,000
// difference = 25,000
// newBalance = 125,000
$response = $this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => '75.000',
'description' => 'Formatted amount',
'receipt_key' => 'cash-transaction/deposit.jpg',
]);
$response->assertSessionHasNoErrors();
expect(CashAccount::first()->balance)->toBe(125000);
});
test('transaction update requires receipt_key', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::DEPOSIT,
'amount' => 50000,
'balance_after' => 100000,
'description' => 'Original',
]);
$response = $this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 75000,
'description' => 'Updated',
]);
$response->assertSessionHasErrors('receipt_key');
});
/*
|--------------------------------------------------------------------------
| DELETE TRANSACTION
|--------------------------------------------------------------------------
*/
test('deposit transaction can be deleted', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::DEPOSIT,
'amount' => 50000,
'balance_after' => 100000,
'description' => 'To be deleted',
]);
// Delete deposit: newBalance = 100,000 - 50,000 = 50,000
$response = $this->delete(route('admin.finance.cash-accounts.transactions.destroy', $transaction));
$response
->assertSessionHasNoErrors()
->assertRedirect(route('admin.finance.cash-accounts.index'));
expect(CashAccount::first()->balance)->toBe(50000);
$this->assertSoftDeleted('cash_transactions', ['id' => $transaction->id]);
});
test('withdrawal transaction can be deleted', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::WITHDRAWAL,
'amount' => 30000,
'balance_after' => 100000,
'description' => 'To be deleted',
]);
// Delete withdrawal: newBalance = 100,000 + 30,000 = 130,000
$response = $this->delete(route('admin.finance.cash-accounts.transactions.destroy', $transaction));
$response->assertSessionHasNoErrors();
expect(CashAccount::first()->balance)->toBe(130000);
$this->assertSoftDeleted('cash_transactions', ['id' => $transaction->id]);
});
test('expense transaction cannot be deleted', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::EXPENSE,
'amount' => 30000,
'balance_after' => 100000,
'description' => 'Expense',
]);
$response = $this->delete(route('admin.finance.cash-accounts.transactions.destroy', $transaction));
$response->assertRedirect();
expect(CashAccount::first()->balance)->toBe(100000);
});
test('transfer transaction cannot be deleted', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::TRANSFER,
'amount' => 30000,
'balance_after' => 100000,
'description' => 'Transfer',
]);
$response = $this->delete(route('admin.finance.cash-accounts.transactions.destroy', $transaction));
$response->assertRedirect();
expect(CashAccount::first()->balance)->toBe(100000);
});
test('deleting deposit reduces balance correctly', function () {
$user = User::factory()->create();
$this->actingAs($user);
// Start with 0 balance, make two deposits via API to get correct balance
$cashAccount = CashAccount::factory()->create(['balance' => 0]);
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 100000,
'description' => 'Deposit 1',
'receipt_key' => 'cash-transaction/deposit1.jpg',
]);
$t1 = CashTransaction::latest()->first();
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => 'Deposit 2',
'receipt_key' => 'cash-transaction/deposit2.jpg',
]);
expect(CashAccount::first()->balance)->toBe(150000);
// Delete first deposit: newBalance = 150,000 - 100,000 = 50,000
$this->delete(route('admin.finance.cash-accounts.transactions.destroy', $t1));
expect(CashAccount::first()->balance)->toBe(50000);
});
test('deleting withdrawal increases balance correctly', function () {
$user = User::factory()->create();
$this->actingAs($user);
// Start with 200,000, make two withdrawals via API
$cashAccount = CashAccount::factory()->create(['balance' => 200000]);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 50000,
'description' => 'Withdrawal 1',
'receipt_key' => 'cash-transaction/withdrawal1.jpg',
]);
$t1 = CashTransaction::latest()->first();
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 30000,
'description' => 'Withdrawal 2',
'receipt_key' => 'cash-transaction/withdrawal2.jpg',
]);
expect(CashAccount::first()->balance)->toBe(120000);
// Delete first withdrawal: newBalance = 120,000 + 50,000 = 170,000
$this->delete(route('admin.finance.cash-accounts.transactions.destroy', $t1));
expect(CashAccount::first()->balance)->toBe(170000);
});
test('deleting deposit that would cause negative balance is rejected', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 50000]);
// Deposit 50,000 via API (balance becomes 100,000)
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => 'Deposit',
'receipt_key' => 'cash-transaction/deposit.jpg',
]);
$deposit = CashTransaction::latest()->first();
// Withdraw 80,000 (balance becomes 20,000)
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 80000,
'description' => 'Withdrawal',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
expect(CashAccount::first()->balance)->toBe(20000);
// Try to delete the deposit: newBalance = 20,000 - 50,000 = -30,000 (rejected)
$response = $this->delete(route('admin.finance.cash-accounts.transactions.destroy', $deposit));
$response->assertRedirect();
expect(CashAccount::first()->balance)->toBe(20000);
});
test('multiple transactions can be deleted sequentially', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 0]);
// Create transactions directly with known types
$t1 = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::DEPOSIT,
'amount' => 100000,
'balance_after' => 100000,
'description' => 'First deposit',
]);
$t2 = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::DEPOSIT,
'amount' => 50000,
'balance_after' => 150000,
'description' => 'Second deposit',
]);
$t3 = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::WITHDRAWAL,
'amount' => 30000,
'balance_after' => 120000,
'description' => 'Withdrawal',
]);
// Manually set balance to 120,000 (simulating what would happen)
$cashAccount->update(['balance' => 120000]);
expect(CashAccount::first()->balance)->toBe(120000);
// Delete withdrawal first: 120,000 + 30,000 = 150,000
$this->delete(route('admin.finance.cash-accounts.transactions.destroy', $t3));
expect(CashAccount::first()->balance)->toBe(150000);
// Delete second deposit: 150,000 - 50,000 = 100,000
$this->delete(route('admin.finance.cash-accounts.transactions.destroy', $t2));
expect(CashAccount::first()->balance)->toBe(100000);
// Delete first deposit: 100,000 - 100,000 = 0
$this->delete(route('admin.finance.cash-accounts.transactions.destroy', $t1));
expect(CashAccount::first()->balance)->toBe(0);
});
/*
|--------------------------------------------------------------------------
| AUTHORIZATION
|--------------------------------------------------------------------------
*/
test('guest cannot perform deposit', function () {
CashAccount::factory()->create(['balance' => 0]);
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => 'Unauthorized',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertRedirect(route('login'));
expect(CashAccount::first()->balance)->toBe(0);
});
test('guest cannot perform withdrawal', function () {
CashAccount::factory()->create(['balance' => 100000]);
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 50000,
'description' => 'Unauthorized',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertRedirect(route('login'));
expect(CashAccount::first()->balance)->toBe(100000);
});
test('guest cannot update transaction', function () {
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'type' => CashTransactionType::DEPOSIT,
'amount' => 50000,
]);
$response = $this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 99999,
'description' => 'Unauthorized',
]);
$response->assertRedirect(route('login'));
});
test('guest cannot delete transaction', function () {
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'type' => CashTransactionType::DEPOSIT,
'amount' => 50000,
]);
$response = $this->delete(route('admin.finance.cash-accounts.transactions.destroy', $transaction));
$response->assertRedirect(route('login'));
expect(CashAccount::first()->balance)->toBe(100000);
});
/*
|--------------------------------------------------------------------------
| DATA INTEGRITY
|--------------------------------------------------------------------------
*/
test('cash account has correct timestamps', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create();
expect($cashAccount->created_at)->not->toBeNull();
expect($cashAccount->updated_at)->not->toBeNull();
});
test('cash transaction has correct timestamps', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create();
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
]);
expect($transaction->created_at)->not->toBeNull();
expect($transaction->updated_at)->not->toBeNull();
});
test('cash account balance is cast to integer', function () {
$cashAccount = CashAccount::factory()->create(['balance' => 500000]);
expect($cashAccount->balance)->toBeInt();
});
test('cash transaction amount is cast to integer', function () {
$cashAccount = CashAccount::factory()->create();
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'amount' => 50000,
]);
expect($transaction->amount)->toBeInt();
});
test('cash transaction balance_after is cast to integer', function () {
$cashAccount = CashAccount::factory()->create();
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'balance_after' => 100000,
]);
expect($transaction->balance_after)->toBeInt();
});
test('cash transaction type is cast to enum', function () {
$cashAccount = CashAccount::factory()->create();
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'type' => CashTransactionType::DEPOSIT,
]);
expect($transaction->type)->toBeInstanceOf(CashTransactionType::class);
});
test('cash account balance defaults to zero', function () {
$cashAccount = CashAccount::factory()->create(['balance' => 0]);
expect($cashAccount->balance)->toBe(0);
});
test('cash transaction has correct type values', function () {
expect(CashTransactionType::DEPOSIT->value)->toBe('deposit');
expect(CashTransactionType::WITHDRAWAL->value)->toBe('withdrawal');
expect(CashTransactionType::EXPENSE->value)->toBe('expense');
expect(CashTransactionType::TRANSFER->value)->toBe('transfer');
});
/*
|--------------------------------------------------------------------------
| SOFT DELETE
|--------------------------------------------------------------------------
*/
test('cash transaction is soft deleted', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::DEPOSIT,
'amount' => 50000,
]);
$this->delete(route('admin.finance.cash-accounts.transactions.destroy', $transaction));
$this->assertSoftDeleted('cash_transactions', ['id' => $transaction->id]);
});
test('soft deleted transaction still exists in database', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::DEPOSIT,
'amount' => 50000,
]);
$this->delete(route('admin.finance.cash-accounts.transactions.destroy', $transaction));
$this->assertDatabaseHas('cash_transactions', ['id' => $transaction->id]);
$this->assertSoftDeleted('cash_transactions', ['id' => $transaction->id]);
});
/*
|--------------------------------------------------------------------------
| REALISTIC USER SCENARIOS
|--------------------------------------------------------------------------
*/
test('user makes deposit then withdrawal in sequence', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 500000,
'description' => 'Modal usaha',
'receipt_key' => 'cash-transaction/deposit.jpg',
]);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 100000,
'description' => 'Belanja inventory',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
expect(CashAccount::first()->balance)->toBe(400000);
$this->assertDatabaseCount('cash_transactions', 2);
});
test('user makes multiple deposits throughout the day', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 200000,
'description' => 'Penjualan pagi',
'receipt_key' => 'cash-transaction/deposit1.jpg',
]);
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 350000,
'description' => 'Penjualan siang',
'receipt_key' => 'cash-transaction/deposit2.jpg',
]);
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 150000,
'description' => 'Penjualan sore',
'receipt_key' => 'cash-transaction/deposit3.jpg',
]);
expect(CashAccount::first()->balance)->toBe(700000);
$this->assertDatabaseCount('cash_transactions', 3);
});
test('user makes withdrawal for expenses', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 1000000]);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 50000,
'description' => 'Bayar listrik',
'receipt_key' => 'cash-transaction/withdrawal1.jpg',
]);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 30000,
'description' => 'Bayar air',
'receipt_key' => 'cash-transaction/withdrawal2.jpg',
]);
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 200000,
'description' => 'Gaji karyawan',
'receipt_key' => 'cash-transaction/withdrawal3.jpg',
]);
expect(CashAccount::first()->balance)->toBe(720000);
});
test('user edits deposit description', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::DEPOSIT,
'amount' => 100000,
'balance_after' => 100000,
'description' => 'Penjualan',
]);
$response = $this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 100000,
'description' => 'Penjualan produk A',
'receipt_key' => 'cash-transaction/deposit.jpg',
]);
$response->assertSessionHasNoErrors();
$transaction->refresh();
expect($transaction->description)->toBe('Penjualan produk A');
expect($transaction->amount)->toBe(100000);
expect(CashAccount::first()->balance)->toBe(100000);
});
test('user deletes wrong deposit and re-enters correct amount', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 0]);
// Wrong deposit via API
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 100000,
'description' => 'Wrong amount',
'receipt_key' => 'cash-transaction/deposit.jpg',
]);
$wrongDeposit = CashTransaction::where('cash_account_id', $cashAccount->id)->first();
expect(CashAccount::first()->balance)->toBe(100000);
// Delete wrong deposit: 100,000 - 100,000 = 0
$this->delete(route('admin.finance.cash-accounts.transactions.destroy', $wrongDeposit));
expect(CashAccount::first()->balance)->toBe(0);
// Re-enter correct deposit
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 150000,
'description' => 'Correct amount',
'receipt_key' => 'cash-transaction/deposit-correct.jpg',
]);
expect(CashAccount::first()->balance)->toBe(150000);
// Old transaction is soft deleted, new one created
$this->assertDatabaseCount('cash_transactions', 2);
});
test('user attempts withdrawal with exact balance', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 100000,
'description' => 'Withdraw all',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
$response->assertSessionHasNoErrors();
expect(CashAccount::first()->balance)->toBe(0);
});
test('user attempts withdrawal with 1 more than balance', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 100001,
'description' => 'Over balance',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
$response->assertRedirect();
expect(CashAccount::first()->balance)->toBe(100000);
});
test('user rapidly makes multiple deposits', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
for ($i = 0; $i < 5; $i++) {
$this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 100000,
'description' => "Deposit {$i}",
'receipt_key' => "cash-transaction/deposit{$i}.jpg",
]);
}
expect(CashAccount::first()->balance)->toBe(500000);
$this->assertDatabaseCount('cash_transactions', 5);
});
test('user edits withdrawal amount to reduce it', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::WITHDRAWAL,
'amount' => 50000,
'balance_after' => 100000,
'description' => 'Original',
]);
// Reduce withdrawal from 50,000 to 20,000
// difference = 50,000 - 20,000 = 30,000
// newBalance = 100,000 + 30,000 = 130,000
$response = $this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 20000,
'description' => 'Reduced',
'receipt_key' => 'cash-transaction/withdrawal-reduced.jpg',
]);
$response->assertSessionHasNoErrors();
expect(CashAccount::first()->balance)->toBe(130000);
});
test('user edits withdrawal amount to increase it', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::WITHDRAWAL,
'amount' => 20000,
'balance_after' => 100000,
'description' => 'Original',
]);
// Increase withdrawal from 20,000 to 60,000
// difference = 20,000 - 60,000 = -40,000
// newBalance = 100,000 + (-40,000) = 60,000
$response = $this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 60000,
'description' => 'Increased',
'receipt_key' => 'cash-transaction/withdrawal-increased.jpg',
]);
$response->assertSessionHasNoErrors();
expect(CashAccount::first()->balance)->toBe(60000);
});
test('user cannot edit withdrawal to exceed remaining balance', function () {
$user = User::factory()->create();
$this->actingAs($user);
$cashAccount = CashAccount::factory()->create(['balance' => 100000]);
$transaction = CashTransaction::factory()->create([
'cash_account_id' => $cashAccount->id,
'created_by_id' => $user->id,
'type' => CashTransactionType::WITHDRAWAL,
'amount' => 30000,
'balance_after' => 100000,
'description' => 'Original',
]);
expect(CashAccount::first()->balance)->toBe(100000);
// Increase withdrawal from 30,000 to 200,000
// difference = 30,000 - 200,000 = -170,000
// newBalance = 100,000 + (-170,000) = -70,000 (insufficient)
$response = $this->put(route('admin.finance.cash-accounts.transactions.update', $transaction), [
'amount' => 200000,
'description' => 'Way too much',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
$response->assertRedirect();
expect(CashAccount::first()->balance)->toBe(100000);
});
test('deposit with description containing special characters', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'description' => 'Deposit dari PT. Maju Jaya & Co.',
'receipt_key' => 'cash-transaction/deposit.jpg',
]);
$response->assertSessionHasNoErrors();
$this->assertDatabaseHas('cash_transactions', [
'description' => 'Deposit dari PT. Maju Jaya & Co.',
]);
});
test('withdrawal with description containing unicode characters', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 100000]);
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 50000,
'description' => 'Pembayaran gaji karyawan',
'receipt_key' => 'cash-transaction/withdrawal.jpg',
]);
$response->assertSessionHasNoErrors();
$this->assertDatabaseHas('cash_transactions', [
'description' => 'Pembayaran gaji karyawan',
]);
});
test('deposit without description fails validation', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => 50000,
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasErrors('description');
});
test('withdrawal without description fails validation', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => 50000,
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasErrors('description');
});
test('deposit with empty string amount fails validation', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => '',
'description' => 'Test',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasErrors('amount');
});
test('withdrawal with empty string amount fails validation', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create();
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => '',
'description' => 'Test',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasErrors('amount');
});
test('deposit with float amount is parsed correctly', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 0]);
$response = $this->post(route('admin.finance.cash-accounts.deposit'), [
'amount' => '100.500',
'description' => 'Float amount',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasNoErrors();
expect(CashAccount::first()->balance)->toBe(100500);
});
test('withdrawal with float amount is parsed correctly', function () {
$user = User::factory()->create();
$this->actingAs($user);
CashAccount::factory()->create(['balance' => 1000000]);
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
'amount' => '250.000',
'description' => 'Float amount',
'receipt_key' => 'cash-transaction/receipt.jpg',
]);
$response->assertSessionHasNoErrors();
expect(CashAccount::first()->balance)->toBe(750000);
});