diff --git a/app/Classes/Modules/OpenAI/Services/CreatesChatGPTResponseWithFiles.php b/app/Classes/Modules/OpenAI/Services/CreatesChatGPTResponseWithFiles.php
new file mode 100644
index 00000000..44b52990
--- /dev/null
+++ b/app/Classes/Modules/OpenAI/Services/CreatesChatGPTResponseWithFiles.php
@@ -0,0 +1,52 @@
+ 'input_file',
+ 'file_id' => $fileId,
+ ];
+ }
+
+ $inputs = array_merge([
+ [
+ 'type' => 'input_text',
+ 'text' => $userPrompt,
+ ]
+ ], $fileContents);
+
+ $response = Http::withHeaders([
+ 'Authorization' => 'Bearer ' . config('openai.api_key'),
+ 'Content-Type' => 'application/json',
+ ])->post(config('openai.base_url') . '/v1/responses', [
+ 'model' => 'gpt-4.1-mini',
+ 'input' => [
+ [
+ 'role' => 'user',
+ 'content' => $inputs,
+ ],
+ ],
+ ]);
+
+ return $response->json();
+ }
+ catch (\Illuminate\Http\Client\ConnectionException $exception) {
+ $error = 'Failed to connect to ChatGPT API';
+ throw new ConnectionErrorException($error, $exception->getMessage(), $userPrompt, $exception->getTraceAsString());
+ }
+ catch (\Exception $exception) {
+ throw new MalformedRequestException('Unable to get correct response from ChatGPT API: ' . $exception->getMessage());
+ }
+ }
+}
diff --git a/app/Classes/Modules/OpenAI/Services/UploadsOpenAIFiles.php b/app/Classes/Modules/OpenAI/Services/UploadsOpenAIFiles.php
new file mode 100644
index 00000000..edfdcc1e
--- /dev/null
+++ b/app/Classes/Modules/OpenAI/Services/UploadsOpenAIFiles.php
@@ -0,0 +1,49 @@
+ 'Bearer ' . config('openai.api_key'),
+ ])->attach(
+ 'file',
+ fopen($path, 'r'),
+ basename($path)
+ )->post(config('openai.base_url') . '/v1/files', [
+ 'purpose' => 'user_data',
+ ]);
+
+ $data = $response->json();
+
+ if ($response->successful() && isset($data['id'])) {
+ $ids[] = $data['id'];
+ } else {
+ Log::error('File upload failed', [
+ 'path' => $path,
+ 'response' => $data
+ ]);
+ }
+ }
+
+ return $ids;
+ }
+ catch (\Illuminate\Http\Client\ConnectionException $exception) {
+ $error = 'Failed to connect to ChatGPT API';
+ throw new ConnectionErrorException($error, $exception->getMessage(), $exception->getTraceAsString());
+ }
+ catch (\Exception $exception) {
+ throw new MalformedRequestException('Unable to get correct response from ChatGPT API: ' . $exception->getMessage());
+ }
+ }
+}
diff --git a/app/Classes/Modules/Transactions/ControllersLogic/ETLPurchaseOrderTransactionLogic.php b/app/Classes/Modules/Transactions/ControllersLogic/ETLPurchaseOrderTransactionLogic.php
index 3fd2d7b0..a43895cc 100644
--- a/app/Classes/Modules/Transactions/ControllersLogic/ETLPurchaseOrderTransactionLogic.php
+++ b/app/Classes/Modules/Transactions/ControllersLogic/ETLPurchaseOrderTransactionLogic.php
@@ -5,11 +5,12 @@ namespace App\Classes\Modules\Transactions\ControllersLogic;
use App\Classes\Exceptions\InternalServerErrorException;
use App\Classes\General\Abstracts\AbstractControllerLogic;
-use App\Classes\Modules\Transactions\Processors\ETLPurchaseOrderTransactionProcessor;
+use App\Classes\Modules\Transactions\Processors\ETLPDFPurchaseOrderTransactionProcessor;
use App\Classes\Modules\Transactions\Standards\Rules\CanETLPurchaseOrder;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Log;
class ETLPurchaseOrderTransactionLogic extends AbstractControllerLogic
{
@@ -28,18 +29,18 @@ class ETLPurchaseOrderTransactionLogic extends AbstractControllerLogic
/** @var CanETLPurchaseOrder */
private $canETLPurchaseOrder;
- /** @var ETLPurchaseOrderTransactionProcessor */
- private $etlPurchaseOrderTransactionProcessor;
+ /** @var ETLPDFPurchaseOrderTransactionProcessor */
+ private $etlPDFPurchaseOrderTransactionProcessor;
/**
* ETLPurchaseOrderTransactionLogic constructor.
* @param CanETLPurchaseOrder $canETLPurchaseOrder
- * @param ETLPurchaseOrderTransactionProcessor $etlPurchaseOrderTransactionProcessor
+ * @param ETLPDFPurchaseOrderTransactionProcessor $etlPDFPurchaseOrderTransactionProcessor
*/
- public function __construct(CanETLPurchaseOrder $canETLPurchaseOrder, ETLPurchaseOrderTransactionProcessor $etlPurchaseOrderTransactionProcessor)
+ public function __construct(CanETLPurchaseOrder $canETLPurchaseOrder, ETLPDFPurchaseOrderTransactionProcessor $etlPDFPurchaseOrderTransactionProcessor)
{
$this->canETLPurchaseOrder = $canETLPurchaseOrder;
- $this->etlPurchaseOrderTransactionProcessor = $etlPurchaseOrderTransactionProcessor;
+ $this->etlPDFPurchaseOrderTransactionProcessor = $etlPDFPurchaseOrderTransactionProcessor;
}
/**
@@ -53,17 +54,19 @@ class ETLPurchaseOrderTransactionLogic extends AbstractControllerLogic
{
$this->canETLPurchaseOrder->passes();
- if ($request->has('products') && is_array($request->input('products'))) {
- $products = $request->input('products');
- $productsMetadata = $request->input('productsMetadata');
- }
+ // if ($request->has('products') && is_array($request->input('products'))) {
+ // $products = $request->input('products');
+ // $productsMetadata = $request->input('productsMetadata');
+ // }
- try{
- $products = $this->etlPurchaseOrderTransactionProcessor->execute($products, $productsMetadata);
- } catch (\Exception $exception) {
- throw new InternalServerErrorException('Something went wrong!');
- }
+ // try{
+ // $products = $this->etlPDFPurchaseOrderTransactionProcessor->execute($products, $productsMetadata);
+ // } catch (\Exception $exception) {
+ // throw new InternalServerErrorException('Something went wrong!');
+ // }
- return $this->response($products);
+ $files = $request->input('files');
+ $result = $this->etlPDFPurchaseOrderTransactionProcessor->execute($files);
+ return $this->response($result);
}
}
diff --git a/app/Classes/Modules/Transactions/ControllersLogic/ImportPurchaseOrderTransactionLogic.php b/app/Classes/Modules/Transactions/ControllersLogic/ImportPurchaseOrderTransactionLogic.php
index d1c855d8..15796728 100644
--- a/app/Classes/Modules/Transactions/ControllersLogic/ImportPurchaseOrderTransactionLogic.php
+++ b/app/Classes/Modules/Transactions/ControllersLogic/ImportPurchaseOrderTransactionLogic.php
@@ -9,7 +9,7 @@ use App\Classes\Modules\Bookings\Services\FetchesBooking;
use App\Classes\Modules\Documents\DataTransferObjects\DocumentObject;
use App\Classes\Modules\Transactions\DataTransferObjects\TransactionObject;
use App\Classes\Modules\Transactions\Processors\CreatePurchaseOrderTransactionProcessor;
-use App\Classes\Modules\Transactions\Processors\ETLPurchaseOrderTransactionProcessor;
+use App\Classes\Modules\Transactions\Processors\ETLPDFPurchaseOrderTransactionProcessor;
use App\Classes\Modules\Transactions\Services\GeneratesTransactionBillNumber;
use App\Classes\Modules\Transactions\Standards\Rules\CanETLPurchaseOrder;
use App\Classes\ValueObjects\Constants\ApprovalStatus;
@@ -47,8 +47,8 @@ class ImportPurchaseOrderTransactionLogic extends AbstractControllerLogic
/** @var CreatePurchaseOrderTransactionProcessor */
private $createPurchaseOrderTransactionProcessor;
- /** @var ETLPurchaseOrderTransactionProcessor */
- private $etlPurchaseOrderTransactionProcessor;
+ /** @var ETLPDFPurchaseOrderTransactionProcessor */
+ private $etlPDFPurchaseOrderTransactionProcessor;
/** @var CanETLPurchaseOrder */
private $canETLPurchaseOrder;
@@ -58,15 +58,15 @@ class ImportPurchaseOrderTransactionLogic extends AbstractControllerLogic
* @param FetchesBooking $fetchesBooking
* @param GeneratesTransactionBillNumber $generatesTransactionBillNumber
* @param CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor
- * @param ETLPurchaseOrderTransactionProcessor $etlPurchaseOrderTransactionProcessor
+ * @param ETLPDFPurchaseOrderTransactionProcessor $etlPDFPurchaseOrderTransactionProcessor
* @param CanETLPurchaseOrder $canETLPurchaseOrder
*/
- public function __construct(FetchesBooking $fetchesBooking, GeneratesTransactionBillNumber $generatesTransactionBillNumber, CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor, ETLPurchaseOrderTransactionProcessor $etlPurchaseOrderTransactionProcessor, CanETLPurchaseOrder $canETLPurchaseOrder)
+ public function __construct(FetchesBooking $fetchesBooking, GeneratesTransactionBillNumber $generatesTransactionBillNumber, CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor, ETLPDFPurchaseOrderTransactionProcessor $etlPDFPurchaseOrderTransactionProcessor, CanETLPurchaseOrder $canETLPurchaseOrder)
{
$this->fetchesBooking = $fetchesBooking;
$this->generatesTransactionBillNumber = $generatesTransactionBillNumber;
$this->createPurchaseOrderTransactionProcessor = $createPurchaseOrderTransactionProcessor;
- $this->etlPurchaseOrderTransactionProcessor = $etlPurchaseOrderTransactionProcessor;
+ $this->etlPDFPurchaseOrderTransactionProcessor = $etlPDFPurchaseOrderTransactionProcessor;
$this->canETLPurchaseOrder = $canETLPurchaseOrder;
}
@@ -83,24 +83,13 @@ class ImportPurchaseOrderTransactionLogic extends AbstractControllerLogic
$this->canETLPurchaseOrder->passes();
$products = $request->input('products');
- $productsMetadata = $request->input('productsMetadata');
- $count = collect($products)
- ->filter(function ($product) {
- return isset($product['description']) &&
- preg_match('/\p{Han}+/u', $product['description']);
- })
- ->count();
-
- if($count > 10){
- ETLPurchaseOrderTransactionV2CommandJob::dispatch($products, $productsMetadata, $request->route('id'));
- $this->notificationMessage = "Please refresh page in a few minutes";
- return $this->response([]);
+ if(empty($products)){
+ $files = $request->input('files');
+ $products = $this->etlPDFPurchaseOrderTransactionProcessor->execute($files);
}
- else{
- $products = $this->etlPurchaseOrderTransactionProcessor->execute($products, $productsMetadata);
- }
- } else {
+ }
+ else {
$object = new DocumentObject('', $request->input('files'), '', ApprovalStatus::APPROVED, 'imports');
foreach ($object->getFiles() as $file) {
$collection = Excel::toCollection(null, json_decode($file)->file_info->original->file, null, null, true);
diff --git a/app/Classes/Modules/Transactions/Processors/ETLPDFPurchaseOrderTransactionProcessor.php b/app/Classes/Modules/Transactions/Processors/ETLPDFPurchaseOrderTransactionProcessor.php
new file mode 100644
index 00000000..db9b91f5
--- /dev/null
+++ b/app/Classes/Modules/Transactions/Processors/ETLPDFPurchaseOrderTransactionProcessor.php
@@ -0,0 +1,198 @@
+fetchesBooking = $fetchesBooking;
+ $this->generatesTransactionBillNumber = $generatesTransactionBillNumber;
+ $this->createPurchaseOrderTransactionProcessor = $createPurchaseOrderTransactionProcessor;
+ $this->uploadsOpenAIFiles = $uploadsOpenAIFiles;
+ $this->createsChatGPTResponseWithFiles = $createsChatGPTResponseWithFiles;
+ }
+
+
+ /**
+ * @param UploadedFile[] $files
+ * @param int $bookingId
+ * @return array|null
+ */
+ public function execute(array $files, ?int $bookingId = null): ?array
+ {
+ // Ensure temp directory exists
+ $tempDir = storage_path('app/documents');
+ if (!file_exists($tempDir)) {
+ mkdir($tempDir, 0755, true);
+ }
+
+ $tempPaths = [];
+
+ try {
+ foreach ($files as $index => $fileBase64) {
+ // Strip prefix if present
+ if (preg_match('/^data:application\/pdf;base64,/', $fileBase64)) {
+ $fileBase64 = preg_replace('/^data:application\/pdf;base64,/', '', $fileBase64);
+ }
+
+ // Decode Base64
+ $fileContent = base64_decode($fileBase64);
+ if ($fileContent === false) {
+ Log::warning("Invalid Base64 for file index $index");
+ continue;
+ }
+
+ // Ensure temp directory exists
+ $tempDir = storage_path('app/documents');
+ if (!file_exists($tempDir)) {
+ mkdir($tempDir, 0755, true);
+ }
+
+ // Generate unique temp file path
+ $fileName = uniqid("file_{$index}_") . '.pdf';
+ $filePath = $tempDir . '/' . $fileName;
+
+ // Save file to disk
+ file_put_contents($filePath, $fileContent);
+
+ // Track temp path
+ $tempPaths[] = $filePath;
+ }
+
+ Log::info('tempPaths: ' . json_encode($tempPaths));
+
+ // Upload to OpenAI
+ $fileIds = $this->uploadsOpenAIFiles->execute($tempPaths);
+
+ $kvp = KeyValuePair::where(
+ 'key',
+ KVPKey::CHATGPT_PROMPT_PREFIX . 'EXTRACT_1'
+ )->first();
+
+ if (! $kvp) {
+ throw new ResourceNotFoundException('AI Setup incomplete!');
+ }
+
+ $userPrompt = $kvp->value;
+ $response = $this->createsChatGPTResponseWithFiles->execute($userPrompt, $fileIds);
+
+ // Extract the text content from the first output message
+ $textContent = $response['output'][0]['content'][0]['text'] ?? '[]';
+
+ // Remove ```json fences if present
+ if (preg_match('/```json\s*(.*?)\s*```/s', $textContent, $matches)) {
+ $jsonString = $matches[1];
+ } else {
+ $jsonString = $textContent;
+ }
+
+ // Decode JSON
+ $data = json_decode($jsonString, true);
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ Log::error('Failed to decode OpenAI JSON response', [
+ 'error' => json_last_error_msg(),
+ 'textContent' => $textContent
+ ]);
+ $data = [];
+ }
+
+ // Optional: convert keys to snake_case
+ $data = array_map(function($item) {
+ $newItem = [];
+ foreach ($item as $key => $value) {
+ $newKey = strtolower(str_replace(' ', '_', $key));
+ $newItem[$newKey] = $value;
+ }
+ return $newItem;
+ }, $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);
+
+
+ $transaction = $this->createPurchaseOrderTransactionProcessor->execute($booking, $object);
+ }
+ return $data;
+
+ } catch (\Exception $e) {
+ Log::error('Error processing PDF files with ChatGPT', [
+ 'message' => $e->getMessage(),
+ 'trace' => $e->getTraceAsString(),
+ ]);
+ throw $e;
+ } finally {
+ // Clean up
+ foreach ($tempPaths as $path) {
+ if (file_exists($path)) {
+ unlink($path);
+ }
+ }
+ }
+ }
+
+ 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);
+ }
+}
diff --git a/resources/assets/vue/components/bookings/forms/PdfUpload1688Component.vue b/resources/assets/vue/components/bookings/forms/PdfUpload1688Component.vue
new file mode 100644
index 00000000..a0bbeab3
--- /dev/null
+++ b/resources/assets/vue/components/bookings/forms/PdfUpload1688Component.vue
@@ -0,0 +1,138 @@
+
+