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
This commit is contained in:
Kazem Alghasi 2026-08-04 05:38:35 +03:30
parent 1c3811b01f
commit f8557a0f20
9 changed files with 388 additions and 0 deletions

View File

@ -0,0 +1,77 @@
<?php
namespace App\Console\Commands;
use App\Imports\TrackingDataImport;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Facades\Excel;
class ImportTrackingData extends Command
{
protected $signature = 'ifnex:import:tracking {path : Path to Excel file}';
protected $description = 'Import tracking events from Excel file';
public function handle(): int
{
$path = $this->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;
}
}
}

View File

@ -0,0 +1,224 @@
<?php
namespace App\Imports;
use App\Models\Shipment;
use App\Models\ShipmentTrackingEvent;
use App\Models\Country;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Concerns\ToCollection;
use Maatwebsite\Excel\Concerns\WithHeadingRow;
use Illuminate\Support\Collection;
class TrackingDataImport implements ToCollection, WithHeadingRow
{
protected $importedEvents = 0;
protected $skippedEvents = 0;
protected $errors = [];
protected $createdShipments = 0;
public function collection(Collection $rows)
{
Log::info('Tracking import started', ['total_rows' => $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;
}
}

View File

@ -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);

View File

@ -0,0 +1,11 @@
<?php
require 'vendor/autoload.php';
$app = require_once 'bootstrap/app.php';
$kernel = $app->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;
}

View File

@ -0,0 +1,15 @@
<?php
require 'vendor/autoload.php';
$app = require_once 'bootstrap/app.php';
$kernel = $app->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;
}
}

View File

@ -0,0 +1,16 @@
<?php
require 'vendor/autoload.php';
$app = require_once 'bootstrap/app.php';
$kernel = $app->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;

View File

@ -0,0 +1,24 @@
<?php
require 'vendor/autoload.php';
$app = require_once 'bootstrap/app.php';
$kernel = $app->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');

View File

@ -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');

13
04_Laravel/test_excel.php Normal file
View File

@ -0,0 +1,13 @@
<?php
require 'vendor/autoload.php';
use PhpOffice\PhpSpreadsheet\IOFactory;
$file = '../01_Documents/Data entry 2026-06-28.xlsx';
$spreadsheet = IOFactory::load($file);
$sheetNames = $spreadsheet->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;