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:
parent
143448e6e0
commit
c685f45bcf
@ -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');
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user