mirror of
https://gitlab.com/CIEFWorldwideSdnBhd/exchange-2.0.git
synced 2026-08-19 12:33:56 +00:00
178 lines
6.3 KiB
PHP
178 lines
6.3 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\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\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
|
|
* @return array|null
|
|
*/
|
|
public function execute(array $files, ?int $bookingId = null): ?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']));
|
|
});
|
|
|
|
$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);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|