Merge branch 'dillon/83-yd-order-tracking-query-ui' into vapor/development

This commit is contained in:
Dillon Ngo
2025-03-22 11:40:42 +08:00
19 changed files with 551 additions and 22 deletions
@@ -0,0 +1,20 @@
<?php
namespace App\Classes\General\Eloquent\Filters;
use Illuminate\Database\Eloquent\Builder;
class OrderTrackingId implements Filter
{
/**
* @param Builder $builder
* @param $value
* @return Builder|mixed
*/
public static function apply(Builder $builder, $value)
{
return $builder->where('order_tracking_id', $value);
}
}
@@ -0,0 +1,23 @@
<?php
namespace App\Classes\General\Eloquent\Filters;
use Illuminate\Database\Eloquent\Builder;
class TrackingNo implements Filter
{
/**
* @param Builder $builder
* @param $value
* @return Builder|mixed
*/
public static function apply(Builder $builder, $value)
{
return $builder->where('tracking_no', $value)
->where(function ($query) {
$query->whereNotNull('tracking_english')
->orWhereNotNull('remark_english');
});
}
}
@@ -11,7 +11,7 @@ use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class FetchByTrakingNoYdPortalV2CommandJob implements ShouldQueue
class FetchTrakingInfoYDByPackingListV2CommandJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
@@ -24,7 +24,7 @@ class FetchByTrakingNoYdPortalV2CommandJob implements ShouldQueue
public function handle()
{
Log::info('Processing FetchByTrakingNoYdPortalV2CommandJob');
Log::info('Processing FetchTrakingInfoYDByPackingListV2CommandJob');
$processor = app(FetchByTrakingNoYdPortalV2Processor::class);
$processor->execute($this->packingList);
}
@@ -0,0 +1,30 @@
<?php
namespace App\Classes\Jobs\Commands\V2\YD;
use App\Classes\Modules\PackingLists\Processors\V2\FetchByTrakingNoYdPortalV2Processor;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class FetchTrakingInfoYDByTrackingNoV2CommandJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $trackingNo;
public function __construct(string $trackingNo)
{
$this->trackingNo = $trackingNo;
}
public function handle()
{
Log::info('Processing FetchTrakingInfoYDByTrackingNoV2CommandJob');
$processor = app(FetchByTrakingNoYdPortalV2Processor::class);
$processor->processTrackingNo($this->trackingNo);
}
}
@@ -0,0 +1,52 @@
<?php
namespace App\Classes\Modules\Orders\ControllersLogic;
use App\Classes\General\Abstracts\AbstractControllerLogic;
use App\Classes\Modules\Orders\Services\ListsOrderTracking;
use App\Http\Resources\OrderTrackingResource;
use ErrorException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
class ListOrderTrackingLogic extends AbstractControllerLogic
{
/**
* @return array
*/
protected function notification():array {
return [
'title' => 'Retrieved Order Tracking',
'message' => 'You have successfully retrieved order tracking info'
];
}
/** @var ListsOrderTracking */
private $listsOrderTracking;
/**
* ListOrderTrackingLogic constructor.
* @param ListsOrderTracking $listsOrderTracking
*/
public function __construct(ListsOrderTracking $listsOrderTracking)
{
$this->listsOrderTracking = $listsOrderTracking;
}
public function logic(Request $request) : JsonResponse
{
$query = $this->listsOrderTracking->execute($this->listsOrderTracking->deserializeFilters($request->input('filters')));
$filters = json_decode($request->input('filters'), true);
$trackingNo = $filters['tracking_no'];
if ($trackingNo) {
Artisan::call('process-yd-by-traking-no-data-command', [
'trackingNo' => $trackingNo
]);
}
return $this->collectionResponse(OrderTrackingResource::collection($query));
}
}
@@ -0,0 +1,31 @@
<?php
namespace App\Classes\Modules\Orders\Services;
use Illuminate\Database\Eloquent\Builder;
use App\Classes\General\Eloquent\AbstractListRecord;
use App\Models\YDOrderTracking;
class ListsOrderTracking extends AbstractListRecord
{
/** @var YDOrderTracking */
private $repository;
/**
* ListsOrderTracking constructor.
* @param YDOrderTracking $repository
*/
public function __construct(YDOrderTracking $repository)
{
$this->repository = $repository;
}
/**
* @return Builder
*/
public function getRepository(): Builder
{
return $this->repository->newQuery();
}
}
@@ -40,8 +40,6 @@ class FetchByTrakingNoYdPortalV2Processor
Log::info('FetchByTrakingNoYdPortalV2Processor starts with packingList id ' . $packingList->id);
// return;
$time_start = microtime(true);
$reference = $packingList->reference;
Log::info('reference ' . $reference);
@@ -56,15 +54,29 @@ class FetchByTrakingNoYdPortalV2Processor
return;
}
$this->processTrackingNo($reference);
Log::info('FetchByTrakingNoYdPortalV2Processor ends with packingList id ' . $packingList->id);
Log::info('---');
}
/**
* @param string $reference
* @return void
* @throws MalformedRequestException
*/
public function processTrackingNo(string $reference){
$time_start = microtime(true);
$check2 = YDOrderTracking::where('tracking_no', $reference)
->where('tracking', '第三方提货')
->first();
if ($check2) {
Log::info('FetchByTrakingNoYdPortalV2Processor SKIPPED: 第三方提货');
Log::info('---');
return;
}
// if ($check2) {
// Log::info('FetchByTrakingNoYdPortalV2Processor SKIPPED: 第三方提货');
// Log::info('---');
// return;
// }
$trackingRequest = $this->fetchesDataFRomYDPortal->clientRequest('http://www.yd-wl.com/api/ApiTracking.ashx', 'GET', [
'trakingno' => $reference
@@ -78,11 +90,10 @@ class FetchByTrakingNoYdPortalV2Processor
$execution_time = ($time_end - $time_start)/60;
Log::info('FetchByTrakingNoYdPortalV2Processor ends with packingList id ' . $packingList->id . ' => Total Execution Time: '.$execution_time.' Mins.');
Log::info('---');
Log::info('FetchByTrakingNoYdPortalV2Processor ends with reference ' . $reference . ' => Total Execution Time: '.$execution_time.' Mins.');
}
public function processData($jsonData, $reference)
private function processData($jsonData, $reference)
{
$data = json_decode($jsonData, true);
@@ -93,23 +104,65 @@ class FetchByTrakingNoYdPortalV2Processor
->where('tracking_no', $reference)
->first();
// Log::info('tracking: '.$item['tracking']);
// Log::info('remark: '.$item['remark']);
if (!$existingRecord) {
$etdEta = $this->parseEtdEta($item['remark'], $item['tracking'], $item['trackingtime']);
$isTrackingTranslatedPreviouly = $this->checkIfTranslationAlreadyExist(!empty($item['tracking']) ? $item['tracking'] : "");
$isRemarkTranslatedPreviouly = $this->checkIfTranslationAlreadyExist(!empty($item['remark']) ? $item['remark'] : "");
YDOrderTracking::create([
'order_tracking_id' => $item['Id'],
'tracking_no' => $reference,
'package_id' => $item['package_id'],
'tracking' => $item['tracking'] ?? null,
'tracking_english' => $isTrackingTranslatedPreviouly ? $isTrackingTranslatedPreviouly : $this->translateTracking($item['tracking']),
'tracking_time' => $item['trackingtime'] ?? null,
'add_time' => $item['addtime'] ?? null,
'remark' => $item['remark'] ?? null,
'remark_english' => $item['remark'] ? ($isRemarkTranslatedPreviouly ? $isRemarkTranslatedPreviouly : $this->translateTracking($item['remark'])) : null,
'etd' => $etdEta['etd'],
'eta' => $etdEta['eta']
]);
Log::info('FetchByTrakingNoYdPortalV2Processor RECORD CREATED');
}
else{
Log::info('FetchByTrakingNoYdPortalV2Processor RECORD EXIST');
$isUpdated = false;
if($item['tracking']){
$isTranslatedPreviouly = $this->checkIfTranslationAlreadyExist(!empty($item['tracking']) ? $item['tracking'] : "");
$result = $isTranslatedPreviouly;
if (!$isTranslatedPreviouly){
$result = $this->translateTracking($item['tracking']);
}
if ($result && $existingRecord->tracking_english !== $result) {
$existingRecord->update([
'tracking_english' => $result
]);
$isUpdated = true;
}
}
if($item['remark']) {
$isTranslatedPreviouly = $this->checkIfTranslationAlreadyExist(!empty($item['remark']) ? $item['remark'] : "");
$result = $isTranslatedPreviouly;
if (!$isTranslatedPreviouly){
$result = $this->translateTracking($item['remark']);
}
if ($result && $existingRecord->remark_english !== $result) {
$existingRecord->update([
'remark_english' => $result
]);
$isUpdated = true;
}
}
if($isUpdated){
Log::info('FetchByTrakingNoYdPortalV2Processor RECORD UPDATED');
}
else{
Log::info('FetchByTrakingNoYdPortalV2Processor RECORD EXIST');
}
}
}
}
@@ -232,5 +285,44 @@ class FetchByTrakingNoYdPortalV2Processor
'eta' => $eta ? date('Y-m-d', strtotime($eta)) : null,
];
}
private function checkIfTranslationAlreadyExist($textToTranslate)
{
$returnText = "";
Log::info('textToTranslate: '.$textToTranslate);
if(!$textToTranslate) return $returnText;
$translationFound = YDOrderTracking::where('tracking', $textToTranslate)->whereNotNull('tracking_english')->first();
if(!$translationFound){
$translationFound = YDOrderTracking::where('remark', $textToTranslate)->whereNotNull('remark_english')->first();
if($translationFound){
$returnText = $translationFound->remark_english;
}
}
else{
$returnText = $translationFound->tracking_english;
}
Log::info('FOUND EXISTING TRANSLATION: '.$returnText);
return $returnText;
}
private function translateTracking($textToTranslate)
{
$userPrompt = "Please translate mandarin to english and return result in this format, e.g. {'mandarin': '.$textToTranslate.', 'english': ''}. ".$textToTranslate." ";
Log::info('prompt: '.$userPrompt);
$result = $this->createsChatGPTResponse->execute($userPrompt);
Log::info('result (json_encoded): '.json_encode($result));
$content = $result['choices'][0]['message']['content'] ?? null;
Log::info('content (json_encoded):'. json_encode($content));
$parsedContent = json_decode(str_replace("'", '"', $content), true);
if($parsedContent && $parsedContent['english']){
Log::info('Translated Text: ' . $parsedContent['english']);
return $parsedContent['english'];
}
else{
return null;
}
}
}
@@ -3,7 +3,8 @@
namespace App\Console\Commands\V2;
use App\Classes\Jobs\Commands\V2\YD\FetchByTrakingNoYdPortalV2CommandJob;
use App\Classes\Jobs\Commands\V2\YD\FetchTrakingInfoYDByPackingListV2CommandJob;
use App\Classes\Jobs\Commands\V2\YD\FetchTrakingInfoYDByTrackingNoV2CommandJob;
use App\Classes\ValueObjects\Constants\PackingListType;
use App\Models\PackingList;
use Carbon\Carbon;
@@ -17,7 +18,7 @@ class ProcessYDByTrakingNoDataV2Command extends Command
*
* @var string
*/
protected $signature = 'process-yd-by-traking-no-data-command';
protected $signature = 'process-yd-by-traking-no-data-command {trackingNo?}';
/**
* The console command description.
@@ -33,11 +34,19 @@ class ProcessYDByTrakingNoDataV2Command extends Command
*/
public function handle()
{
$jobs = $this->fetchByTrakingNoYdPortalV2CommandJob();
$trackingNo = $this->argument('trackingNo');
foreach ($jobs as $index => $job) {
$delayInSeconds = intdiv($index, 2);
dispatch($job)->delay(now()->addSeconds($delayInSeconds));
if ($trackingNo) {
dispatch(new FetchTrakingInfoYDByTrackingNoV2CommandJob($trackingNo));
Log::info("Job dispatched for tracking number: {$trackingNo}");
}
else {
$jobs = $this->fetchByTrakingNoYdPortalV2CommandJob();
foreach ($jobs as $index => $job) {
$delayInSeconds = intdiv($index, 2);
dispatch($job)->delay(now()->addSeconds($delayInSeconds));
}
}
}
@@ -48,9 +57,13 @@ class ProcessYDByTrakingNoDataV2Command extends Command
$packingLists = PackingList::where('type', PackingListType::SHIPPING_PACKING_LIST)->whereDate('created_at', '>=', $maxFetchTime)->get();
$count = 0;
foreach ($packingLists as $packingList) {
$jobs[] = new FetchByTrakingNoYdPortalV2CommandJob($packingList);
$jobs[] = new FetchTrakingInfoYDByPackingListV2CommandJob($packingList);
Log::info('Dispatched FetchByTrakingNoYdPortalV2CommandJob for packingList with id ' . $packingList->id . ' reference ' . $packingList->reference . ' created_at ' . $packingList->created_at);
$count++;
// Log::info('Dispatched count '. $count);
// if ($count === 50) {
// break;
// }
}
Log::info('Total FetchByTrakingNoYdPortalV2CommandJob dispatched: ' . $count);
@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Orders;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use App\Classes\Modules\Orders\ControllersLogic\ListOrderTrackingLogic;
class ListOrderTrackingController
{
/**
* @param Request $request
* @param ListOrderTrackingLogic $logic
* @return JsonResponse
*/
public function list(Request $request, ListOrderTrackingLogic $logic) : JsonResponse {
return $logic->execute($request);
}
}
@@ -0,0 +1,25 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class OrderTrackingResource extends JsonResource
{
/**
* Transform the resource into an array.
*
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'tracking_no' => $this->tracking_no,
'tracking' => $this->tracking_english . ($this->remark_english ? " (" . $this->remark_english . ")" : ''),
'tracking_time' => $this->tracking_time,
];
}
}
+2
View File
@@ -16,9 +16,11 @@ class YDOrderTracking extends Model
'tracking_no',
'package_id',
'tracking',
'tracking_english',
'tracking_time',
'add_time',
'remark',
'remark_english',
'eta',
'etd',
];
@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddTrackingEnglishAndRemarkEnglishToYdOrderTrackingTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('yd_order_tracking', function (Blueprint $table) {
$table->string('tracking_english')->nullable()->after('tracking');
$table->text('remark_english')->nullable()->after('remark');
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('yd_order_tracking', function (Blueprint $table) {
$table->dropColumn(['tracking_english', 'remark_english']);
});
}
}
@@ -22,7 +22,11 @@
<div class="row m-b-10 align-items-center" v-if="$store.getters.isAdmin">
<div class="col-auto">
<p class="no-margin all-caps fs-10 lh-10 light">Reference</p>
<p class="no-margin fs-12">{{item.reference}}</p>
<p class="no-margin fs-12">
<a :href="route('tracking', item.reference)" target="_blank">
{{ item.reference }}
</a>
</p>
</div>
</div>
<div class="row b-t b-b b-grey m-b-15">
@@ -98,7 +98,11 @@
<div class="row">
<div class="col">
<p class="no-margin fs-10 all-caps">Packinglist Reference</p>
<div>{{ item.packing_list_reference }}</div>
<div>
<a :href="route('tracking', item.packing_list_reference)" target="_blank">
{{ item.packing_list_reference }}
</a>
</div>
</div>
</div>
</div>
@@ -0,0 +1,51 @@
<template>
<div class="row m-b-15 parentContainer">
<div class="col">
<div class="row align-items-center">
<div class="col-auto">
<div class="row">
<div class="col-auto p-r-10">
<div class="font-heading all-caps fs-8 muted">Tracking No</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="font-heading all-caps fs-12">{{item.tracking_no}}</div>
</div>
</div>
</div>
<div class="col-auto">
<div class="row">
<div class="col-auto p-r-10">
<div class="font-heading all-caps fs-8 muted">Tracking Time</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="font-heading all-caps fs-12">{{item.tracking_time}}</div>
</div>
</div>
</div>
<div class="col-6">
<div class="row">
<div class="col-auto p-r-10">
<div class="font-heading all-caps fs-8 muted">Tracking</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="font-heading fs-12">{{item.tracking}}</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import componentHandler from '../../../general/mixins/componentHandler';
export default {
mixins: [componentHandler]
}
</script>
@@ -0,0 +1,89 @@
<template>
<div class="row m-b-15 parentContainer">
<div class="col">
<div class="row m-b-20">
<div class="col">
<h4 class="bold">Track your parcel</h4>
</div>
</div>
<div class="row">
<div class="col">
<div class="row m-b-15">
<div class="col">
<div class="row">
<div class="col p-r-0">
<div class="form-group no-margin form-group-default b-rad-none">
<label class="text-primary">Tracking No</label>
<input type="text" class="form-control" v-model="trackingNo" @keyup.enter="search"/>
</div>
</div>
</div>
</div>
<div class="col-auto p-l-0 p-r-0">
<div class="btn btn-primary b-rad-none" @click="search">
<i class="fa fa-search lh-40"></i>
</div>
</div>
<div class="col-auto p-l-0">
<div class="btn btn-secondary b-rad-none" @click="reset">
<i class="fa fa-remove lh-40"></i>
</div>
</div>
<div class="col"></div>
</div>
</div>
</div>
<list-component :key="currentKey" :section="section" :endpoint="route('api.order.tracking.list')" :options="options" v-if="showList">
<template slot="list" slot-scope="{data}">
<order-tracking-component :data="data"></order-tracking-component>
</template>
</list-component>
</div>
</div>
</template>
<script>
import componentHandler from '../../../general/mixins/componentHandler';
export default {
props: {
section: {
type: String,
required: true
},
tracking_no: {
type: String,
}
},
data() {
return {
showList: false,
currentKey: 1,
trackingNo: this.tracking_no || '',
options: {
'order_by': {column: 'order_tracking_id', DESC: false},
}
}
},
mounted() {
if (this.trackingNo.trim() !== '') {
this.search();
}
},
methods: {
search() {
const sanitizedTrackingNo = encodeURIComponent(this.trackingNo);
this.options['tracking_no'] = sanitizedTrackingNo;
this.currentKey+=1;
this.showList = true;
},
reset() {
this.trackingNo = '';
delete(this.options['tracking_no']);
this.currentKey+=1;
this.showList = false;
window.history.replaceState({}, '', '/tracking');
},
},
mixins: [componentHandler]
}
</script>
@@ -0,0 +1,8 @@
@extends('layouts.base_portal')
@section('inner_content')
<div class="row">
<div class="col">
<tracking-section-component section="trackingSection" tracking_no="{{ $tracking_no ?? '' }}"></tracking-section-component>
</div>
</div>
@endsection
+2
View File
@@ -26,4 +26,6 @@ Route::group(['prefix' => 'order', 'as' => 'order.', 'namespace' => 'Orders'], f
Route::post('/{id}/supplier-tax-rebate/create', 'CreateSupplierTaxRebateController@create')->name('tax_rebate.create');
Route::post('/supplier-tax-rebate/{id}/delete', 'DeleteSupplierTaxRebateController@delete')->name('tax_rebate.delete');
Route::get('/tracking/list', 'ListOrderTrackingController@list')->name('tracking.list');
});
+27
View File
@@ -46,6 +46,7 @@ use Illuminate\Support\Facades\Route;
use Mccarlosen\LaravelMpdf\Facades\LaravelMpdf;
use Illuminate\Support\Facades\Storage;
use App\Classes\General\AWSS3Helper;
use Illuminate\Support\Facades\Artisan;
/*
|--------------------------------------------------------------------------
@@ -445,6 +446,29 @@ Route::get('/yd', function (Request $request){
// (App()->make(FetchPackingListFromVTPortalProcessor::class))->execute();
})->name('yd.refresh');
Route::get('/yd-track', function (Request $request){
$trackingNo = $request->input('tracking_no');
if ($trackingNo) {
Artisan::call('process-yd-by-traking-no-data-command', [
'trackingNo' => $trackingNo
]);
return response()->json([
'message' => 'Tracking process started successfully with tracking number',
'trackingNo' => $trackingNo
]);
} else {
Artisan::call('process-yd-by-traking-no-data-command');
return response()->json([
'message' => 'Tracking process started using packing list'
]);
}
})->name('yd.track');
Route::get('/container/{reference}/refresh/', function (string $reference){
$container = Container::where('reference', $reference)->first();
$supplier = in_array($container->owner_id, [3, 4]) ? 'VT' : 'YD';
@@ -1442,3 +1466,6 @@ Route::get('/error-test', function () {
throw new \Exception("Intentional Exception for testing stack trace");
});
Route::get('/tracking/{tracking_no?}', function ($tracking_no = null) {
return view('pages.tracking.search', ['tracking_no' => $tracking_no]);
})->name('tracking');