first(); } public function getAllTransactions(): 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') ->latest() ->get() ->map(fn (CashTransaction $transaction) => $this->formatTransaction($transaction)); } 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 { return DB::transaction(function () use ($data) { $cashAccount = CashAccount::firstOrFail(); $newBalance = $cashAccount->balance + $data['amount']; $cashAccount->update(['balance' => $newBalance]); $transaction = CashTransaction::create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => auth()->id(), 'amount' => $data['amount'], 'balance_after' => $newBalance, 'type' => CashTransactionType::DEPOSIT, 'description' => $data['description'], ]); if (! empty($data['receipt_key'])) { $this->registerMedia($transaction, $data['receipt_key'], $data['file_size'] ?? null, $data['file_mime_type'] ?? null); } return $transaction; }); } public function withdrawal(array $data): CashTransaction { return DB::transaction(function () use ($data) { $cashAccount = CashAccount::firstOrFail(); if ($cashAccount->balance < $data['amount']) { throw ValidationException::withMessages([ 'amount' => 'Saldo tidak mencukupi.', ]); } $newBalance = $cashAccount->balance - $data['amount']; $cashAccount->update(['balance' => $newBalance]); $transaction = CashTransaction::create([ 'cash_account_id' => $cashAccount->id, 'created_by_id' => auth()->id(), 'amount' => $data['amount'], 'balance_after' => $newBalance, 'type' => CashTransactionType::WITHDRAWAL, 'description' => $data['description'], ]); if (! empty($data['receipt_key'])) { $this->registerMedia($transaction, $data['receipt_key'], $data['file_size'] ?? null, $data['file_mime_type'] ?? null); } return $transaction; }); } public function updateTransaction(CashTransaction $transaction, array $data): CashTransaction { return 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'], $data['file_size'] ?? null, $data['file_mime_type'] ?? null); } } } 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'); return $transaction->delete(); }); } private function registerMedia(CashTransaction $transaction, string $s3Key, ?int $fileSize = null, ?string $mimeType = null): void { $fileName = pathinfo($s3Key, PATHINFO_BASENAME); $name = pathinfo($s3Key, PATHINFO_FILENAME); Media::create([ 'model_type' => CashTransaction::class, 'model_id' => $transaction->id, 'uuid' => Str::uuid(), 'collection_name' => 'receipts', 'name' => $name, 'file_name' => $s3Key, 'mime_type' => $mimeType ?? 'image/jpeg', 'disk' => 's3', 'conversions_disk' => 's3', 'size' => $fileSize ?? 0, 'manipulations' => [], 'custom_properties' => [], 'generated_conversions' => ['thumb' => true], 'responsive_images' => [], 'order_column' => 1, ]); } }