'Update Purchase Order', 'message' => $this->notificationMessage ?? "You have successfully updated your booking's purchase order", ]; } private ?string $notificationMessage = null; /** @var FetchesBooking */ private $fetchesBooking; /** @var GeneratesTransactionBillNumber */ private $generatesTransactionBillNumber; /** @var CreatePurchaseOrderTransactionProcessor */ private $createPurchaseOrderTransactionProcessor; /** @var ETLPDFPurchaseOrderClaudeProcessor */ private $eTLPDFPurchaseOrderClaudeProcessor; /** @var CanETLPurchaseOrder */ private $canETLPurchaseOrder; /** @var CreatesDocument */ private $createsDocument; /** @var DeletesDocument */ private $deletesDocument; /** @var CreatesFiles */ private $createsFile; /** * ImportPurchaseOrderTransactionLogic constructor. * @param FetchesBooking $fetchesBooking * @param GeneratesTransactionBillNumber $generatesTransactionBillNumber * @param CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor * @param ETLPDFPurchaseOrderClaudeProcessor $eTLPDFPurchaseOrderClaudeProcessor * @param CanETLPurchaseOrder $canETLPurchaseOrder * @param CreatesDocument $createsDocument * @param DeletesDocument $deletesDocument * @param CreatesFiles $createsFile */ public function __construct(FetchesBooking $fetchesBooking, GeneratesTransactionBillNumber $generatesTransactionBillNumber, CreatePurchaseOrderTransactionProcessor $createPurchaseOrderTransactionProcessor, ETLPDFPurchaseOrderClaudeProcessor $eTLPDFPurchaseOrderClaudeProcessor, CanETLPurchaseOrder $canETLPurchaseOrder, CreatesDocument $createsDocument, DeletesDocument $deletesDocument, CreatesFiles $createsFile) { $this->fetchesBooking = $fetchesBooking; $this->generatesTransactionBillNumber = $generatesTransactionBillNumber; $this->createPurchaseOrderTransactionProcessor = $createPurchaseOrderTransactionProcessor; $this->eTLPDFPurchaseOrderClaudeProcessor = $eTLPDFPurchaseOrderClaudeProcessor; $this->canETLPurchaseOrder = $canETLPurchaseOrder; $this->createsDocument = $createsDocument; $this->deletesDocument = $deletesDocument; $this->createsFile = $createsFile; } /** * @param Request $request * @param string $id * @return JsonResponse * @throws \App\Classes\Exceptions\MalformedRequestException */ public function logic(Request $request, $id = ''): JsonResponse { $products = []; /** @var Booking $booking */ $booking = $this->fetchesBooking->execute(['id' => $request->route('id') ?? $id]); if ($request->has('products') && is_array($request->input('products'))) { $this->canETLPurchaseOrder->passes(); $products = $request->input('products'); if(empty($products)){ $files = $request->input('files', []); $maxSize = 150 * 1024; // 150 KB $totalSize = 0; $base64FilesEN = []; // files with "EN" in the name $base64FilesOther = []; // files without "EN" foreach ($files as $index => $file) { if (!isset($file['base64'])) { Log::info("Missing base64 for file index {$index}"); continue; } $fileBase64 = $file['base64']; $fileName = $file['name'] ?? "file_{$index}"; if (stripos($fileName, 'EN') !== false) { $base64FilesEN[] = $fileBase64; if (preg_match('/^data:application\/pdf;base64,/', $fileBase64)) { $cleanBase64 = preg_replace('/^data:application\/pdf;base64,/', '', $fileBase64); } else{ $cleanBase64 = $fileBase64; } $fileContent = base64_decode($cleanBase64, true); if ($fileContent === false) { Log::info("Invalid Base64 for file index {$index}"); } $totalSize += strlen($fileContent); } else { $base64FilesOther[] = $fileBase64; } } //Keep a copy of all PDF files uploaded include pdf file with 'EN' as part of its filename $allBase64Files = array_merge($base64FilesEN, $base64FilesOther); $deleteDocument = $booking->documents()->where('document_type', DocumentType::ECOMMERCE_PURCHASE_ORDER)->latest()->first(); if($deleteDocument){ $this->deletesDocument->execute($deleteDocument); } $object = new DocumentObject(DocumentType::ECOMMERCE_PURCHASE_ORDER, $allBase64Files, '', ApprovalStatus::APPROVED, '1688_purchase_orders'); /** @var Document $document */ $document = $this->createsDocument->execute($booking, $object); $this->createsFile->execute($document, $object); //ETL only applicable for pdf file with 'EN' as part of its filename if ($totalSize > 0) { if ($totalSize > $maxSize) { // ETLPurchaseOrderTransactionV2CommandJob::dispatch($base64FilesEN, $request->route('id')); $this->runAsJob($base64FilesEN, $request->route('id')); $this->notificationMessage = "Please don’t refresh the page. The page will automatically update when the process is done."; return $this->response([]); } else{ $products = $this->eTLPDFPurchaseOrderClaudeProcessor->execute($base64FilesEN); } } else{ return $this->response([]); } } } 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); $sheet = $collection->first()->skip(1); $products = $sheet->map(function ($row) { Log::info($row); $stockCode = $row[0]; $description = $row[1]; $quantity = $row[2]; $unit_price = $row[3]; return [ 'stockCode' => $stockCode, 'description' => $description, 'quantity' => $quantity, 'unit_price' => $unit_price ]; })->all(); } } $billNumber = $this->generatesTransactionBillNumber->execute('PO-'); $total = collect($products)->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, $products); $transaction = $this->createPurchaseOrderTransactionProcessor->execute($booking, $object); return $this->resourceResponse(new TransactionResource($transaction)); } private function runAsJob($files, $bookingId){ $maxFilesPerBatch = 4; $maxBytesPerBatch = 350 * 1024; // 350 KB $currentBatch = []; $currentBatchSize = 0; $fileBatches = []; foreach ($files as $index => $fileBase64) { if (preg_match('/^data:application\/pdf;base64,/', $fileBase64)) { $cleanBase64 = preg_replace('/^data:application\/pdf;base64,/', '', $fileBase64); } else{ $cleanBase64 = $fileBase64; } $fileContent = base64_decode($cleanBase64, true); if ($fileContent === false) { Log::info("Invalid Base64 for file index {$index}"); continue; // skip invalid files } $fileSize = strlen($fileContent); // size in bytes // If adding this file would exceed limits, start a new batch $wouldExceedFileCount = count($currentBatch) >= $maxFilesPerBatch; $wouldExceedTotalSize = ($currentBatchSize + $fileSize) > $maxBytesPerBatch; if ($wouldExceedFileCount || $wouldExceedTotalSize) { // save current batch $fileBatches[] = $currentBatch; // reset $currentBatch = []; $currentBatchSize = 0; } // Add file to batch $currentBatch[] = $fileBase64; $currentBatchSize += $fileSize; } // push final remaining batch if (!empty($currentBatch)) { $fileBatches[] = $currentBatch; } $totalBatches = count($fileBatches); $groupId = (string) Str::uuid(); foreach ($fileBatches as $index => $batch) { $isLast = ($index === $totalBatches - 1); // Calculate total size of this batch $batchSize = 0; foreach ($batch as $fileBase64) { if (preg_match('/^data:application\/pdf;base64,/', $fileBase64)) { $cleanBase64 = preg_replace('/^data:application\/pdf;base64,/', '', $fileBase64); } else{ $cleanBase64 = $fileBase64; } $fileContent = base64_decode($cleanBase64, true); if ($fileContent !== false) { $batchSize += strlen($fileContent); } } // Log batch info before dispatching job Log::info("Dispatching Job for Batch #" . ($index + 1), [ 'group_id' => $groupId, 'batch_index' => $index + 1, 'total_batches' => $totalBatches, 'is_last' => $isLast, 'files_in_batch' => count($batch), 'batch_size_bytes' => $batchSize, ]); ETLPurchaseOrderTransactionV2CommandJob::dispatch( $batch, $bookingId, $groupId, $index + 1, $totalBatches, $isLast )->delay(now()->addSeconds($index * 5)); } } }