1688 PO Automation Project, feedback from Brian

This commit is contained in:
Dillon Ngo
2026-01-13 14:08:11 +08:00
parent 52ec3e9535
commit a0e7781b06
8 changed files with 159 additions and 17 deletions
@@ -21,15 +21,43 @@ class ETLPurchaseOrderTransactionV2CommandJob implements ShouldQueue
/** @var int|null */
private $bookingId;
/** @var string */
private $groupId;
/** @var int */
private $batchIndex;
/** @var int */
private $totalBatches;
/** @var bool */
private $isLast;
/**
* ETLPurchaseOrderTransactionV2CommandJob constructor.
* constructor.
*
* @param array $files
* @param int $bookingId
* @param int|null $bookingId
* @param string $groupId
* @param int $batchIndex
* @param int $totalBatches
* @param bool $isLast
*/
public function __construct(array $files, ?int $bookingId)
{
public function __construct(
array $files,
?int $bookingId,
string $groupId,
int $batchIndex,
int $totalBatches,
bool $isLast = false
) {
$this->files = $files;
$this->bookingId = $bookingId;
$this->groupId = $groupId;
$this->batchIndex = $batchIndex;
$this->totalBatches = $totalBatches;
$this->isLast = $isLast;
}
public function handle()
@@ -37,7 +65,15 @@ class ETLPurchaseOrderTransactionV2CommandJob implements ShouldQueue
Log::info(Carbon::now() . ': Start job - ETL Purchase Order Transaction.');
$start = new Carbon();
(App()->make(ETLPDFPurchaseOrderClaudeProcessor::class))->execute($this->files, $this->bookingId);
Log::info(Carbon::now() . ': Processing - ETL Purchase Order Transaction.', [
'group_id' => $this->groupId,
'batch_index' => $this->batchIndex,
'total_batches' => $this->totalBatches,
'is_last' => $this->isLast,
'file_count' => count($this->files),
]);
(App()->make(ETLPDFPurchaseOrderClaudeProcessor::class))->execute($this->files, $this->bookingId, $this->batchIndex !== 1);
$end = new Carbon();
$elapsedTime = $start->diff($end)->format('%H:%I:%S');
@@ -19,7 +19,7 @@ class CreatesClaudeResponse
public function execute(string $userPrompt, string $model = "") {
try {
// Default to the latest recommended Claude model
$modelId = $model ?? config('anthropic.default_model', 'claude-sonnet-4-5-20250929');
$modelId = $model ?? config('anthropic.default_model', 'claude-haiku-4-5-20251001');
$data = [
'model' => $modelId,
@@ -37,7 +37,7 @@ class CreatesClaudeResponseWithFiles
'anthropic-beta' => 'files-api-2025-04-14',
'Content-Type' => 'application/json',
])->post(config('anthropic.base_url') . '/v1/messages', [
'model' => config('anthropic.default_model', 'claude-sonnet-4-5-20250929'),
'model' => config('anthropic.default_model', 'claude-haiku-4-5-20251001'),
'max_tokens' => 16384,
'messages' => [
[
@@ -25,6 +25,7 @@ use App\Models\Document;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Maatwebsite\Excel\Facades\Excel;
class ImportPurchaseOrderTransactionLogic extends AbstractControllerLogic
@@ -98,7 +99,7 @@ class ImportPurchaseOrderTransactionLogic extends AbstractControllerLogic
*/
public function logic(Request $request, $id = ''): JsonResponse
{
$products = []; // Initialize
$products = [];
/** @var Booking $booking */
$booking = $this->fetchesBooking->execute(['id' => $request->route('id') ?? $id]);
@@ -163,7 +164,8 @@ class ImportPurchaseOrderTransactionLogic extends AbstractControllerLogic
//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'));
// ETLPurchaseOrderTransactionV2CommandJob::dispatch($base64FilesEN, $request->route('id'));
$this->runAsJob($base64FilesEN, $request->route('id'));
$this->notificationMessage = "Please refresh page in a few minutes";
return $this->response([]);
}
@@ -218,4 +220,93 @@ class ImportPurchaseOrderTransactionLogic extends AbstractControllerLogic
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
);
}
}
}
@@ -53,10 +53,11 @@ class CreatePurchaseOrderTransactionProcessor
/**
* @param Booking $booking
* @param TransactionObject $object
* @param bool $append
* @return Transaction|\Illuminate\Database\Eloquent\Model
* @throws \App\Classes\Exceptions\MalformedRequestException
*/
public function execute(Booking $booking, TransactionObject $object){
public function execute(Booking $booking, TransactionObject $object, bool $append = false){
/** @var Transaction $transaction */
$transaction = $booking->transactions()->where('type', TransactionType::PURCHASE_ORDER)->first();
@@ -64,7 +65,9 @@ class CreatePurchaseOrderTransactionProcessor
$this->updatesTransactionStatus->execute($transaction, (float) number_format($object->getAmount(), 2, '.', '') === (float) number_format((float)$booking->fix_amount, 2, '.', '') ? ApprovalStatus::PENDING_VERIFICATION : ApprovalStatus::PENDING_SUBMISSION);
$this->deletesTransactionDetails->execute($transaction);
if(!$append){
$this->deletesTransactionDetails->execute($transaction);
}
foreach ($object->getDetails() as $product){
$this->createsTransactionDetail->execute($transaction, $product);
@@ -73,4 +76,4 @@ class CreatePurchaseOrderTransactionProcessor
return $transaction;
}
}
}
@@ -44,9 +44,10 @@ class ETLPDFPurchaseOrderClaudeProcessor
/**
* @param UploadedFile[] $files
* @param int|null $bookingId
* @param bool $append
* @return array|null
*/
public function execute(array $files, ?int $bookingId = null): ?array
public function execute(array $files, ?int $bookingId = null, bool $append = false): ?array
{
$tempPaths = [];
@@ -142,8 +143,7 @@ class ETLPDFPurchaseOrderClaudeProcessor
$data
);
$this->createPurchaseOrderTransactionProcessor
->execute($booking, $object);
$this->createPurchaseOrderTransactionProcessor->execute($booking, $object, $append);
}
return $data;
+2
View File
@@ -6,6 +6,8 @@ server {
access_log /var/log/nginx/access.log;
root /var/www/html/public;
client_max_body_size 10M;
server_name localhost;
location / {
@@ -13,7 +13,17 @@
</file-input-component>
</div>
</div>
<div class="row m-t-20 justify-content-center" v-if="parameters.files.length > 0">
<div class="row">
<div class="col">
<button type="button"
class="btn btn-sm btn-block p-t-10 p-b-10 p-r-35 p-l-35 btn-success b-rad-none"
@click="importToSystem"
:disabled="isExporting || isImporting">
{{ isImporting ? 'Uploading...' : 'Upload' }}
</button>
</div>
</div>
<!-- <div class="row m-t-20 justify-content-center" v-if="parameters.files.length > 0">
<div class="col-auto">
<button type="button"
class="btn btn-sm btn-outline-complete rounded-0 mx-1"
@@ -31,7 +41,7 @@
{{ isImporting ? 'Uploading...' : 'Continue Upload' }}
</button>
</div>
</div>
</div> -->
</div>
</div>
</div>