first(); } public function getAllTransactions(array $filters = []): Collection { $cashAccount = $this->get(); if (! $cashAccount) { return collect(); } return $cashAccount->cashTransactions() ->select(['id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at']) ->with('createdBy.userProfile', 'media') ->when($filters['type'] ?? null, function ($query, $type) { $query->where('type', $type); }) ->latest() ->get() ->map(fn (CashTransaction $transaction) => $this->formatTransaction($transaction)); } public function paginatedTransactions(int $perPage = 25, string $search = '', string $sort = 'created_at', string $direction = 'desc', array $filters = []): LengthAwarePaginator { $cashAccount = $this->get(); if (! $cashAccount) { return new PaginationLengthAwarePaginator(collect(), 0, $perPage); } $paginator = $cashAccount->cashTransactions() ->select(['id', 'created_by_id', 'reference_type', 'reference_id', 'amount', 'balance_after', 'type', 'description', 'created_at']) ->with('createdBy.userProfile', 'media') ->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%")) ->when($filters['type'] ?? null, function ($query, $type) { $query->where('type', $type); }) ->orderBy($sort, $direction) ->paginate($perPage); $paginator->getCollection()->transform(fn (CashTransaction $transaction) => $this->formatTransaction($transaction)); return $paginator; } private function formatTransaction(CashTransaction $transaction): array { $media = $transaction->getFirstMedia('receipts'); if (! $media) { return $transaction->toArray() + [ 'receipt_key' => null, 'receipt_url' => null, ]; } $s3Key = $media->file_name; if (str_starts_with($s3Key, '/') || ! str_contains($s3Key, '/')) { $s3Key = $media->getPath(); } return $transaction->toArray() + [ 'receipt_key' => $s3Key, 'receipt_url' => $this->s3Service->getTemporaryUrl($s3Key), ]; } public function deposit(array $data): CashTransaction { $transaction = DB::transaction(fn () => $this->creditCash( amount: $data['amount'], description: $data['description'], )); if (! empty($data['receipt_key'])) { $this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null); } NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], title: 'Setoran Kas Toko', body: 'Setoran sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.', url: route('admin.finance.cash-accounts.index'), ); return $transaction; } public function withdrawal(array $data): CashTransaction { $transaction = DB::transaction(fn () => $this->debitCash( amount: $data['amount'], description: $data['description'], type: CashTransactionType::WITHDRAWAL, )); if (! empty($data['receipt_key'])) { $this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null); } NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], title: 'Penarikan Kas Toko', body: 'Penarikan sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.', url: route('admin.finance.cash-accounts.index'), ); return $transaction; } public function updateTransaction(CashTransaction $transaction, array $data): CashTransaction { $transaction = DB::transaction(function () use ($transaction, $data) { if (! in_array($transaction->type, [CashTransactionType::DEPOSIT, CashTransactionType::WITHDRAWAL])) { throw ValidationException::withMessages([ 'amount' => 'Transaksi ini tidak dapat diedit.', ]); } $cashAccount = CashAccount::firstOrFail(); $oldAmount = $transaction->amount; $newAmount = $data['amount']; $isDeposit = $transaction->type === CashTransactionType::DEPOSIT; $difference = $isDeposit ? $newAmount - $oldAmount : $oldAmount - $newAmount; if ($difference < 0 && $cashAccount->balance < abs($difference)) { throw ValidationException::withMessages([ 'amount' => 'Saldo tidak mencukupi.', ]); } $newBalance = $cashAccount->balance + $difference; $cashAccount->update(['balance' => $newBalance]); $transaction->update([ 'amount' => $newAmount, 'balance_after' => $newBalance, 'description' => $data['description'], ]); if (array_key_exists('receipt_key', $data)) { $currentMedia = $transaction->getFirstMedia('receipts'); $currentKey = $currentMedia?->file_name; if ($currentMedia && ! str_contains($currentKey, '/')) { $currentKey = $currentMedia->getPath(); } if ($data['receipt_key'] !== $currentKey) { $transaction->clearMediaCollection('receipts'); if (! empty($data['receipt_key'])) { $this->registerMedia($transaction, $data['receipt_key'], 'receipts', ['thumb' => true], $data['file_size'] ?? null, $data['file_mime_type'] ?? null); } } } return $transaction; }); NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], title: 'Transaksi Kas Diperbarui', body: 'Transaksi kas sebesar Rp '.number_format($transaction->amount, 0, ',', '.').' berhasil diperbarui'.' oleh '.auth()->user()->full_name.'.', url: route('admin.finance.cash-accounts.index'), ); return $transaction; } public function deleteTransaction(CashTransaction $transaction): bool { return DB::transaction(function () use ($transaction) { if (! in_array($transaction->type, [CashTransactionType::DEPOSIT, CashTransactionType::WITHDRAWAL])) { throw ValidationException::withMessages([ 'amount' => 'Transaksi ini tidak dapat dihapus.', ]); } $cashAccount = CashAccount::firstOrFail(); $isDeposit = $transaction->type === CashTransactionType::DEPOSIT; $newBalance = $isDeposit ? $cashAccount->balance - $transaction->amount : $cashAccount->balance + $transaction->amount; if ($newBalance < 0) { throw ValidationException::withMessages([ 'amount' => 'Saldo tidak mencukupi untuk menghapus transaksi ini.', ]); } $cashAccount->update(['balance' => $newBalance]); $transaction->clearMediaCollection('receipts'); $deleted = $transaction->delete(); if ($deleted) { NotificationService::notify( roles: ['Owner', 'Developer', 'Direktur', 'Admin Toko'], title: 'Transaksi Kas Dihapus', body: 'Transaksi kas sebesar Rp '.number_format($transaction->amount, 0, ',', '.').' berhasil dihapus'.' oleh '.auth()->user()->full_name.'.', url: route('admin.finance.cash-accounts.index'), ); } return $deleted; }); } }