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.data') ->where('transactions.current_page', 1) ->where('transactions.per_page', 25) ); }); 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.data', 0) ->where('transactions.total', 0) ->where('transactions.current_page', 1) ->where('transactions.per_page', 25) ); }); 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) ); }); /* |-------------------------------------------------------------------------- | FILTER - TRANSACTION TYPE |-------------------------------------------------------------------------- */ test('index can filter by type deposit', function () { $user = User::factory()->create(); $this->actingAs($user); $cashAccount = CashAccount::factory()->create(['balance' => 200000]); CashTransaction::factory()->create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => $user->id, 'type' => CashTransactionType::DEPOSIT, 'amount' => 100000, 'balance_after' => 100000, 'description' => 'Deposit pertama', ]); CashTransaction::factory()->create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => $user->id, 'type' => CashTransactionType::WITHDRAWAL, 'amount' => 50000, 'balance_after' => 200000, 'description' => 'Withdrawal pertama', ]); CashTransaction::factory()->create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => $user->id, 'type' => CashTransactionType::DEPOSIT, 'amount' => 100000, 'balance_after' => 200000, 'description' => 'Deposit kedua', ]); $response = $this->get(route('admin.finance.cash-accounts.index', ['type' => 'deposit'])); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/finance/cash-account/index') ->has('transactions.data', 2) ->where('transactions.total', 2) ->where('transactions.current_page', 1) ->where('transactions.per_page', 25) ); }); test('index can filter by type withdrawal', function () { $user = User::factory()->create(); $this->actingAs($user); $cashAccount = CashAccount::factory()->create(['balance' => 100000]); CashTransaction::factory()->create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => $user->id, 'type' => CashTransactionType::DEPOSIT, 'amount' => 100000, 'balance_after' => 100000, 'description' => 'Deposit', ]); CashTransaction::factory()->create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => $user->id, 'type' => CashTransactionType::WITHDRAWAL, 'amount' => 30000, 'balance_after' => 100000, 'description' => 'Withdrawal pertama', ]); CashTransaction::factory()->create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => $user->id, 'type' => CashTransactionType::WITHDRAWAL, 'amount' => 20000, 'balance_after' => 100000, 'description' => 'Withdrawal kedua', ]); $response = $this->get(route('admin.finance.cash-accounts.index', ['type' => 'withdrawal'])); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/finance/cash-account/index') ->has('transactions.data', 2) ->where('transactions.total', 2) ->where('transactions.current_page', 1) ->where('transactions.per_page', 25) ); }); test('index can filter by type expense', function () { $user = User::factory()->create(); $this->actingAs($user); $cashAccount = CashAccount::factory()->create(['balance' => 100000]); CashTransaction::factory()->create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => $user->id, 'type' => CashTransactionType::DEPOSIT, 'amount' => 100000, 'balance_after' => 100000, 'description' => 'Deposit', ]); CashTransaction::factory()->create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => $user->id, 'type' => CashTransactionType::EXPENSE, 'amount' => 25000, 'balance_after' => 100000, 'description' => 'Pengeluaran', ]); $response = $this->get(route('admin.finance.cash-accounts.index', ['type' => 'expense'])); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/finance/cash-account/index') ->has('transactions.data', 1) ->where('transactions.total', 1) ->where('transactions.current_page', 1) ->where('transactions.per_page', 25) ); }); test('index can filter by type transfer', function () { $user = User::factory()->create(); $this->actingAs($user); $cashAccount = CashAccount::factory()->create(['balance' => 100000]); CashTransaction::factory()->create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => $user->id, 'type' => CashTransactionType::DEPOSIT, 'amount' => 100000, 'balance_after' => 100000, 'description' => 'Deposit', ]); CashTransaction::factory()->create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => $user->id, 'type' => CashTransactionType::TRANSFER, 'amount' => 50000, 'balance_after' => 100000, 'description' => 'Transfer', ]); $response = $this->get(route('admin.finance.cash-accounts.index', ['type' => 'transfer'])); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/finance/cash-account/index') ->has('transactions.data', 1) ->where('transactions.total', 1) ->where('transactions.current_page', 1) ->where('transactions.per_page', 25) ); }); test('index without type filter shows all transactions', function () { $user = User::factory()->create(); $this->actingAs($user); $cashAccount = CashAccount::factory()->create(['balance' => 200000]); CashTransaction::factory()->create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => $user->id, 'type' => CashTransactionType::DEPOSIT, 'amount' => 100000, 'balance_after' => 100000, 'description' => 'Deposit', ]); CashTransaction::factory()->create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => $user->id, 'type' => CashTransactionType::WITHDRAWAL, 'amount' => 50000, 'balance_after' => 200000, 'description' => 'Withdrawal', ]); CashTransaction::factory()->create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => $user->id, 'type' => CashTransactionType::EXPENSE, 'amount' => 30000, 'balance_after' => 200000, 'description' => 'Expense', ]); $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.data', 3) ->where('transactions.total', 3) ->where('transactions.current_page', 1) ->where('transactions.per_page', 25) ); }); test('index passes type filter to view', function () { $user = User::factory()->create(); $this->actingAs($user); $response = $this->get(route('admin.finance.cash-accounts.index', ['type' => 'deposit'])); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->where('filters.type', 'deposit') ); }); test('index passes empty filters when no type specified', 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 ->where('filters', []) ); }); test('index with invalid type filter shows no transactions', function () { $user = User::factory()->create(); $this->actingAs($user); $cashAccount = CashAccount::factory()->create(['balance' => 100000]); CashTransaction::factory()->create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => $user->id, 'type' => CashTransactionType::DEPOSIT, 'amount' => 100000, 'balance_after' => 100000, 'description' => 'Deposit', ]); $response = $this->get(route('admin.finance.cash-accounts.index', ['type' => 'invalid_type'])); $response->assertOk(); $response->assertInertia(fn (Assert $page) => $page ->component('admin/finance/cash-account/index') ->has('transactions.data', 0) ->where('transactions.total', 0) ->where('transactions.current_page', 1) ->where('transactions.per_page', 25) ); }); /* |-------------------------------------------------------------------------- | 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); });