Appearance
Multi-Carrier Tracking
Flow ID: AD-33 Module(s): job, eshop, Transporters Complexity: High Last Updated: 2026-06-04
Business Overview
Ecommercen supports automated delivery tracking across 15 carrier integrations. A scheduled job polls each carrier's API for shipment status updates, writing normalized tracking data back to the shop_order table. The system also provides on-demand track-and-trace lookups from the admin order detail view and generates external tracking URLs for customer-facing emails and order pages.
The tracking subsystem is decoupled from voucher generation -- an order only enters the tracking pipeline once it has a gtcode (voucher/tracking number) assigned by the voucher generation flow (AD-34).
API Reference
REST Endpoints
No dedicated REST endpoint. Tracking data is exposed as part of the order resource:
| Method | Path | Action |
|---|---|---|
GET | /rest/order/order/{id} | Order resource includes gtcode, gtstatus, gtdatetime, gtflag |
Admin Endpoints
| Route | Controller Method | Description |
|---|---|---|
CLI: php cli.php job/AdvGetOrdersTransferStatus | AdvGetOrdersTransferStatus::executeCommand() | Scheduled bulk tracking poll (legacy, active) |
CLI: php cli.php job/Advisable\Domains\Transporter\Jobs\GetOrdersTransferStatus | GetOrdersTransferStatus::executeCommand() | Modern port — same poll, FQCN-invoked (toggle-activate) |
| Admin order detail | Adv_orders_admin (inline) | On-demand single-order track-and-trace |
Code Flow
Automated Tracking Poll (Cron)
AdvGetOrdersTransferStatus::executeCommand()
|
+--> Query shop_order WHERE gtcode != '' AND status NOT IN (PENDING, CANCELED, RETURN)
| AND gtflag = 0 AND gtcount < 10
|
+--> getOrdersByTransferProviderId() -- group orders by transport_id
|
+--> For each provider:
|
+--> Load provider master record + settings from transporters_model
+--> Switch on provider->class_name:
|
+--> Instantiate carrier SDK (e.g., new Acs(new AcsConfig($settings)))
+--> Call trackAndTrace() / tracking() / getTracking() per order
+--> Normalize response to: gtstatus, gtdatetime, gtflag
+--> UPDATE shop_order SET gtstatus, gtdatetime, gtflag, gtcount+1On-Demand Track & Trace
AdvTrackAndTrace::track($provider, $providerSettings, $voucher)
|
+--> Switch on provider->class_name
+--> Instantiate carrier SDK
+--> Call trackAndTrace() / tracking() / getTracking()
+--> Return raw carrier response objectExternal Tracking URLs
getLinkForTransferProvider($provider, $gtCode) -- full tracking URL for customer
getMinifiedLinkForTransferProvider($provider) -- shortened base URLEach carrier SDK has a static externalTrackUrl($gtCode) method in its Helper class (e.g., AcsHelper::externalTrackUrl()).
Domain Layer
Key Files
| File | Responsibility |
|---|---|
ecommercen/job/libraries/AdvGetOrdersTransferStatus.php | Scheduled job (legacy, active) -- polls all carriers |
src/Domains/Transporter/Jobs/GetOrdersTransferStatus.php | Modern port -- same 15-carrier logic, DI-injected TransporterSettingsLoader; FQCN-registered; toggle-activated when ready |
ecommercen/libraries/vouchers/AdvTrackAndTrace.php | On-demand single-voucher tracking dispatcher |
ecommercen/helpers/transporters_helper.php | External tracking URL generators, provider grouping |
src/Transporters/{Carrier}/{Carrier}.php | Carrier SDK client (API calls) |
src/Transporters/{Carrier}/{Carrier}Config.php | Carrier-specific configuration DTO |
src/Transporters/{Carrier}/{Carrier}Helper.php | Static helpers (external URLs, utilities) |
Supported Carriers (15)
| class_name | Carrier | SDK Class | Track Method |
|---|---|---|---|
GT | Geniki v1 | Geniki | trackAndTrace() |
GTV2 | Geniki v2 | GenikiV2 | trackAndTrace() |
ACS | ACS (REST) | Acs | trackAndTrace() |
ACSSoap | ACS (SOAP) | AcsSoap | trackAndTrace() |
ELTA | ELTA Courier | Elta | trackAndTrace() |
SPEEDEX | Speedex | Speedex | getLastCheckpoint() |
CENTER | Center | Center | tracking() |
EASYMAIL | EasyMail | EasyMail | trackAndTrace() |
BOXNOW | BoxNow | BoxNow | trackAndTrace() |
CYPRUSPOST | Cyprus Post | CyprusPost | trackAndTrace() |
DHL | DHL Express | Dhl | getTracking() |
TAXYDEMA | Taxydema v1 | Taxydema | trackAndTrace() |
TAXYDEMAV2 | Taxydema v2 | TaxydemaV2 | tracking() |
SKROUTZ | Skroutz Last Mile | Skroutz | trackAndTrace() |
ASAP | ASAP Courier | Asap | getFulfillmentEventData() (disabled -- needs testing) |
Architecture
ecommercen/job/libraries/AdvGetOrdersTransferStatus.php -- legacy cron entry (active)
src/Domains/Transporter/Jobs/GetOrdersTransferStatus.php -- modern port (toggle-activate via jobs.php)
ecommercen/libraries/vouchers/AdvTrackAndTrace.php -- admin on-demand
ecommercen/helpers/transporters_helper.php -- URL helpers
src/Transporters/
├── {Carrier}/
│ ├── {Carrier}.php -- API client
│ ├── {Carrier}Config.php -- Config DTO (from transporters_settings)
│ └── {Carrier}Helper.php -- Static helpers, externalTrackUrl()
├── BaseTransporterConfig.php -- Shared config base
└── TransporterConfigInterface.phpAll carrier SDKs live under the Advisable\Transporters namespace. Each carrier follows the pattern: API client class + Config DTO + Helper class. Configs are populated from the transporters_settings key-value table, loaded via transporters_model::settings($providerId).
Data Model
shop_order (tracking columns)
| Column | Type | Description |
|---|---|---|
transport_id | int(11) | FK to transporters master table |
gtcode | varchar(30) | Carrier tracking/voucher number |
gtjobcode | varchar(255) | Carrier job ID (for cancellation) |
gtstatus | varchar(255) | Latest tracking status (carrier-specific text) |
gtflag | tinyint(1) | 1 = delivered (terminal state), 0 = in transit |
gtdatetime | datetime | Timestamp of last status update |
gtcount | int(3) | Number of poll attempts (max 10 before giving up) |
Key index: gtcode_gtflag_gtcount -- optimizes the cron job's SELECT query.
Tracking State Machine
gtcode assigned (voucher created)
└── gtflag=0, gtcount=0
└── Cron polls carrier API
├── Status update → gtstatus='{carrier status}', gtcount++
├── Delivered → gtstatus='ΠΑΡΑΔΟΜΕΝΟ', gtflag=1 (stops polling)
└── gtcount reaches 10 → stops polling (abandoned)Configuration
- Cron schedule: Configured as a scheduled job via
AdvGetOrdersTransferStatus(25 4 * * *— daily at 04:25, perapplication/config/jobs.php). The modern port ships with a commented-out toggle entry alongside the legacy one; operators flip the leading//to switch from the legacy job to the modern Transporter-domain job (the same convention asPollPayByBankStatus, SY-16). - Provider settings: Stored in
transporters_settingstable, managed viaAdv_transporters_admin(AD-06) - Poll limit: Hardcoded at
gtcount < 10(10 attempts before abandoning) - Rate limiting: 250ms delay (
usleep(250000)) between provider batches
Client Extension Points
- Custom carrier: Add a new class under
src/Transporters/, implementtrackAndTrace(), and add acaseto each voucher library switch statement - Client-repo override: The legacy
AdvGetOrdersTransferStatusjob can be overridden viaapplication/modules/job/libraries/GetOrdersTransferStatus.php(an empty subclass by default). The modern port is overridden via a DI alias incustom/Domains/container.php(thePollPayByBankStatuspattern, SY-16).
Business Rules
- Only active orders are tracked: Orders with status
PENDING,CANCELED, orRETURNare excluded from polling - Terminal state is delivery: When
gtflagis set to 1 (delivered), the order exits the tracking pipeline permanently - Maximum 10 attempts: After 10 unsuccessful polls, the system stops checking -- manual intervention required
- Normalized delivery status: Most carriers normalize delivery status to the Greek string
ΠΑΡΑΔΟΜΕΝΟ. Exceptions: GenikiV2 writes the carrier's raw English status (e.g.'DELIVERED') while still settinggtflag = 1 - Speedex uses final status codes: Unlike other carriers that check string status, Speedex uses numeric
finalVoucherStatusCodes(success + failure codes) - ASAP tracking is disabled: The
getAsapTransporterStatus()method contains an earlyreturn-- needs real data testing before activation
Known Issues & Security Gaps
Fixed in #275
Three pre-existing defensive-coding gaps in the tracking poller, fixed in both the legacy job and the modern port under Advisable-com/ecommercen#275 (the #274 port carried them over verbatim; #275 fixed both copies together).
Null
$providertriggers a recurring warning.switch ($provider->class_name)(ecommercen/job/libraries/AdvGetOrdersTransferStatus.php, portsrc/Domains/Transporter/Jobs/GetOrdersTransferStatus.php) assumedgetMasterRecord()returns a row. An orphanedtransporters_settingsrow (settings present, master record deleted) passed theif (!$providerSettings) continue;guard but yielded$provider === null, raising anAttempt to read property "class_name" on nullwarning under PHP 8.1 (non-fatal —switch(null)then matches no case and the provider is skipped, so the run continues, but the warning recurs every poll for every orphaned row). Fix: the guard is nowif (!$provider || !$providerSettings) continue;. Regression-guarded bytests/Integration/Domains/Transporter/Jobs/GetOrdersTransferStatusTest.php.array_pop()on single-checkpoint responses.array_pop($track->Checkpoints->Checkpoint)(Center, EasyMail, CyprusPost, TaxydemaV2) requires an array passed by reference. When a carrier collapsed a single checkpoint to a scalar/object, the truthy guard still passed butarray_popraised a TypeError/warning and returned null, then$lastCheckpoint->Statusdereferenced null. Fix: normalize first —$checkpoints = $track->Checkpoints->Checkpoint; $lastCheckpoint = is_array($checkpoints) ? end($checkpoints) : $checkpoints;.Undefined-array-key warning on unmapped status.
$statusLabels[$track->Status]/$statusLabels[$track->status_description](BoxNow, Skroutz) — an unmapped carrier status raised an "Undefined array key" warning and evaluated to null. Fix:$statusLabels[$key] ?? null— the null-coalescing operator suppresses the warning while persisting the same NULL value as before, so nothing downstream changes.
Stale class docblock
GetOrdersTransferStatusclass docblock says "14 carrier gateways" but there are 15 (src/Domains/Transporter/Jobs/GetOrdersTransferStatus.php:47). The switch block handles GT, GTV2, ACS, ACSSoap, ELTA, SPEEDEX, CENTER, EASYMAIL, BOXNOW, CYPRUSPOST, DHL, TAXYDEMA, SKROUTZ, ASAP, and TAXYDEMAV2 — 15 cases. The "14" comment is a stale copy-paste error from before TAXYDEMAV2 was added.
Fixed in this doc-ba-resync pass
- Speedex
'succcess'typo caused delivered orders to never setgtflag = 1.getSpeedexTransporterStatus()accessed$speedexConfig->finalVoucherStatusCodes['succcess'](triple 'c') butSpeedexConfig::finalVoucherStatusCodesdefines the key as'success'. The typo made the success-code sub-array null, soarray_merge(null, $failureCodes)produced only failure codes —in_array($lastCheckpoint->StatusCode, $finalVoucherStatusCodes)could never match a successful-delivery code, meaninggtflagwas never set to 1 for delivered Speedex orders, wasting 10 poll cycles per order. Fixed in bothecommercen/job/libraries/AdvGetOrdersTransferStatus.php:301andsrc/Domains/Transporter/Jobs/GetOrdersTransferStatus.php:322. Tracked as Advisable-com/ecommercen#277.
Fixed in #276
- Loyalty-points job skipped delivered orders when
gtstatus != 'ΠΑΡΑΔΟΜΕΝΟ'.AdvAddPointsToCustomerDeliverpreviously matched delivered orders byWHERE gtstatus = 'ΠΑΡΑΔΟΜΕΝΟ'without also checkinggtflag = 1. GenikiV2 deliveries returned the English'DELIVERED'status (gtflag = 1,gtstatus = 'DELIVERED'), so the loyalty job's string filter skipped them. Fixed on branchfix/loyalty-gtflag-276: the job now matches onWHERE gtflag = 1, correctly including GenikiV2 English-status delivered orders. Tracked in Advisable-com/ecommercen#276.
Open
No open issues at the time of writing.
Tests
| Suite | File / Method | What it covers |
|---|---|---|
| Integration | tests/Integration/Domains/Transporter/Jobs/GetOrdersTransferStatusTest.php::test_orphaned_provider_settings_do_not_abort_the_poll | Regression-guards the null-provider fix (#275 item 1): seeds an orphaned transporters_settings row with no matching transporters master record, installs a strict E_WARNING→exception handler, and asserts executeCommand() completes without warning and leaves the order's gtcount unchanged. |
| Integration | …::test_speedex_final_voucher_status_codes_merge_does_not_warn | Regression-guards the Speedex typo fix (#277): invokes getSpeedexTransporterStatus() via reflection with empty $orders so the array_merge($success, $failure) line runs without making a SOAP call; a strict E_WARNING handler fails the test if anyone re-introduces 'succcess'. |
The single-checkpoint normalization (#275 item 2) and the unmapped-status null-coalescing (#275 item 3) are verified by php -l and code inspection — they live inside carrier methods that instantiate live-API gateway classes, making them unsuitable for unit tests without a refactor.
Related Flows
- AD-06 Transporter Admin -- Provider configuration and API credentials
- AD-34 Voucher Generation -- Creates the
gtcodethat enables tracking - AD-03 Order Management -- Admin order views showing tracking status
- SY-01 Cron Framework -- Job scheduling infrastructure
- CF-32 Loyalty Points -- Awards points on delivered orders keyed on
gtflag = 1(fixed in #276) - IN-09 Transporter Integrations -- Carrier API integration details
Wiki Guide: DHL Integration Guide -- specific DHL Express setup and configuration