Files
exchange-2.0/app/Classes/Modules/Transactions/Processors/ETLPDFPurchaseOrderClaudeProcessor.php
T
2026-01-15 00:53:35 +08:00

189 lines
6.9 KiB
PHP

<?php
namespace App\Classes\Modules\Transactions\Processors;
use App\Classes\Modules\Anthropic\Services\UploadsClaudeFiles;
use App\Classes\Modules\Anthropic\Services\CreatesClaudeResponseWithFiles;
use App\Classes\Exceptions\ResourceNotFoundException;
use App\Classes\Jobs\Commands\V2\ETLPurchaseOrderTransactionCompleteV2CommandJob;
use App\Classes\Modules\Bookings\Services\FetchesBooking;
use App\Classes\Modules\Transactions\Services\GeneratesTransactionBillNumber;
use App\Classes\Modules\Transactions\Processors\CreatePurchaseOrderTransactionProcessor;
use App\Classes\Modules\Transactions\DataTransferObjects\TransactionObject;
use App\Classes\ValueObjects\Constants\KVPKey;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
use App\Classes\ValueObjects\Constants\PaymentMethodType;
use App\Classes\ValueObjects\Constants\TransactionType;
use App\Events\ETLPurchaseOrderTransactionCompleteEvent;
use App\Models\KeyValuePair;
use App\Models\Booking;
use Illuminate\Support\Facades\Log;
use Illuminate\Http\UploadedFile;
class ETLPDFPurchaseOrderClaudeProcessor
{
private FetchesBooking $fetchesBooking;
private GeneratesTransactionBillNumber $generatesTransactionBillNumber;
private CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor;
private UploadsClaudeFiles $uploadsClaudeFiles;
private CreatesClaudeResponseWithFiles $createsClaudeResponseWithFiles;
public function __construct(
FetchesBooking $fetchesBooking,
GeneratesTransactionBillNumber $generatesTransactionBillNumber,
CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor,
UploadsClaudeFiles $uploadsClaudeFiles,
CreatesClaudeResponseWithFiles $createsClaudeResponseWithFiles
) {
$this->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, bool $broadcast = 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);
$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);
$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::error('Failed to decode Claude JSON response', [
'error' => json_last_error_msg(),
'textContent' => $textContent,
]);
$data = [];
}
// Normalize keys to snake_case
$data = $this->convertKeysToSnakeCase($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);
Log::info('broadcast: '. json_encode($broadcast));
$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);
if($broadcast){
$baseDelay = 5;
$delaySeconds = ($index + 1) * $baseDelay;
ETLPurchaseOrderTransactionCompleteV2CommandJob::dispatch(true, $bookingId)->delay(now()->addSeconds($delaySeconds));
}
}
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);
}
}