1706 lines
53 KiB
PHP
1706 lines
53 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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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' => '',
|
|
]);
|
|
|
|
$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),
|
|
]);
|
|
|
|
$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),
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$this->post(route('admin.finance.cash-accounts.deposit'), [
|
|
'amount' => 200000,
|
|
'description' => 'Second deposit',
|
|
]);
|
|
|
|
$this->post(route('admin.finance.cash-accounts.deposit'), [
|
|
'amount' => 300000,
|
|
'description' => 'Third deposit',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
|
|
$response->assertRedirect();
|
|
});
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| 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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
// 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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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' => '',
|
|
]);
|
|
|
|
$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),
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
|
|
'amount' => 200000,
|
|
'description' => 'Second withdrawal',
|
|
]);
|
|
|
|
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
|
|
'amount' => 300000,
|
|
'description' => 'Third withdrawal',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
|
|
$this->assertDatabaseCount('cash_transactions', 0);
|
|
});
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| 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',
|
|
]);
|
|
expect(CashAccount::first()->balance)->toBe(500000);
|
|
|
|
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
|
|
'amount' => 200000,
|
|
'description' => 'First withdrawal',
|
|
]);
|
|
expect(CashAccount::first()->balance)->toBe(300000);
|
|
|
|
$this->post(route('admin.finance.cash-accounts.deposit'), [
|
|
'amount' => 100000,
|
|
'description' => 'Second deposit',
|
|
]);
|
|
expect(CashAccount::first()->balance)->toBe(400000);
|
|
|
|
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
|
|
'amount' => 400000,
|
|
'description' => 'Final withdrawal',
|
|
]);
|
|
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',
|
|
]);
|
|
|
|
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
|
|
'amount' => 80000,
|
|
'description' => 'Withdrawal',
|
|
]);
|
|
|
|
expect(CashAccount::first()->balance)->toBe(20000);
|
|
|
|
$response = $this->post(route('admin.finance.cash-accounts.withdrawal'), [
|
|
'amount' => 30000,
|
|
'description' => 'Should fail',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$response->assertSessionHasNoErrors();
|
|
expect(CashAccount::first()->balance)->toBe(125000);
|
|
});
|
|
|
|
/*
|
|
|--------------------------------------------------------------------------
|
|
| 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',
|
|
]);
|
|
$t1 = CashTransaction::latest()->first();
|
|
|
|
$this->post(route('admin.finance.cash-accounts.deposit'), [
|
|
'amount' => 50000,
|
|
'description' => 'Deposit 2',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
$t1 = CashTransaction::latest()->first();
|
|
|
|
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
|
|
'amount' => 30000,
|
|
'description' => 'Withdrawal 2',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
$deposit = CashTransaction::latest()->first();
|
|
|
|
// Withdraw 80,000 (balance becomes 20,000)
|
|
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
|
|
'amount' => 80000,
|
|
'description' => 'Withdrawal',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
|
|
'amount' => 100000,
|
|
'description' => 'Belanja inventory',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
|
|
$this->post(route('admin.finance.cash-accounts.deposit'), [
|
|
'amount' => 350000,
|
|
'description' => 'Penjualan siang',
|
|
]);
|
|
|
|
$this->post(route('admin.finance.cash-accounts.deposit'), [
|
|
'amount' => 150000,
|
|
'description' => 'Penjualan sore',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
|
|
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
|
|
'amount' => 30000,
|
|
'description' => 'Bayar air',
|
|
]);
|
|
|
|
$this->post(route('admin.finance.cash-accounts.withdrawal'), [
|
|
'amount' => 200000,
|
|
'description' => 'Gaji karyawan',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
$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',
|
|
]);
|
|
|
|
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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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}",
|
|
]);
|
|
}
|
|
|
|
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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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.',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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,
|
|
]);
|
|
|
|
$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,
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$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',
|
|
]);
|
|
|
|
$response->assertSessionHasNoErrors();
|
|
expect(CashAccount::first()->balance)->toBe(750000);
|
|
});
|