refactor: replace UpdateCashTransactionRequest with CashTransactionRequest in CashController; add ensureEditable method in CashTransaction model for validation; update CashService to use transaction handling methods; enhance CashTest descriptions for clarity

This commit is contained in:
Yoga Pangestu 2026-07-03 23:44:42 +07:00
parent 143448e6e0
commit c685f45bcf
7 changed files with 92 additions and 157 deletions

View File

@ -5,13 +5,14 @@
use App\Http\Controllers\Concerns\FlashesEntityMessage;
use App\Http\Controllers\Concerns\ParsesDataTableQuery;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\Finance\Cash\CashTransactionRequest;
use App\Http\Requests\Admin\Finance\Cash\DepositRequest;
use App\Http\Requests\Admin\Finance\Cash\WithdrawRequest;
use App\Http\Requests\Admin\Finance\UpdateCashTransactionRequest;
use App\Models\CashTransaction;
use App\Services\Finance\CashService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Inertia\Inertia;
use Inertia\Response;
@ -38,8 +39,14 @@ public function index(Request $request): Response
]);
}
public function update(UpdateCashTransactionRequest $request, CashTransaction $cashTransaction): RedirectResponse
public function update(CashTransactionRequest $request, CashTransaction $cashTransaction): RedirectResponse
{
try {
$cashTransaction->ensureEditable();
} catch (ValidationException) {
abort(403);
}
$this->cashService->updateDeposit($cashTransaction, $request->validated());
$this->flashUpdated('Setor kas');
@ -49,6 +56,12 @@ public function update(UpdateCashTransactionRequest $request, CashTransaction $c
public function destroy(CashTransaction $cashTransaction): RedirectResponse
{
try {
$cashTransaction->ensureEditable();
} catch (ValidationException) {
abort(403);
}
$this->cashService->deleteTransaction($cashTransaction);
$this->flashDeleted('Setor kas');

View File

@ -1,12 +1,12 @@
<?php
namespace App\Http\Requests\Admin\Finance;
namespace App\Http\Requests\Admin\Finance\Cash;
use App\Enums\Permission;
use App\Http\Requests\Concerns\ValidatesMediaUploads;
use Illuminate\Foundation\Http\FormRequest;
class UpdateCashTransactionRequest extends FormRequest
class CashTransactionRequest extends FormRequest
{
use ValidatesMediaUploads;

View File

@ -16,6 +16,7 @@
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Validation\ValidationException;
use Spatie\MediaLibrary\HasMedia;
#[Guarded(['id'])]
@ -127,6 +128,15 @@ public function sourceBadgeClass(): Attribute
}
// 5. Other Methods
public function ensureEditable(): void
{
if ($this->reference_type !== null) {
throw ValidationException::withMessages([
'transaction' => 'Transaksi ini tidak dapat diubah dari halaman kas.',
]);
}
}
public function getEmployeeAdvanceLabel(): string
{
return $this->isEmployeeAdvanceRepayment() ? 'Pelunasan Kasbon' : 'Pencairan Kasbon';

View File

@ -6,18 +6,20 @@
use App\Models\CashAccount;
use App\Models\CashTransaction;
use App\Models\User;
use App\Services\Concerns\RunsInTransaction;
use App\Services\Concerns\SyncsPhotos;
use App\Services\Media\MediaService;
use App\Services\System\PushNotificationService;
use App\Support\Media\MediaPresenter;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Validation\ValidationException;
class CashService
{
use RunsInTransaction, SyncsPhotos;
private const MAX_PHOTOS = 1;
public function __construct(
@ -66,8 +68,8 @@ public function paginateForIndex(CashAccount $cashAccount, array $tableQuery, st
public function deposit(CashAccount $cashAccount, array $validated, User $user): CashTransaction
{
try {
$transaction = DB::transaction(function () use ($cashAccount, $validated, $user): CashTransaction {
$transaction = $this->runInTransaction(
function () use ($cashAccount, $validated, $user): CashTransaction {
$account = CashAccount::query()->lockForUpdate()->findOrFail($cashAccount->id);
$amount = (int) $validated['amount'];
$newBalance = $account->balance + $amount;
@ -83,21 +85,12 @@ public function deposit(CashAccount $cashAccount, array $validated, User $user):
'created_by_id' => $user->id,
]);
$this->syncPhotos($transaction, $validated);
$this->syncPhotos($transaction, $validated, self::MAX_PHOTOS);
return $transaction;
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal melakukan setoran kas: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
},
'Gagal melakukan setoran kas',
);
$this->pushNotificationService->sendToRoles(
'💰 Setoran Kas',
@ -111,8 +104,8 @@ public function deposit(CashAccount $cashAccount, array $validated, User $user):
public function withdraw(CashAccount $cashAccount, array $validated, User $user): CashTransaction
{
try {
$transaction = DB::transaction(function () use ($cashAccount, $validated, $user): CashTransaction {
$transaction = $this->runInTransaction(
function () use ($cashAccount, $validated, $user): CashTransaction {
$account = CashAccount::query()->lockForUpdate()->findOrFail($cashAccount->id);
$amount = (int) $validated['amount'];
@ -135,21 +128,12 @@ public function withdraw(CashAccount $cashAccount, array $validated, User $user)
'created_by_id' => $user->id,
]);
$this->syncPhotos($transaction, $validated);
$this->syncPhotos($transaction, $validated, self::MAX_PHOTOS);
return $transaction;
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal melakukan tarik kas: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
},
'Gagal melakukan tarik kas',
);
$this->pushNotificationService->sendToRoles(
'🏦 Tarik Kas',
@ -168,8 +152,8 @@ public function recordOutgoing(
User $user,
?CashAccount $cashAccount = null,
): CashTransaction {
try {
return DB::transaction(function () use ($reference, $amount, $description, $user, $cashAccount): CashTransaction {
return $this->runInTransaction(
function () use ($reference, $amount, $description, $user, $cashAccount): CashTransaction {
$account = CashAccount::query()->lockForUpdate()->findOrFail(
($cashAccount ?? $this->getDefaultAccount())->id,
);
@ -197,18 +181,9 @@ public function recordOutgoing(
$transaction->save();
return $transaction;
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal mencatat transaksi keluar kas: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
},
'Gagal mencatat transaksi keluar kas',
);
}
public function recordIncoming(
@ -218,8 +193,8 @@ public function recordIncoming(
User $user,
?CashAccount $cashAccount = null,
): CashTransaction {
try {
return DB::transaction(function () use ($reference, $amount, $description, $user, $cashAccount): CashTransaction {
return $this->runInTransaction(
function () use ($reference, $amount, $description, $user, $cashAccount): CashTransaction {
$account = CashAccount::query()->lockForUpdate()->findOrFail(
($cashAccount ?? $this->getDefaultAccount())->id,
);
@ -241,26 +216,15 @@ public function recordIncoming(
$transaction->save();
return $transaction;
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal mencatat transaksi masuk kas: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
},
'Gagal mencatat transaksi masuk kas',
);
}
public function updateDeposit(CashTransaction $transaction, array $validated): void
{
$this->ensureEditable($transaction);
try {
DB::transaction(function () use ($transaction, $validated): void {
$this->runInTransaction(
function () use ($transaction, $validated): void {
CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id);
$transaction->update([
@ -268,7 +232,7 @@ public function updateDeposit(CashTransaction $transaction, array $validated): v
'description' => $validated['description'],
]);
$this->syncPhotos($transaction, $validated);
$this->syncPhotos($transaction, $validated, self::MAX_PHOTOS);
$this->recalculateBalances($transaction->cashAccount);
@ -279,18 +243,9 @@ public function updateDeposit(CashTransaction $transaction, array $validated): v
'amount' => 'Saldo kas tidak mencukupi.',
]);
}
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal memperbarui setoran kas: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
},
'Gagal memperbarui setoran kas',
);
$this->pushNotificationService->sendToRoles(
'✏️ Transaksi Kas Diperbarui',
@ -302,10 +257,11 @@ public function updateDeposit(CashTransaction $transaction, array $validated): v
public function deleteTransaction(CashTransaction $transaction): void
{
$this->ensureEditable($transaction);
$amountFormatted = $transaction->amount_formatted;
$description = $transaction->description;
try {
DB::transaction(function () use ($transaction): void {
$this->runInTransaction(
function () use ($transaction): void {
CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id);
$account = $transaction->cashAccount;
@ -320,22 +276,13 @@ public function deleteTransaction(CashTransaction $transaction): void
'transaction' => 'Saldo kas tidak mencukupi jika transaksi ini dihapus.',
]);
}
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal menghapus transaksi kas: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
},
'Gagal menghapus transaksi kas',
);
$this->pushNotificationService->sendToRoles(
'🗑️ Transaksi Kas Dihapus',
"Transaksi kas senilai {$transaction->amount_formatted} dengan keterangan {$transaction->description} telah dihapus.",
"Transaksi kas senilai {$amountFormatted} dengan keterangan {$description} telah dihapus.",
['owner', 'developer', 'admin-toko'],
route('admin.finance.cash.index'),
);
@ -346,8 +293,8 @@ public function updateReferencedTransaction(
int $amount,
string $description,
): void {
try {
DB::transaction(function () use ($transaction, $amount, $description): void {
$this->runInTransaction(
function () use ($transaction, $amount, $description): void {
CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id);
$transaction->update([
@ -364,24 +311,15 @@ public function updateReferencedTransaction(
'amount' => 'Saldo kas tidak mencukupi.',
]);
}
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal memperbarui transaksi kas referensi: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
},
'Gagal memperbarui transaksi kas referensi',
);
}
public function deleteReferencedTransaction(CashTransaction $transaction): void
{
try {
DB::transaction(function () use ($transaction): void {
$this->runInTransaction(
function () use ($transaction): void {
CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id);
$account = $transaction->cashAccount;
@ -394,43 +332,11 @@ public function deleteReferencedTransaction(CashTransaction $transaction): void
'transaction' => 'Saldo kas tidak mencukupi jika transaksi ini dihapus.',
]);
}
});
} catch (ValidationException $e) {
throw $e;
} catch (\Throwable $e) {
Log::error('Gagal menghapus transaksi kas referensi: '.$e->getMessage(), [
'trace' => $e->getTraceAsString(),
]);
throw ValidationException::withMessages([
'system' => 'Terjadi kesalahan pada server. Silakan laporkan masalah ini ke pihak terkait.',
]);
}
}
private function syncPhotos(CashTransaction $transaction, array $validated): void
{
$this->mediaService->syncCollection(
$transaction,
'photos',
$validated['photos'] ?? null,
$validated['remove_media_ids'] ?? null,
self::MAX_PHOTOS,
required: true,
errorKey: 'photos',
s3Keys: $validated['s3_keys'] ?? null,
},
'Gagal menghapus transaksi kas referensi',
);
}
private function ensureEditable(CashTransaction $transaction): void
{
if ($transaction->reference_type !== null) {
throw ValidationException::withMessages([
'transaction' => 'Transaksi ini tidak dapat diubah dari halaman kas.',
]);
}
}
private function recalculateBalances(CashAccount $cashAccount): void
{
$account = CashAccount::query()->lockForUpdate()->findOrFail($cashAccount->id);

View File

@ -6,15 +6,19 @@ import { DataTable } from '@/components/data-table';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { useCan } from '@/composables/useCan';
import {
useDataTableQuery,
useDataTableQuerySync,
} from '@/composables/useDataTableQuery';
import { CashReferenceType } from '@/constants/cash-reference-type';
import { CashTransactionType } from '@/constants/cash-transaction-type';
import { useDataTableQuery, useDataTableQuerySync } from '@/composables/useDataTableQuery';
import AdminLayout from '@/layouts/AdminLayout.vue';
import { index } from '@/routes/admin/finance/cash';
import type { CashAccount, CashTransactionListItem, PaginatedCashTransactions } from '@/types/cash';
import type { DataTableFilterDef, DataTableSort } from '@/types/data-table';
import CashTransactionFormModal from './form/CashTransactionFormModal.vue';
import { createColumns } from './table/columns';
import { index } from '@/routes/admin/finance/cash';
const props = defineProps<{
cashAccount: CashAccount;
@ -116,11 +120,13 @@ watch(
</div>
<div class="flex shrink-0 items-center gap-2 self-start sm:self-center">
<Button v-if="can('cash.deposit')" variant="outline" @click="openCreateModal(CashTransactionType.DEPOSIT)">
<Button v-if="can('cash.deposit')" variant="outline"
@click="openCreateModal(CashTransactionType.DEPOSIT)">
<ArrowDownCircle class="size-4" />
Setor Kas
</Button>
<Button v-if="can('cash.withdraw')" variant="outline" @click="openCreateModal(CashTransactionType.WITHDRAWAL)">
<Button v-if="can('cash.withdraw')" variant="outline"
@click="openCreateModal(CashTransactionType.WITHDRAWAL)">
<ArrowUpCircle class="size-4" />
Tarik Kas
</Button>
@ -142,7 +148,7 @@ watch(
</Card>
<Card class="min-w-0">
<CardContent class="min-w-0 pt-6">
<CardContent class="min-w-0">
<DataTable v-model:search="search" :columns="columns" :data="transactions.data" :pagination="pagination"
:pagination-links="transactions.links" :sort="currentSort" :filter-defs="filterDefs"
:filter-values="filterValues" @sort-change="setSort" @filter-change="setFilter"

View File

@ -1,5 +1,5 @@
import type { MediaItem } from '@/types/media';
import type { Paginated } from '@/types/common';
import type { MediaItem } from '@/types/media';
export type CashAccount = {
id: number;

View File

@ -307,9 +307,9 @@ function withdrawPayload(?int $amount = null, ?string $description = null): arra
});
});
// ─── Update Transaction ───────────────────────────────────
// ─── Update ───────────────────────────────────────────────
describe('Cash Update Transaction', function () {
describe('Cash Update', function () {
test('authenticated user with permission can update a manual transaction', function () {
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_UPDATE);
@ -380,9 +380,9 @@ function withdrawPayload(?int $amount = null, ?string $description = null): arra
});
});
// ─── Destroy Transaction ──────────────────────────────────
// ─── Destroy ──────────────────────────────────────────────
describe('Cash Destroy Transaction', function () {
describe('Cash Destroy', function () {
test('authenticated user with permission can delete a manual transaction', function () {
$user = createCashUserWithPermission(PermissionEnum::CASH_VIEW, PermissionEnum::CASH_DELETE);