select(['id', 'created_by_id', 'amount', 'description', 'created_at']) ->with('createdBy.userProfile', 'media') ->when($highlight, fn ($q) => $q->where('id', $highlight)) ->when($search, fn ($q) => $q->where('description', 'like', "%{$search}%")) ->orderBy($sort, $direction) ->paginate($perPage); $paginator->getCollection()->transform(fn (Expense $expense) => $this->formatExpense($expense)); return $paginator; } public function store(array $data): Expense { $expense = DB::transaction(function () use ($data) { $cashTransaction = $this->debitCash( amount: $data['amount'], description: $data['description'], ); $expense = Expense::create([ 'cash_transaction_id' => $cashTransaction->id, 'created_by_id' => auth()->id(), 'amount' => $data['amount'], 'description' => $data['description'], ]); if (array_key_exists('receipt_key', $data)) { $this->syncReceipt($expense, $data['receipt_key'] ?? null, 'photos', 'expense_receipt', $data['file_size'] ?? null, $data['file_mime_type'] ?? null); } return $expense; }); NotificationService::notify( roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], title: 'Pengeluaran Baru', body: 'Pengeluaran sebesar Rp '.number_format($data['amount'], 0, ',', '.').' berhasil dicatat'.' oleh '.auth()->user()->full_name.'.', url: route('admin.finance.expenses.index', ['highlight' => $expense->id]), ); return $expense; } public function update(Expense $expense, array $data): Expense { $expense = DB::transaction(function () use ($expense, $data) { $cashAccount = CashAccount::firstOrFail(); $cashTransaction = $expense->cashTransaction; $oldAmount = $expense->amount; $newAmount = $data['amount']; $difference = $newAmount - $oldAmount; if ($difference > 0 && $cashAccount->balance < $difference) { throw ValidationException::withMessages([ 'amount' => 'Saldo tidak mencukupi.', ]); } $newBalance = $cashAccount->balance - $difference; $cashAccount->update(['balance' => $newBalance]); $cashTransaction->update([ 'amount' => $newAmount, 'balance_after' => $newBalance, 'description' => $data['description'], ]); $expense->update([ 'amount' => $newAmount, 'description' => $data['description'], ]); if (array_key_exists('receipt_key', $data)) { $this->syncReceipt($expense, $data['receipt_key'] ?? null, 'photos', 'expense_receipt', $data['file_size'] ?? null, $data['file_mime_type'] ?? null); } return $expense; }); NotificationService::notify( roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], title: 'Pengeluaran Diperbarui', body: 'Pengeluaran sebesar Rp '.number_format($expense->amount, 0, ',', '.').' berhasil diperbarui'.' oleh '.auth()->user()->full_name.'.', url: route('admin.finance.expenses.index', ['highlight' => $expense->id]), ); return $expense; } public function destroy(Expense $expense): bool { return DB::transaction(function () use ($expense) { $this->creditCash( amount: $expense->amount, description: 'Pembatalan pengeluaran: '.$expense->description, type: CashTransactionType::DEPOSIT, ); $media = $expense->getFirstMedia('photos'); if ($media) { Cache::forget("expense_receipt_{$media->id}"); } $deleted = $expense->delete(); if ($deleted) { NotificationService::notify( roles: [Role::OWNER, Role::DEVELOPER, Role::DIREKTUR, Role::ADMIN_TOKO], title: 'Pengeluaran Dihapus', body: 'Pengeluaran sebesar Rp '.number_format($expense->amount, 0, ',', '.').' berhasil dihapus'.' oleh '.auth()->user()->full_name.'.', url: route('admin.finance.expenses.index'), ); } return $deleted; }); } private function formatExpense(Expense $expense): array { $media = $expense->getFirstMedia('photos'); if (! $media) { return $expense->toArray() + [ 'receipt_key' => null, 'receipt_url' => null, 'receipt_conversion_url' => null, ]; } $s3Key = $media->getCustomProperty('s3_key') ?? $media->getPath(); $cacheKey = "expense_receipt_{$media->id}"; $receiptUrl = Cache::remember($cacheKey, now()->addMinutes(55), function () use ($s3Key) { return $this->s3Service->getTemporaryUrl($s3Key); }); $conversionCacheKey = "expense_receipt_conversion_{$media->id}"; $receiptConversionUrl = Cache::remember($conversionCacheKey, now()->addMinutes(55), function () use ($media) { return $this->s3Service->getTemporaryUrl($media->getPath('thumb')); }); return $expense->toArray() + [ 'receipt_key' => $s3Key, 'receipt_url' => $receiptUrl, 'receipt_conversion_url' => $receiptConversionUrl, ]; } }