479 lines
18 KiB
PHP
479 lines
18 KiB
PHP
<?php
|
|
|
|
use App\Enums\Permission as PermissionEnum;
|
|
use App\Models\CashAccount;
|
|
use App\Models\CashTransaction;
|
|
use App\Models\User;
|
|
use Database\Seeders\CashAccountSeeder;
|
|
use Database\Seeders\RolePermissionSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Storage;
|
|
|
|
uses(RefreshDatabase::class);
|
|
|
|
beforeEach(function () {
|
|
$this->seed(RolePermissionSeeder::class);
|
|
User::factory()->create(); // Required by CashAccountSeeder
|
|
$this->seed(CashAccountSeeder::class);
|
|
Storage::fake('public');
|
|
});
|
|
|
|
// ─── Helper ───────────────────────────────────────────────
|
|
|
|
function createCashUserWithPermission(PermissionEnum ...$permissions): User
|
|
{
|
|
$user = User::factory()->create();
|
|
|
|
$user->givePermissionTo(
|
|
array_merge(
|
|
[PermissionEnum::DASHBOARD_VIEW->value],
|
|
array_map(fn (PermissionEnum $p) => $p->value, $permissions)
|
|
)
|
|
);
|
|
|
|
$user->forgetCachedPermissions();
|
|
|
|
return $user;
|
|
}
|
|
|
|
function createCashAccountWithBalance(int $balance = 1000000): CashAccount
|
|
{
|
|
return CashAccount::factory()->create(['balance' => $balance]);
|
|
}
|
|
|
|
function depositPayload(?int $amount = null, ?string $description = null): array
|
|
{
|
|
return [
|
|
'amount' => $amount ?? 100000,
|
|
'description' => $description ?? 'Setoran kas',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg', 100, 100)],
|
|
];
|
|
}
|
|
|
|
function withdrawPayload(?int $amount = null, ?string $description = null): array
|
|
{
|
|
return [
|
|
'amount' => $amount ?? 50000,
|
|
'description' => $description ?? 'Penarikan kas',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg', 100, 100)],
|
|
];
|
|
}
|
|
|
|
// ─── Index ────────────────────────────────────────────────
|
|
|
|
describe('Cash Index', function () {
|
|
test('authenticated user with permission can view cash index', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.finance.cash.index'))
|
|
->assertOk();
|
|
});
|
|
|
|
test('guest is redirected to login', function () {
|
|
$this->get(route('admin.finance.cash.index'))
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without permission is forbidden', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.finance.cash.index'))
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('index displays cash transactions', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW);
|
|
|
|
$account = CashAccount::query()->first();
|
|
CashTransaction::factory()->count(3)->create([
|
|
'cash_account_id' => $account->id,
|
|
'created_by_id' => $user->id,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.finance.cash.index'))
|
|
->assertOk();
|
|
});
|
|
|
|
test('index can search transactions by description', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW);
|
|
|
|
$account = CashAccount::query()->first();
|
|
CashTransaction::factory()->create([
|
|
'cash_account_id' => $account->id,
|
|
'created_by_id' => $user->id,
|
|
'description' => 'Setoran modal awal',
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.finance.cash.index', ['search' => 'modal']))
|
|
->assertOk();
|
|
});
|
|
});
|
|
|
|
// ─── Deposit ──────────────────────────────────────────────
|
|
|
|
describe('Cash Deposit', function () {
|
|
test('authenticated user with permission can deposit', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_DEPOSIT);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.deposit'), depositPayload(200000))
|
|
->assertRedirect(route('admin.finance.cash.index'));
|
|
|
|
$account = CashAccount::query()->first();
|
|
expect($account->fresh()->balance)->toBe(200000);
|
|
});
|
|
|
|
test('guest cannot deposit', function () {
|
|
$this->post(route('admin.finance.cash.deposit'), depositPayload())
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without deposit permission is forbidden', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.deposit'), depositPayload())
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('amount is required', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_DEPOSIT);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.deposit'), [
|
|
'amount' => '',
|
|
'description' => 'Test',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg')],
|
|
])
|
|
->assertSessionHasErrors('amount');
|
|
});
|
|
|
|
test('amount must be at least 1', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_DEPOSIT);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.deposit'), [
|
|
'amount' => 0,
|
|
'description' => 'Test',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg')],
|
|
])
|
|
->assertSessionHasErrors('amount');
|
|
});
|
|
|
|
test('description is required', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_DEPOSIT);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.deposit'), [
|
|
'amount' => 100000,
|
|
'description' => '',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg')],
|
|
])
|
|
->assertSessionHasErrors('description');
|
|
});
|
|
|
|
test('description must not exceed 100 characters', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_DEPOSIT);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.deposit'), [
|
|
'amount' => 100000,
|
|
'description' => str_repeat('a', 101),
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg')],
|
|
])
|
|
->assertSessionHasErrors('description');
|
|
});
|
|
|
|
test('deposit creates a cash transaction', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_DEPOSIT);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.deposit'), depositPayload(500000, 'Setoran modal'));
|
|
|
|
$this->assertDatabaseHas('cash_transactions', [
|
|
'description' => 'Setoran modal',
|
|
'amount' => 500000,
|
|
]);
|
|
});
|
|
|
|
test('deposit updates account balance', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_DEPOSIT);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.deposit'), depositPayload(300000));
|
|
|
|
$account = CashAccount::query()->first();
|
|
expect($account->fresh()->balance)->toBe(300000);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.deposit'), depositPayload(200000));
|
|
|
|
expect($account->fresh()->balance)->toBe(500000);
|
|
});
|
|
});
|
|
|
|
// ─── Withdraw ─────────────────────────────────────────────
|
|
|
|
describe('Cash Withdraw', function () {
|
|
test('authenticated user with permission can withdraw', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_WITHDRAW);
|
|
|
|
CashAccount::query()->first()->update(['balance' => 500000]);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.withdraw'), withdrawPayload(100000))
|
|
->assertRedirect(route('admin.finance.cash.index'));
|
|
|
|
$account = CashAccount::query()->first();
|
|
expect($account->fresh()->balance)->toBe(400000);
|
|
});
|
|
|
|
test('guest cannot withdraw', function () {
|
|
$this->post(route('admin.finance.cash.withdraw'), withdrawPayload())
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without withdraw permission is forbidden', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.withdraw'), withdrawPayload())
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('amount is required', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_WITHDRAW);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.withdraw'), [
|
|
'amount' => '',
|
|
'description' => 'Test',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg')],
|
|
])
|
|
->assertSessionHasErrors('amount');
|
|
});
|
|
|
|
test('amount must be at least 1', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_WITHDRAW);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.withdraw'), [
|
|
'amount' => 0,
|
|
'description' => 'Test',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg')],
|
|
])
|
|
->assertSessionHasErrors('amount');
|
|
});
|
|
|
|
test('description is required', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_WITHDRAW);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.withdraw'), [
|
|
'amount' => 100000,
|
|
'description' => '',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg')],
|
|
])
|
|
->assertSessionHasErrors('description');
|
|
});
|
|
|
|
test('withdraw fails if insufficient balance', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_WITHDRAW);
|
|
|
|
CashAccount::query()->first()->update(['balance' => 50000]);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.withdraw'), withdrawPayload(100000))
|
|
->assertSessionHasErrors('amount');
|
|
});
|
|
|
|
test('withdraw creates a cash transaction', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_WITHDRAW);
|
|
|
|
CashAccount::query()->first()->update(['balance' => 500000]);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.cash.withdraw'), withdrawPayload(200000, 'Pembelian bahan'));
|
|
|
|
$this->assertDatabaseHas('cash_transactions', [
|
|
'description' => 'Pembelian bahan',
|
|
'amount' => 200000,
|
|
]);
|
|
});
|
|
});
|
|
|
|
// ─── Update ───────────────────────────────────────────────
|
|
|
|
describe('Cash Update', function () {
|
|
test('authenticated user with permission can update a manual transaction', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_UPDATE);
|
|
|
|
$account = CashAccount::query()->first();
|
|
$transaction = CashTransaction::factory()->deposit()->create([
|
|
'cash_account_id' => $account->id,
|
|
'created_by_id' => $user->id,
|
|
'amount' => 100000,
|
|
'balance_after' => 100000,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.finance.cash.transactions.update', $transaction), [
|
|
'amount' => 150000,
|
|
'description' => 'Deskripsi diperbarui',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg')],
|
|
])
|
|
->assertRedirect(route('admin.finance.cash.index'));
|
|
|
|
expect($transaction->fresh()->amount)->toBe(150000);
|
|
expect($transaction->fresh()->description)->toBe('Deskripsi diperbarui');
|
|
});
|
|
|
|
test('guest cannot update a transaction', function () {
|
|
$account = CashAccount::query()->first();
|
|
$transaction = CashTransaction::factory()->deposit()->create([
|
|
'cash_account_id' => $account->id,
|
|
]);
|
|
|
|
$this->put(route('admin.finance.cash.transactions.update', $transaction), [
|
|
'amount' => 100000,
|
|
'description' => 'Test',
|
|
])->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without update permission is forbidden', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW);
|
|
|
|
$account = CashAccount::query()->first();
|
|
$transaction = CashTransaction::factory()->deposit()->create([
|
|
'cash_account_id' => $account->id,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.finance.cash.transactions.update', $transaction), [
|
|
'amount' => 100000,
|
|
'description' => 'Test',
|
|
])
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('referenced transaction cannot be updated', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_UPDATE);
|
|
|
|
$account = CashAccount::query()->first();
|
|
$transaction = CashTransaction::factory()->create([
|
|
'cash_account_id' => $account->id,
|
|
'reference_type' => 'App\\Models\\Expense',
|
|
'reference_id' => 1,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.finance.cash.transactions.update', $transaction), [
|
|
'amount' => 100000,
|
|
'description' => 'Test',
|
|
])
|
|
->assertForbidden();
|
|
});
|
|
});
|
|
|
|
// ─── Destroy ──────────────────────────────────────────────
|
|
|
|
describe('Cash Destroy', function () {
|
|
test('authenticated user with permission can delete a manual transaction', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_DELETE);
|
|
|
|
$account = CashAccount::query()->first();
|
|
$transaction = CashTransaction::factory()->deposit()->create([
|
|
'cash_account_id' => $account->id,
|
|
'created_by_id' => $user->id,
|
|
'amount' => 100000,
|
|
'balance_after' => 100000,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->delete(route('admin.finance.cash.transactions.destroy', $transaction))
|
|
->assertRedirect(route('admin.finance.cash.index'));
|
|
|
|
$this->assertSoftDeleted('cash_transactions', ['id' => $transaction->id]);
|
|
});
|
|
|
|
test('guest cannot delete a transaction', function () {
|
|
$account = CashAccount::query()->first();
|
|
$transaction = CashTransaction::factory()->deposit()->create([
|
|
'cash_account_id' => $account->id,
|
|
]);
|
|
|
|
$this->delete(route('admin.finance.cash.transactions.destroy', $transaction))
|
|
->assertRedirect(route('login'));
|
|
|
|
$this->assertNotSoftDeleted('cash_transactions', ['id' => $transaction->id]);
|
|
});
|
|
|
|
test('user without delete permission is forbidden', function () {
|
|
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW);
|
|
|
|
$account = CashAccount::query()->first();
|
|
$transaction = CashTransaction::factory()->deposit()->create([
|
|
'cash_account_id' => $account->id,
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->delete(route('admin.finance.cash.transactions.destroy', $transaction))
|
|
->assertForbidden();
|
|
|
|
$this->assertNotSoftDeleted('cash_transactions', ['id' => $transaction->id]);
|
|
});
|
|
});
|
|
|
|
// ─── Cash Model ───────────────────────────────────────────
|
|
|
|
describe('Cash Model', function () {
|
|
test('cash transaction uses soft deletes', function () {
|
|
$account = CashAccount::query()->first();
|
|
$transaction = CashTransaction::factory()->create([
|
|
'cash_account_id' => $account->id,
|
|
]);
|
|
|
|
$transaction->delete();
|
|
|
|
expect($transaction->trashed())->toBeTrue();
|
|
});
|
|
|
|
test('cash account has balance cast to integer', function () {
|
|
$account = CashAccount::factory()->create(['balance' => 500000]);
|
|
|
|
expect($account->balance)->toBeInt();
|
|
expect($account->balance)->toBe(500000);
|
|
});
|
|
|
|
test('cash account has balance formatted accessor', function () {
|
|
$account = CashAccount::factory()->create(['balance' => 1500000]);
|
|
|
|
expect($account->balance_formatted)->toBe('Rp 1.500.000');
|
|
});
|
|
|
|
test('cash transaction has amount formatted accessor', function () {
|
|
$account = CashAccount::query()->first();
|
|
$transaction = CashTransaction::factory()->create([
|
|
'cash_account_id' => $account->id,
|
|
'amount' => 2500000,
|
|
]);
|
|
|
|
expect($transaction->amount_formatted)->toBe('Rp 2.500.000');
|
|
});
|
|
|
|
test('cash transaction belongs to cash account', function () {
|
|
$account = CashAccount::query()->first();
|
|
$transaction = CashTransaction::factory()->create([
|
|
'cash_account_id' => $account->id,
|
|
]);
|
|
|
|
expect($transaction->cashAccount)->not->toBeNull();
|
|
expect($transaction->cashAccount->id)->toBe($account->id);
|
|
});
|
|
});
|