cacheRemember('finance:default_account', 86400, function (): CashAccount { return CashAccount::query()->firstOrFail(); }); } public function paginateForIndex(CashAccount $cashAccount, array $tableQuery, string $referenceType = ''): 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}%"); }); }) ->when($referenceType !== '', function (Builder $query) use ($referenceType): void { if ($referenceType === 'manual') { $query->whereNull('reference_type'); } else { $query->where('reference_type', $referenceType); } }); $this->applySorting($query, $tableQuery['sort'], $tableQuery['direction']); return $query ->paginate(25) ->withQueryString() ->through(function (CashTransaction $transaction) { $transaction->setAttribute( 'photos', MediaPresenter::collection($transaction, 'photos'), ); return $transaction; }); } public function deposit(CashAccount $cashAccount, array $validated, User $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; $account->update(['balance' => $newBalance]); $transaction = CashTransaction::create([ 'cash_account_id' => $account->id, 'type' => CashTransactionType::DEPOSIT, 'amount' => $amount, 'balance_after' => $newBalance, 'description' => $validated['description'], 'created_by_id' => $user->id, ]); $this->syncPhotos($transaction, $validated, self::MAX_PHOTOS); return $transaction; }, 'Gagal melakukan setoran kas', ); $this->cacheForget('finance:default_account'); $this->pushNotificationService->sendToRoles( '💰 Setoran Kas', "Setoran kas baru {$transaction->amount_formatted} dengan keterangan: {$transaction->description} oleh {$user->profile?->full_name}.", ['owner', 'developer', 'admin-toko'], route('admin.finance.cash.index'), ); return $transaction; } public function withdraw(CashAccount $cashAccount, array $validated, User $user): CashTransaction { $transaction = $this->runInTransaction( function () use ($cashAccount, $validated, $user): CashTransaction { $account = CashAccount::query()->lockForUpdate()->findOrFail($cashAccount->id); $amount = (int) $validated['amount']; if ($account->balance < $amount) { throw ValidationException::withMessages([ 'amount' => 'Saldo kas tidak mencukupi.', ]); } $newBalance = $account->balance - $amount; $account->update(['balance' => $newBalance]); $transaction = CashTransaction::create([ 'cash_account_id' => $account->id, 'type' => CashTransactionType::WITHDRAWAL, 'amount' => $amount, 'balance_after' => $newBalance, 'description' => $validated['description'], 'created_by_id' => $user->id, ]); $this->syncPhotos($transaction, $validated, self::MAX_PHOTOS); return $transaction; }, 'Gagal melakukan tarik kas', ); $this->cacheForget('finance:default_account'); $this->pushNotificationService->sendToRoles( '🏦 Tarik Kas', "Tarik kas {$transaction->amount_formatted} dengan keterangan: {$transaction->description} oleh {$user->profile?->full_name}.", ['owner', 'developer', 'admin-toko'], route('admin.finance.cash.index'), ); return $transaction; } public function recordOutgoing( Model $reference, int $amount, string $description, User $user, ?CashAccount $cashAccount = null, ): CashTransaction { $transaction = $this->runInTransaction( 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->update(['balance' => $newBalance]); $transaction = new CashTransaction([ 'cash_account_id' => $account->id, 'type' => CashTransactionType::WITHDRAWAL, 'amount' => $amount, 'balance_after' => $newBalance, 'description' => $description, 'created_by_id' => $user->id, ]); $transaction->reference()->associate($reference); $transaction->save(); return $transaction; }, 'Gagal mencatat transaksi keluar kas', ); $this->cacheForget('finance:default_account'); return $transaction; } public function recordIncoming( Model $reference, int $amount, string $description, User $user, ?CashAccount $cashAccount = null, ): CashTransaction { $transaction = $this->runInTransaction( function () use ($reference, $amount, $description, $user, $cashAccount): CashTransaction { $account = CashAccount::query()->lockForUpdate()->findOrFail( ($cashAccount ?? $this->getDefaultAccount())->id, ); $newBalance = $account->balance + $amount; $account->update(['balance' => $newBalance]); $transaction = new CashTransaction([ 'cash_account_id' => $account->id, 'type' => CashTransactionType::DEPOSIT, 'amount' => $amount, 'balance_after' => $newBalance, 'description' => $description, 'created_by_id' => $user->id, ]); $transaction->reference()->associate($reference); $transaction->save(); return $transaction; }, 'Gagal mencatat transaksi masuk kas', ); $this->cacheForget('finance:default_account'); return $transaction; } public function updateDeposit(CashTransaction $transaction, array $validated, User $user): void { $this->runInTransaction( function () use ($transaction, $validated): void { CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id); $transaction->update([ 'amount' => (int) $validated['amount'], 'description' => $validated['description'], ]); $this->syncPhotos($transaction, $validated, self::MAX_PHOTOS); $this->recalculateBalances($transaction->cashAccount); $account = $transaction->cashAccount->fresh(); if ($account->balance < 0) { throw ValidationException::withMessages([ 'amount' => 'Saldo kas tidak mencukupi.', ]); } }, 'Gagal memperbarui setoran kas', ); $this->cacheForget('finance:default_account'); $this->pushNotificationService->sendToRoles( '✏️ Transaksi Kas Diperbarui', "Transaksi kas dengan keterangan {$transaction->description} diperbarui menjadi senilai {$transaction->amount_formatted} oleh {$user->profile?->full_name}.", ['owner', 'developer', 'admin-toko'], route('admin.finance.cash.index'), ); } public function deleteTransaction(CashTransaction $transaction, User $user): void { $amountFormatted = $transaction->amount_formatted; $description = $transaction->description; $this->runInTransaction( function () use ($transaction): void { CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id); $account = $transaction->cashAccount; $transaction->delete(); $this->recalculateBalances($account); if ($account->fresh()->balance < 0) { throw ValidationException::withMessages([ 'transaction' => 'Saldo kas tidak mencukupi jika transaksi ini dihapus.', ]); } }, 'Gagal menghapus transaksi kas', ); $this->cacheForget('finance:default_account'); $this->pushNotificationService->sendToRoles( '🗑️ Transaksi Kas Dihapus', "Transaksi kas senilai {$amountFormatted} dengan keterangan {$description} telah dihapus oleh {$user->profile?->full_name}.", ['owner', 'developer', 'admin-toko'], route('admin.finance.cash.index'), ); } public function updateReferencedTransaction( CashTransaction $transaction, int $amount, string $description, ): void { $this->runInTransaction( function () use ($transaction, $amount, $description): void { CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id); $transaction->update([ 'amount' => $amount, 'description' => $description, ]); $this->recalculateBalances($transaction->cashAccount); $account = $transaction->cashAccount->fresh(); if ($account->balance < 0) { throw ValidationException::withMessages([ 'amount' => 'Saldo kas tidak mencukupi.', ]); } }, 'Gagal memperbarui transaksi kas referensi', ); $this->cacheForget('finance:default_account'); } public function deleteReferencedTransaction(CashTransaction $transaction): void { $this->runInTransaction( function () use ($transaction): void { CashAccount::query()->lockForUpdate()->findOrFail($transaction->cash_account_id); $account = $transaction->cashAccount; $transaction->delete(); $this->recalculateBalances($account); if ($account->fresh()->balance < 0) { throw ValidationException::withMessages([ 'transaction' => 'Saldo kas tidak mencukupi jika transaksi ini dihapus.', ]); } }, 'Gagal menghapus transaksi kas referensi', ); $this->cacheForget('finance:default_account'); } 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 ($transaction->type === CashTransactionType::DEPOSIT) { $runningBalance += $transaction->amount; } else { $runningBalance -= $transaction->amount; } $transaction->updateQuietly(['balance_after' => $runningBalance]); } $account->update(['balance' => $runningBalance]); } 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(); } }