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:
parent
1c3811b01f
commit
f8557a0f20
77
04_Laravel/app/Console/Commands/ImportTrackingData.php
Normal file
77
04_Laravel/app/Console/Commands/ImportTrackingData.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
224
04_Laravel/app/Imports/TrackingDataImport.php
Normal file
224
04_Laravel/app/Imports/TrackingDataImport.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
|
||||
11
04_Laravel/check_shipments.php
Normal file
11
04_Laravel/check_shipments.php
Normal 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;
|
||||
}
|
||||
15
04_Laravel/check_tracking.php
Normal file
15
04_Laravel/check_tracking.php
Normal 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;
|
||||
}
|
||||
}
|
||||
16
04_Laravel/debug_import.php
Normal file
16
04_Laravel/debug_import.php
Normal 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;
|
||||
24
04_Laravel/debug_import2.php
Normal file
24
04_Laravel/debug_import2.php
Normal 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');
|
||||
@ -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
13
04_Laravel/test_excel.php
Normal 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;
|
||||
Loading…
Reference in New Issue
Block a user