From f8557a0f20437a41943e5290f5545222c5a3cd74 Mon Sep 17 00:00:00 2001 From: Kazem Alghasi Date: Tue, 4 Aug 2026 05:38:35 +0330 Subject: [PATCH] feat(console): add tracking data import functionality and utility scripts Implement a new Artisan command and Excel import logic to handle tracking data ingestion. This includes registering new commands in the application bootstrap and providing several utility scripts for debugging and testing the import process. - Add `ImportTrackingData` command and `TrackingDataImport` class - Register `ImportShippingRates` and `ImportTrackingData` in `app.php` - Add `ifnex:import:tracking` closure command in `console.php` - Include various debugging and testing scripts for Excel and shipment verification --- .../Console/Commands/ImportTrackingData.php | 77 ++++++ 04_Laravel/app/Imports/TrackingDataImport.php | 224 ++++++++++++++++++ 04_Laravel/bootstrap/app.php | 4 + 04_Laravel/check_shipments.php | 11 + 04_Laravel/check_tracking.php | 15 ++ 04_Laravel/debug_import.php | 16 ++ 04_Laravel/debug_import2.php | 24 ++ 04_Laravel/routes/console.php | 4 + 04_Laravel/test_excel.php | 13 + 9 files changed, 388 insertions(+) create mode 100644 04_Laravel/app/Console/Commands/ImportTrackingData.php create mode 100644 04_Laravel/app/Imports/TrackingDataImport.php create mode 100644 04_Laravel/check_shipments.php create mode 100644 04_Laravel/check_tracking.php create mode 100644 04_Laravel/debug_import.php create mode 100644 04_Laravel/debug_import2.php create mode 100644 04_Laravel/test_excel.php diff --git a/04_Laravel/app/Console/Commands/ImportTrackingData.php b/04_Laravel/app/Console/Commands/ImportTrackingData.php new file mode 100644 index 0000000..006097e --- /dev/null +++ b/04_Laravel/app/Console/Commands/ImportTrackingData.php @@ -0,0 +1,77 @@ +argument('path'); + + if (!file_exists($path)) { + $this->error("File not found: {$path}"); + return 1; + } + + $this->info("Starting tracking data import from: {$path}"); + $this->newLine(); + + try { + $import = new TrackingDataImport(); + Excel::import($import, $path); + + $imported = $import->getImportedEvents(); + $skipped = $import->getSkippedEvents(); + $errors = $import->getErrors(); + $createdShipments = $import->getCreatedShipments(); + + $this->info("Import completed successfully!"); + $this->newLine(); + $this->table( + ['Metric', 'Count'], + [ + ['Imported Events', $imported], + ['Skipped Events', $skipped], + ['Created Shipments', $createdShipments], + ['Errors', count($errors)], + ] + ); + + if ($errors) { + $this->newLine(); + $this->warn('Errors:'); + foreach (array_slice($errors, 0, 20) as $error) { + $this->error(" - {$error}"); + } + if (count($errors) > 20) { + $this->warn(" ... and " . (count($errors) - 20) . " more errors"); + } + } + + Log::info('Tracking import command completed', [ + 'path' => $path, + 'imported' => $imported, + 'skipped' => $skipped, + 'errors' => count($errors), + ]); + + return 0; + } catch (\Throwable $e) { + $this->error("Import failed: " . $e->getMessage()); + Log::error('Tracking import command failed', [ + 'path' => $path, + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + return 1; + } + } +} diff --git a/04_Laravel/app/Imports/TrackingDataImport.php b/04_Laravel/app/Imports/TrackingDataImport.php new file mode 100644 index 0000000..797d199 --- /dev/null +++ b/04_Laravel/app/Imports/TrackingDataImport.php @@ -0,0 +1,224 @@ + $rows->count()]); + + if ($rows->count() === 0) { + Log::warning('No rows found'); + return; + } + + $firstRow = $rows->first(); + Log::info('First row keys', ['keys' => $firstRow->keys()->toArray()]); + Log::info('First row awb', ['awb' => $firstRow['awb'] ?? 'N/A']); + + foreach ($rows as $index => $row) { + try { + $awbNo = $this->normalizeAwb($row['awb'] ?? null); + $eventDate = $this->normalizeDate($row['date'] ?? null); + $eventTime = $this->normalizeTime($row['time'] ?? null); + $eventDescription = $this->normalizeDescription($row['state'] ?? null); + $location = $this->normalizeLocation($row['country'] ?? null); + $deliveryStatus = $this->normalizeDeliveryStatus($row['last_state'] ?? null); + + if (!$awbNo || !$eventDate || !$eventDescription) { + $this->skippedEvents++; + continue; + } + + $shipment = Shipment::where('awb_no', $awbNo)->first(); + + if (!$shipment) { + $shipment = $this->createShipmentFromTrackingRow($row, $awbNo, $location); + $this->createdShipments++; + } + + ShipmentTrackingEvent::create([ + 'shipment_id' => $shipment->id, + 'event_date' => $eventDate, + 'event_time' => $eventTime, + 'location' => $location, + 'event_description' => $eventDescription, + 'delivery_status' => $deliveryStatus, + 'source' => 'manual', + ]); + + $this->importedEvents++; + } catch (\Throwable $e) { + $this->skippedEvents++; + $this->errors[] = "Row {$index}: " . $e->getMessage(); + Log::error('Tracking import error', ['row' => $index, 'error' => $e->getMessage()]); + } + } + + Log::info('Tracking import completed', [ + 'imported' => $this->importedEvents, + 'skipped' => $this->skippedEvents, + 'created' => $this->createdShipments, + 'errors' => count($this->errors), + ]); + } + + protected function createShipmentFromTrackingRow(array $row, string $awbNo, ?string $location): Shipment + { + $countryName = $location ? explode(' - ', $location)[0] : null; + $country = Country::where('name', 'like', "%{$countryName}%")->first(); + + $lastState = strtolower($row['last_state'] ?? ''); + + $status = 'processed'; + if (str_contains($lastState, 'delivered')) $status = 'delivered'; + elseif (str_contains($lastState, 'in transit')) $status = 'in_transit'; + elseif (str_contains($lastState, 'picked')) $status = 'picked_up'; + elseif (str_contains($lastState, 'out for delivery')) $status = 'out_for_delivery'; + elseif (str_contains($lastState, 'failed')) $status = 'failed'; + + $shipment = Shipment::create([ + 'awb_no' => $awbNo, + 'direction' => 'export', + 'type' => 'PARCEL', + 'status' => $status, + 'weight' => 0, + 'volumetric_weight' => 0, + 'chargeable_weight' => 0, + 'shipping_price' => 0, + 'extra_service' => 0, + 'packing_cost' => 0, + 'domestic_pickup' => 0, + 'domestic_delivery' => 0, + 'warehousing_cost' => 0, + 'vat_amount' => 0, + 'discount' => 0, + 'total_fee' => 0, + 'net_dirham' => 0, + 'net_rial' => 0, + 'from_country_id' => Country::where('name', 'Iran')->first()?->id, + 'to_country_id' => $country?->id, + 'sender_name' => 'Unknown', + 'receiver_name' => 'Unknown', + ]); + + return $shipment; + } + + public function getImportedEvents(): int + { + return $this->importedEvents; + } + + public function getSkippedEvents(): int + { + return $this->skippedEvents; + } + + public function getErrors(): array + { + return $this->errors; + } + + public function getCreatedShipments(): int + { + return $this->createdShipments; + } + + protected function normalizeAwb($value): ?string + { + if (!$value) return null; + $value = trim($value); + return $value === '' || $value === '0' ? null : $value; + } + + protected function normalizeDate($value): ?string + { + if (!$value) return null; + + if ($value instanceof \DateTime) { + return $value->format('Y-m-d'); + } + + $value = trim($value); + if ($value === '' || $value === '1899-12-31') return null; + + try { + return date('Y-m-d', strtotime($value)); + } catch (\Throwable $e) { + return null; + } + } + + protected function normalizeTime($value): ?string + { + if (!$value) return null; + + if ($value instanceof \DateTime) { + return $value->format('H:i:s'); + } + + $value = trim($value); + if ($value === '' || $value === '1899-12-31') return null; + + try { + return date('H:i:s', strtotime($value)); + } catch (\Throwable $e) { + return null; + } + } + + protected function normalizeDescription($value): ?string + { + if (!$value) return null; + $value = trim($value); + return $value === '' ? null : $value; + } + + protected function normalizeLocation($value): ?string + { + if (!$value) return null; + $value = trim($value); + return $value === '' || $value === '.' ? null : $value; + } + + protected function normalizeDeliveryStatus($value): ?string + { + if (!$value) return null; + $value = trim($value); + if ($value === '') return null; + + $statuses = [ + 'processed' => 'processed', + 'picked up' => 'picked_up', + 'in transit' => 'in_transit', + 'out for delivery' => 'out_for_delivery', + 'failed' => 'failed', + 'delivered' => 'delivered', + 'returned' => 'returned', + ]; + + $lower = strtolower($value); + foreach ($statuses as $key => $status) { + if (str_contains($lower, $key)) { + return $status; + } + } + + return null; + } +} diff --git a/04_Laravel/bootstrap/app.php b/04_Laravel/bootstrap/app.php index 05cdcf1..e5c4b91 100644 --- a/04_Laravel/bootstrap/app.php +++ b/04_Laravel/bootstrap/app.php @@ -11,6 +11,10 @@ return Application::configure(basePath: dirname(__DIR__)) commands: __DIR__.'/../routes/console.php', health: '/up', ) + ->withCommands([ + \App\Console\Commands\ImportShippingRates::class, + \App\Console\Commands\ImportTrackingData::class, + ]) ->withMiddleware(function (Middleware $middleware): void { $middleware->api(prepend: \Illuminate\Http\Middleware\HandleCors::class); diff --git a/04_Laravel/check_shipments.php b/04_Laravel/check_shipments.php new file mode 100644 index 0000000..fb72617 --- /dev/null +++ b/04_Laravel/check_shipments.php @@ -0,0 +1,11 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +$shipments = App\Models\Shipment::all(); +echo 'Total shipments: ' . $shipments->count() . PHP_EOL; +foreach ($shipments as $s) { + echo 'AWB: ' . $s->awb_no . PHP_EOL; +} diff --git a/04_Laravel/check_tracking.php b/04_Laravel/check_tracking.php new file mode 100644 index 0000000..9a80dbe --- /dev/null +++ b/04_Laravel/check_tracking.php @@ -0,0 +1,15 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +$shipment = App\Models\Shipment::where('awb_no', '980103619')->first(); +echo 'Shipment: ' . ($shipment ? $shipment->awb_no : 'Not found') . PHP_EOL; + +if ($shipment) { + echo 'Events: ' . $shipment->trackingEvents()->count() . PHP_EOL; + foreach ($shipment->trackingEvents()->orderBy('event_date', 'desc')->limit(3)->get() as $e) { + echo $e->event_date . ' - ' . $e->event_description . PHP_EOL; + } +} diff --git a/04_Laravel/debug_import.php b/04_Laravel/debug_import.php new file mode 100644 index 0000000..8af73da --- /dev/null +++ b/04_Laravel/debug_import.php @@ -0,0 +1,16 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +use Maatwebsite\Excel\Facades\Excel; +use App\Imports\TrackingDataImport; + +Excel::import(new TrackingDataImport(), '../01_Documents/Data entry 2026-06-28.xlsx'); + +$import = app(TrackingDataImport::class); +echo 'Imported: ' . $import->getImportedEvents() . PHP_EOL; +echo 'Skipped: ' . $import->getSkippedEvents() . PHP_EOL; +echo 'Created: ' . $import->getCreatedShipments() . PHP_EOL; +echo 'Errors: ' . count($import->getErrors()) . PHP_EOL; diff --git a/04_Laravel/debug_import2.php b/04_Laravel/debug_import2.php new file mode 100644 index 0000000..5035a02 --- /dev/null +++ b/04_Laravel/debug_import2.php @@ -0,0 +1,24 @@ +make(Illuminate\Contracts\Console\Kernel::class); +$kernel->bootstrap(); + +use Maatwebsite\Excel\Facades\Excel; +use Maatwebsite\Excel\Concerns\ToCollection; +use Maatwebsite\Excel\Concerns\WithHeadingRow; +use Illuminate\Support\Collection; + +class DebugImport implements ToCollection, WithHeadingRow +{ + public function collection(Collection $rows) + { + echo 'Total rows: ' . $rows->count() . PHP_EOL; + if ($rows->count() > 0) { + echo 'First row keys: ' . implode(', ', $rows->first()->keys()->toArray()) . PHP_EOL; + echo 'First row AWB: ' . ($rows->first()['AWB'] ?? 'N/A') . PHP_EOL; + } + } +} + +Excel::import(new DebugImport(), '../01_Documents/Data entry 2026-06-28.xlsx'); diff --git a/04_Laravel/routes/console.php b/04_Laravel/routes/console.php index 3c9adf1..8e249da 100644 --- a/04_Laravel/routes/console.php +++ b/04_Laravel/routes/console.php @@ -6,3 +6,7 @@ use Illuminate\Support\Facades\Artisan; Artisan::command('inspire', function () { $this->comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); + +Artisan::command('ifnex:import:tracking {path : Path to Excel file}', function ($path) { + $this->call('import:tracking', ['path' => $path]); +})->purpose('Import tracking events from Excel file'); diff --git a/04_Laravel/test_excel.php b/04_Laravel/test_excel.php new file mode 100644 index 0000000..79cc4a1 --- /dev/null +++ b/04_Laravel/test_excel.php @@ -0,0 +1,13 @@ +getSheetNames(); +echo 'Sheets: ' . implode(', ', $sheetNames) . PHP_EOL; +$sheet = $spreadsheet->getSheet(0); +echo 'Sheet 0 name: ' . $sheet->getTitle() . PHP_EOL; +echo 'Row 1: ' . implode(', ', $sheet->rangeToArray('A1:H1')[0]) . PHP_EOL; +echo 'Row 2: ' . implode(', ', $sheet->rangeToArray('A2:H2')[0]) . PHP_EOL; +echo 'Row 3: ' . implode(', ', $sheet->rangeToArray('A3:H3')[0]) . PHP_EOL; +echo 'Total rows: ' . $sheet->getHighestRow() . PHP_EOL;