fetchesBooking = $fetchesBooking; $this->generatesTransactionBillNumber = $generatesTransactionBillNumber; $this->createPurchaseOrderTransactionProcessor = $createPurchaseOrderTransactionProcessor; $this->uploadsClaudeFiles = $uploadsClaudeFiles; $this->createsClaudeResponseWithFiles = $createsClaudeResponseWithFiles; } /** * @param UploadedFile[] $files * @param int|null $bookingId * @param bool $append * @return array|null */ public function execute(array $files, ?int $bookingId = null, bool $append = false): ?array { $tempPaths = []; try { foreach ($files as $index => $fileBase64) { if (preg_match('/^data:application\/pdf;base64,/', $fileBase64)) { $fileBase64 = preg_replace('/^data:application\/pdf;base64,/', '', $fileBase64); } $fileContent = base64_decode($fileBase64); if ($fileContent === false) { Log::warning("Invalid Base64 for file index {$index}"); continue; } $tempDir = storage_path('app/documents'); if (!file_exists($tempDir)) { mkdir($tempDir, 0755, true); } $filePath = $tempDir . '/' . uniqid("file_{$index}_") . '.pdf'; file_put_contents($filePath, $fileContent); $tempPaths[] = $filePath; } Log::info('Claude tempPaths', $tempPaths); // Upload PDFs to Claude $fileIds = $this->uploadsClaudeFiles->execute($tempPaths); Log::info('fileIds: '. json_encode($fileIds)); $kvp = KeyValuePair::where( 'key', KVPKey::CLAUDE_PROMPT_PREFIX . 'EXTRACT_1' )->first(); if (!$kvp) { throw new ResourceNotFoundException('AI Setup incomplete!'); } $userPrompt = $kvp->value; $response = $this->createsClaudeResponseWithFiles->execute($userPrompt, $fileIds); Log::info('response: '. json_encode($response)); $textContent = $response['content'][0]['text'] ?? '[]'; // Strip ```json fences if (preg_match('/```json\s*(.*?)\s*```/s', $textContent, $matches)) { $jsonString = $matches[1]; } else { $jsonString = $textContent; } $data = json_decode($jsonString, true); if (json_last_error() !== JSON_ERROR_NONE) { Log::info('Failed to decode Claude JSON response', [ 'error' => json_last_error_msg(), 'textContent' => $textContent, ]); $data = []; } // Normalize keys to snake_case $data = $this->convertKeysToSnakeCase($data); Log::info('data: '. json_encode($data)); if ($bookingId) { /** @var Booking $booking */ $booking = $this->fetchesBooking->execute(['id' => $bookingId]); $billNumber = $this->generatesTransactionBillNumber->execute('PO-'); $total = collect($data)->sum(function ($product) { return $product['quantity'] * floatval(str_replace(',', '', $product['unit_price'])); }); Log::info('total: '. (string) $total); $object = new TransactionObject( $billNumber, TransactionType::PURCHASE_ORDER, $booking->company->id, 1, 1, PaymentMethodType::CASH, $total, $total, $booking->fix_currency_id, $booking->fix_currency_id, 1, 0, 0, null, ApprovalStatus::PENDING_SUBMISSION, $data ); $this->createPurchaseOrderTransactionProcessor->execute($booking, $object, $append); } return $data; } catch (\Exception $e) { Log::error('Error processing PDF files with Claude', [ 'message' => $e->getMessage(), 'trace' => $e->getTraceAsString(), ]); throw $e; } finally { foreach ($tempPaths as $path) { if (file_exists($path)) { unlink($path); } } } } private function convertKeysToSnakeCase(array $items): array { return array_map(function ($item) { $newItem = []; foreach ($item as $key => $value) { $newKey = strtolower(str_replace(' ', '_', $key)); $newItem[$newKey] = $value; } return $newItem; }, $items); } }