369 lines
14 KiB
PHP
369 lines
14 KiB
PHP
<?php
|
|
|
|
use App\Enums\Permission as PermissionEnum;
|
|
use App\Models\CashAccount;
|
|
use App\Models\CashTransaction;
|
|
use App\Models\Expense;
|
|
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 createExpenseUserWithPermission(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 createExpenseWithCashTransaction(User $user): Expense
|
|
{
|
|
$account = CashAccount::query()->first();
|
|
$account->update(['balance' => 5000000]);
|
|
|
|
$expense = Expense::factory()->create([
|
|
'created_by_id' => $user->id,
|
|
'amount' => 100000,
|
|
]);
|
|
|
|
$cashTransaction = CashTransaction::factory()->create([
|
|
'cash_account_id' => $account->id,
|
|
'created_by_id' => $user->id,
|
|
'reference_type' => Expense::class,
|
|
'reference_id' => $expense->id,
|
|
'amount' => $expense->amount,
|
|
'balance_after' => $account->balance - $expense->amount,
|
|
]);
|
|
|
|
$expense->update(['cash_transaction_id' => $cashTransaction->id]);
|
|
|
|
return $expense;
|
|
}
|
|
|
|
function expensePayload(?int $amount = null, ?string $description = null): array
|
|
{
|
|
return [
|
|
'amount' => $amount ?? 100000,
|
|
'description' => $description ?? 'Pembelian ATK',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg', 100, 100)],
|
|
];
|
|
}
|
|
|
|
// ─── Index ────────────────────────────────────────────────
|
|
|
|
describe('Expense Index', function () {
|
|
test('authenticated user with permission can view expense index', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.finance.expenses.index'))
|
|
->assertOk();
|
|
});
|
|
|
|
test('guest is redirected to login', function () {
|
|
$this->get(route('admin.finance.expenses.index'))
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without permission is forbidden', function () {
|
|
$user = User::factory()->create();
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.finance.expenses.index'))
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('index displays expenses', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW);
|
|
|
|
Expense::factory()->count(3)->create(['created_by_id' => $user->id]);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.finance.expenses.index'))
|
|
->assertOk();
|
|
});
|
|
|
|
test('index can search expenses by description', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW);
|
|
|
|
Expense::factory()->create([
|
|
'created_by_id' => $user->id,
|
|
'description' => 'Pembelian kain',
|
|
]);
|
|
|
|
$this->actingAs($user)
|
|
->get(route('admin.finance.expenses.index', ['search' => 'kain']))
|
|
->assertOk();
|
|
});
|
|
});
|
|
|
|
// ─── Store ────────────────────────────────────────────────
|
|
|
|
describe('Expense Store', function () {
|
|
test('authenticated user with permission can create an expense', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW, PermissionEnum::EXPENSES_CREATE);
|
|
|
|
CashAccount::query()->first()->update(['balance' => 5000000]);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.expenses.store'), expensePayload(250000, 'Beli mesin jahit'))
|
|
->assertRedirect(route('admin.finance.expenses.index'));
|
|
|
|
$this->assertDatabaseHas('expenses', [
|
|
'amount' => 250000,
|
|
'description' => 'Beli mesin jahit',
|
|
]);
|
|
});
|
|
|
|
test('guest cannot create an expense', function () {
|
|
$this->post(route('admin.finance.expenses.store'), expensePayload())
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without create permission is forbidden', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.expenses.store'), expensePayload())
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('amount is required', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW, PermissionEnum::EXPENSES_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.expenses.store'), [
|
|
'amount' => '',
|
|
'description' => 'Test',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg')],
|
|
])
|
|
->assertSessionHasErrors('amount');
|
|
});
|
|
|
|
test('amount must be at least 1', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW, PermissionEnum::EXPENSES_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.expenses.store'), [
|
|
'amount' => 0,
|
|
'description' => 'Test',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg')],
|
|
])
|
|
->assertSessionHasErrors('amount');
|
|
});
|
|
|
|
test('description is required', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW, PermissionEnum::EXPENSES_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.expenses.store'), [
|
|
'amount' => 100000,
|
|
'description' => '',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg')],
|
|
])
|
|
->assertSessionHasErrors('description');
|
|
});
|
|
|
|
test('description must not exceed 100 characters', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW, PermissionEnum::EXPENSES_CREATE);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.expenses.store'), [
|
|
'amount' => 100000,
|
|
'description' => str_repeat('a', 101),
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg')],
|
|
])
|
|
->assertSessionHasErrors('description');
|
|
});
|
|
|
|
test('creating expense also creates cash transaction', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW, PermissionEnum::EXPENSES_CREATE);
|
|
|
|
CashAccount::query()->first()->update(['balance' => 5000000]);
|
|
|
|
$this->actingAs($user)
|
|
->post(route('admin.finance.expenses.store'), expensePayload(300000, 'Operasional'));
|
|
|
|
$expense = Expense::where('description', 'Operasional')->first();
|
|
expect($expense)->not->toBeNull();
|
|
expect($expense->cash_transaction_id)->not->toBeNull();
|
|
|
|
$this->assertDatabaseHas('cash_transactions', [
|
|
'id' => $expense->cash_transaction_id,
|
|
'amount' => 300000,
|
|
]);
|
|
});
|
|
});
|
|
|
|
// ─── Update ───────────────────────────────────────────────
|
|
|
|
describe('Expense Update', function () {
|
|
test('authenticated user with permission can update an expense', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW, PermissionEnum::EXPENSES_UPDATE);
|
|
|
|
$expense = createExpenseWithCashTransaction($user);
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.finance.expenses.update', $expense), expensePayload(200000, 'Deskripsi baru'))
|
|
->assertRedirect(route('admin.finance.expenses.index'));
|
|
|
|
expect($expense->fresh()->amount)->toBe(200000);
|
|
expect($expense->fresh()->description)->toBe('Deskripsi baru');
|
|
});
|
|
|
|
test('guest cannot update an expense', function () {
|
|
$user = User::factory()->create();
|
|
$expense = createExpenseWithCashTransaction($user);
|
|
|
|
$this->put(route('admin.finance.expenses.update', $expense), expensePayload())
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('user without update permission is forbidden', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW);
|
|
$expense = createExpenseWithCashTransaction($user);
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.finance.expenses.update', $expense), expensePayload())
|
|
->assertForbidden();
|
|
});
|
|
|
|
test('amount is required on update', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW, PermissionEnum::EXPENSES_UPDATE);
|
|
$expense = createExpenseWithCashTransaction($user);
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.finance.expenses.update', $expense), [
|
|
'amount' => '',
|
|
'description' => 'Test',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg')],
|
|
])
|
|
->assertSessionHasErrors('amount');
|
|
});
|
|
|
|
test('description is required on update', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW, PermissionEnum::EXPENSES_UPDATE);
|
|
$expense = createExpenseWithCashTransaction($user);
|
|
|
|
$this->actingAs($user)
|
|
->put(route('admin.finance.expenses.update', $expense), [
|
|
'amount' => 100000,
|
|
'description' => '',
|
|
'photos' => [UploadedFile::fake()->image('bukti.jpg')],
|
|
])
|
|
->assertSessionHasErrors('description');
|
|
});
|
|
});
|
|
|
|
// ─── Destroy ──────────────────────────────────────────────
|
|
|
|
describe('Expense Destroy', function () {
|
|
test('authenticated user with permission can delete an expense', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW, PermissionEnum::EXPENSES_DELETE);
|
|
|
|
$expense = createExpenseWithCashTransaction($user);
|
|
|
|
$this->actingAs($user)
|
|
->delete(route('admin.finance.expenses.destroy', $expense))
|
|
->assertRedirect(route('admin.finance.expenses.index'));
|
|
|
|
$this->assertSoftDeleted('expenses', ['id' => $expense->id]);
|
|
});
|
|
|
|
test('guest cannot delete an expense', function () {
|
|
$user = User::factory()->create();
|
|
$expense = createExpenseWithCashTransaction($user);
|
|
|
|
$this->delete(route('admin.finance.expenses.destroy', $expense))
|
|
->assertRedirect(route('login'));
|
|
|
|
$this->assertNotSoftDeleted('expenses', ['id' => $expense->id]);
|
|
});
|
|
|
|
test('user without delete permission is forbidden', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW);
|
|
$expense = createExpenseWithCashTransaction($user);
|
|
|
|
$this->actingAs($user)
|
|
->delete(route('admin.finance.expenses.destroy', $expense))
|
|
->assertForbidden();
|
|
|
|
$this->assertNotSoftDeleted('expenses', ['id' => $expense->id]);
|
|
});
|
|
|
|
test('deleting expense also deletes cash transaction', function () {
|
|
$user = createExpenseUserWithPermission(PermissionEnum::EXPENSES_VIEW, PermissionEnum::EXPENSES_DELETE);
|
|
|
|
$expense = createExpenseWithCashTransaction($user);
|
|
$cashTransactionId = $expense->cash_transaction_id;
|
|
|
|
$this->actingAs($user)
|
|
->delete(route('admin.finance.expenses.destroy', $expense));
|
|
|
|
$this->assertSoftDeleted('expenses', ['id' => $expense->id]);
|
|
$this->assertSoftDeleted('cash_transactions', ['id' => $cashTransactionId]);
|
|
});
|
|
});
|
|
|
|
// ─── Expense Model ────────────────────────────────────────
|
|
|
|
describe('Expense Model', function () {
|
|
test('expense uses soft deletes', function () {
|
|
$expense = Expense::factory()->create();
|
|
|
|
$expense->delete();
|
|
|
|
expect($expense->trashed())->toBeTrue();
|
|
});
|
|
|
|
test('expense has amount cast to integer', function () {
|
|
$expense = Expense::factory()->create(['amount' => 500000]);
|
|
|
|
expect($expense->amount)->toBeInt();
|
|
expect($expense->amount)->toBe(500000);
|
|
});
|
|
|
|
test('expense has amount formatted accessor', function () {
|
|
$expense = Expense::factory()->create(['amount' => 1500000]);
|
|
|
|
expect($expense->amount_formatted)->toBe('Rp 1.500.000');
|
|
});
|
|
|
|
test('expense belongs to cash transaction', function () {
|
|
$user = User::factory()->create();
|
|
$expense = createExpenseWithCashTransaction($user);
|
|
|
|
expect($expense->cashTransaction)->not->toBeNull();
|
|
});
|
|
|
|
test('expense belongs to created by user', function () {
|
|
$user = User::factory()->create();
|
|
$expense = Expense::factory()->create(['created_by_id' => $user->id]);
|
|
|
|
expect($expense->createdBy)->not->toBeNull();
|
|
expect($expense->createdBy->id)->toBe($user->id);
|
|
});
|
|
});
|