firstOrFail(); } /** * @param array{search: string, sort: string, direction: 'asc'|'desc'} $tableQuery */ public function paginateForIndex(CashAccount $cashAccount, array $tableQuery): LengthAwarePaginator { $query = CashTransaction::query() ->with(['createdBy.profile', 'reference', 'media']) ->where('cash_account_id', $cashAccount->id) ->when($tableQuery['search'] !== '', function (Builder $query) use ($tableQuery): void { $search = $tableQuery['search']; $query->where(function (Builder $query) use ($search): void { $query->where('description', 'like', "%{$search}%"); }); }); $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']); return $query ->paginate(10) ->withQueryString() ->through(function (CashTransaction $transaction) { $transaction->setAttribute( 'photos', MediaPresenter::collection($transaction, 'photos'), ); return $transaction; }); } /** * @param array{amount: int, description: string} $validated */ public function deposit(CashAccount $cashAccount, array $validated, User $user): CashTransaction { return DB::transaction(function () use ($cashAccount, $validated, $user): CashTransaction { $account = CashAccount::query()->lockForUpdate()->findOrFail($cashAccount->id); $amount = (int) $validated['amount']; $newBalance = $account->balance + $amount; $account->balance = $newBalance; $account->save(); $transaction = CashTransaction::create([ 'cash_account_id' => $account->id, 'amount' => $amount, 'balance_after' => $newBalance, 'description' => $validated['description'], 'created_by_id' => $user->id, ]); $this->syncPhotos($transaction, $validated); return $transaction; }); } public function recordOutgoing( Model $reference, int $amount, string $description, User $user, ?CashAccount $cashAccount = null, ): CashTransaction { return DB::transaction(function () use ($reference, $amount, $description, $user, $cashAccount): CashTransaction { $account = CashAccount::query()->lockForUpdate()->findOrFail( ($cashAccount ?? $this->getDefaultAccount())->id, ); if ($account->balance < $amount) { throw ValidationException::withMessages([ 'amount' => 'Saldo kas tidak mencukupi.', ]); } $newBalance = $account->balance - $amount; $account->balance = $newBalance; $account->save(); $transaction = new CashTransaction([ 'cash_account_id' => $account->id, 'amount' => $amount, 'balance_after' => $newBalance, 'description' => $description, 'created_by_id' => $user->id, ]); $transaction->reference()->associate($reference); $transaction->save(); return $transaction; }); } public function recordIncoming( Model $reference, int $amount, string $description, User $user, ?CashAccount $cashAccount = null, ): CashTransaction { return DB::transaction(function () use ($reference, $amount, $description, $user, $cashAccount): CashTransaction { $account = CashAccount::query()->lockForUpdate()->findOrFail( ($cashAccount ?? $this->getDefaultAccount())->id, ); $newBalance = $account->balance + $amount; $account->balance = $newBalance; $account->save(); $transaction = new CashTransaction([ 'cash_account_id' => $account->id, 'amount' => $amount, 'balance_after' => $newBalance, 'description' => $description, 'created_by_id' => $user->id, ]); $transaction->reference()->associate($reference); $transaction->save(); return $transaction; }); } /** * @param array{amount: int, description: string} $validated */ public function updateDeposit(CashTransaction $transaction, array $validated): void { $this->ensureEditable($transaction); DB::transaction(function () use ($transaction, $validated): void { CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id); $transaction->amount = (int) $validated['amount']; $transaction->description = $validated['description']; $transaction->save(); $this->syncPhotos($transaction, $validated); $this->recalculateBalances($transaction->cashAccount); }); } public function deleteTransaction(CashTransaction $transaction): void { $this->ensureEditable($transaction); DB::transaction(function () use ($transaction): void { CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id); $transaction->clearMediaCollection('photos'); $transaction->delete(); $this->recalculateBalances($transaction->cashAccount); }); } public function updateReferencedTransaction( CashTransaction $transaction, int $amount, string $description, ): void { DB::transaction(function () use ($transaction, $amount, $description): void { CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id); $transaction->amount = $amount; $transaction->description = $description; $transaction->save(); $this->recalculateBalances($transaction->cashAccount); $account = $transaction->cashAccount->fresh(); if ($account->balance < 0) { throw ValidationException::withMessages([ 'amount' => 'Saldo kas tidak mencukupi.', ]); } }); } public function deleteReferencedTransaction(CashTransaction $transaction): void { DB::transaction(function () use ($transaction): void { CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id); $account = $transaction->cashAccount; $transaction->delete(); $this->recalculateBalances($account); }); } /** * @param array $validated */ 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', ); } 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); $runningBalance = 0; $transactions = CashTransaction::query() ->with('reference') ->where('cash_account_id', $account->id) ->orderBy('created_at') ->orderBy('id') ->get(); foreach ($transactions as $transaction) { if (CashTransaction::isIncomingTransaction($transaction)) { $runningBalance += $transaction->amount; } else { $runningBalance -= $transaction->amount; } $transaction->balance_after = $runningBalance; $transaction->saveQuietly(); } $account->balance = $runningBalance; $account->save(); } private function applySorting(Builder $query, string $sort, string $direction): void { if (in_array($sort, ['created_at', 'amount', 'reference_type'], true)) { $query->orderBy($sort, $direction); return; } $query->latest(); } }