Appearance
Transporter Integrations (Courier APIs)
Flow ID: IN-09 Module(s): src/Transporters Complexity: High Last Updated: 2026-05-27
Business Context
17 shipping courier APIs integrated for voucher creation, cancellation, tracking, label printing, and pickup scheduling. Each transporter has a dedicated class in src/Transporters/ with config, helper, and main API class.
Integrated Transporters
| # | Provider | Class | Protocol | Auth |
|---|---|---|---|---|
| 1 | ACS Courier | Acs | REST JSON | Multi-credential (Company+User+API Key) |
| 2 | ACS (SOAP fallback) | AcsSoap | SOAP 1.1 | Same as ACS |
| 3 | Geniki Taxydromiki v1 | Geniki | SOAP 1.1 (curl) | Username + Password |
| 4 | Geniki Taxydromiki v2 | GenikiV2 | SOAP 1.2 (SoapClient) | Username + Password + App Key |
| 5 | DHL Express | Dhl | REST JSON (Guzzle) | HTTP Basic Auth |
| 6 | ELTA Couriers | Elta | SOAP 1.1 (custom XML) | Embedded in SOAP |
| 7 | Speedex | Speedex | SOAP 1.2 (SoapClient) | Session-based (login → SessionID) |
| 8 | Courier Center | Center | REST JSON (QualcoProvider) | Context object (UserAlias+Credential+ApiKey) |
| 9 | EasyMail | EasyMail | SOAP 1.1 (SoapClient) | Credentials object |
| 10 | FIS | Fis | SOAP 1.1 (SoapClient) | Web Service Code |
| 11 | Box Now | BoxNow | REST JSON | OAuth 2.0 (Client Credentials) |
| 12 | Taxydema v1 | Taxydema | SOAP 1.1 (custom) | Embedded in SOAP |
| 13 | Taxydema v2 | TaxydemaV2 | REST JSON (QualcoProvider) | Context object |
| 14 | Cyprus Post | CyprusPost | REST JSON (Guzzle) | Bearer Token |
| 15 | ASAP (Last Mily) | Asap | REST JSON | HMAC-SHA256 signed |
| 16 | Daily Courier | DailyCourier | REST JSON (Guzzle) | Bearer Token |
| 17 | Skroutz Last Mile | Skroutz | REST JSON (Guzzle) | Bearer Token |
Common Operations
| Operation | Method | Description |
|---|---|---|
| Create voucher | createVoucher() | Generate shipping label with tracking code |
| Cancel voucher | cancelVoucher() / deleteVoucher() | Cancel/delete shipment |
| Track & trace | trackAndTrace() | Get shipment status history |
| Print voucher | getVoucherPdf() | Download shipping label PDF |
| Close pending | closePendingJobs() | Create pickup list/dispatch |
| Get picking points | getPickingPoints() | Locker/collection point locations |
| Live pickup-point catalog | SmartPointCatalogService::fetch() | Aggregates getPickingPoints()/fetch() across one transporter via external API; exposed via GET /rest/transporter/{id}/smart-point (guest). (src/Domains/Transporter/SmartPointCatalog/Service.php:27) |
Base Classes
QualcoProvider: Shared base for Center and TaxydemaV2 (REST JSON with Context auth)BaseTransporterConfig: Standard config interface withgetFieldConfiguration()for admin UI schema andinitialize()for settings loading
Protocol Distribution
- REST JSON: 9 providers (DHL, BoxNow, ASAP, DailyCourier, Skroutz, Center, TaxydemaV2, CyprusPost, ACS)
- SOAP 1.1: 6 providers (ACS SOAP, Geniki v1, EasyMail, Elta, Taxydema v1, FIS)
- SOAP 1.2: 2 providers (Geniki v2, Speedex)
Environment Support
Most providers support dual environments (testing/production) via config toggle. SSL verification uses cached cacert.pem in development, true in production.
Live Platform Data
Aggregate statistics from production databases across all client deployments:
Transporter configuration: 14 transporters configured (8 active):
- Active: ACS, ACS-CY, DHL, SVUUM, Courier Center, BOXNOW + 2 more
- 6 inactive/standby configurations
Shipping volume data:
- 14,537 total pricing data points across 5 pricing tables (weight-based, zone-based, flat-rate, volumetric, smart-point)
- 150,000+ smart point deliveries (locker/collection point), representing ~12% of all orders
- 3 store locations configured (all active, all GR)
Smart Point Tracking (shop_order_smart_point table):
| Column | Type | Description |
|---|---|---|
id | int (PK, AI) | Row identifier |
order_id | int (FK) | Associated order |
transporter_id | int (FK) | Transporter providing the smart point |
shop_id | int (FK) | Shop/store location |
json_data | text | Smart point metadata (locker ID, address, provider details) |
Smart Point Catalog API
Two distinct smart-point surfaces exist in the REST API — do not conflate them:
| Endpoint | Purpose | Auth | Storage |
|---|---|---|---|
GET /rest/order/smart-point | Per-order saved pickup-point row | auth=backend, roles [ADMIN, ORDERS] (application/config/rest_policies.php:563) | shop_order_smart_point DB row |
GET /rest/transporter/{id}/smart-point | Live pickup-point catalog for one transporter | auth=guest (application/config/rest_policies.php:594-595) | No storage — live external API call |
Provider activation gating
Service::fetch() (src/Domains/Transporter/SmartPointCatalog/Service.php:29-51) applies a 4-stage gate:
- Transporter row exists in DB AND
active=1— else404 unknown_transporter class_nameis in$config['smartPoints'](application/config/app.php:402-425, 12 providers) — else409 no_providerprovider->areSmartPointsEnabledForThisProvider()returns true — else409 not_enabled_for_providerprovider->shouldFetchSmartPoints()returns true — else409 widget_only(widget transporters like BoxNow/Skroutz render the picker client-side; the REST endpoint is not the data source for them)
Provider shape normalization
The SmartPoints provider implementations (src/SmartPoints/Transporters/) return the legacy storefront shape that Adv_order::smartPointsInitialize() consumes — a nested array keyed by class_name:
[ $class_name => [
'smartPointStationData' => SmartPointDTO[],
'smartPointsEnable' => bool,
'responseError'? => string
] ]SmartPointCatalogService::normalize($result, $className) (src/Domains/Transporter/SmartPointCatalog/Service.php:92-109) unwraps $result[$className]['smartPointStationData'] into a flat array_values() list before the isMalformed() guard runs. Already-flat lists (empty [] from stub providers) pass through unchanged via isFlatDtoList() (:111-119). The isMalformed() guard (:121-124) runs after normalization as a post-normalization safety net — a genuinely unknown shape still produces a TransporterApiException (502). The provider implementations are never touched; the legacy storefront continues to receive the nested shape via its own call path.
Payload schema
Each pickup point in the response collection is a SmartPointDTO with 17 fields (src/Rest/Transporter/Resources/SmartPoint/Resource.php:9-30):
| Field | Type | Nullable |
|---|---|---|
| id | string | no |
| name | string | no |
| address | string | no |
| city | string | yes |
| country | string | no |
| latitude | float | no |
| longitude | float | no |
| postalCode | integer | no |
| image | string | yes |
| note | string | yes |
| title | string | yes |
| type | string | yes |
| string | yes | |
| workingHours | string | yes |
| phone | string | yes |
| stationDestination | string | yes |
| stationBranchDestination | integer | yes |
No caching
Requests hit the external provider API directly. No caching layer is applied (src/Domains/Transporter/SmartPointCatalog/Service.php:53-67). Org-wide REST caching policy is under discussion in ecommercen#241; this endpoint is a candidate once a policy lands.
External Rate Catalog API
Two REST endpoints expose live carrier pricing and availability lookups. Neither caches results. Both are guest-accessible on the same rationale as the legacy storefront equivalents they wrap.
GET /rest/transporter/{id}/dhl-rates
- Route:
application/config/rest_routes.php:1026-1027(+ locale-prefixed variant) - Auth: guest (
application/config/rest_policies.php:603-604) - Controller:
src/Rest/Transporter/Controllers/DhlRates.php - Service:
src/Domains/Transporter/ExternalRates/DhlRatesService.php - What it does: wraps legacy
AdvTransporters::baseGetDhlRates(). Hits the DHL Rates API live (no cache). Returnslist<{productName, productCode, price, pickupDateTime}>. - Query params:
destinationCountryCode(required)weightin grams (required, >0; divided by 1000 for DHL kg atDhlRatesService.php:77)destinationPostalCode(optional)
- Status codes: 200 list; 400
missing_query_params/invalid_weight; 404unknown_transporter(row missing oractive != 1); 409carrier_class_mismatch(transporterclass_name !== 'DHL'); 502transporter_unavailable - Testability:
$dhlFactoryis injectable (DhlRatesService.php:34-40); production default buildsnew Dhl(new DhlConfig($settings)) - Added in commit
0d06e18b0(#113f)
GET /rest/transporter/{id}/asap-services
- Route:
application/config/rest_routes.php:1028-1029; auth: guest (application/config/rest_policies.php:605-606) - Controller:
src/Rest/Transporter/Controllers/AsapServices.php - Service:
src/Domains/Transporter/ExternalRates/AsapServicesService.php - What it does: multi-hop — geocodes customer address (OpenStreetMaps), fetches carrier depot list, queries ASAP for available services. Thin bridge over legacy
AdvTransporters::getAsapServices()(porting rejected as not worth refactor — docblock atAsapServicesService.php:10-29). Server-side cutoff-time filter and Next Day/Express whitelist apply per the legacy contract. Returns the same{productName, productCode, price, pickupDateTime}shape as the DHL endpoint. - Query params:
country,weight(grams),address,city(all required)postalCode,county(optional)
- Status codes: same set as DHL; 409 fires on
class_name !== 'ASAP' - Added in commit
0d06e18b0(#113f)
ExternalRates exception classes
New namespace Advisable\Domains\Transporter\ExternalRates\Exceptions (distinct from SmartPointCatalog's exceptions):
| Exception | Trigger | HTTP status |
|---|---|---|
UnknownTransporterException | Transporter row missing or active != 1 | 404 unknown_transporter |
CarrierClassMismatchException | class_name doesn't match endpoint (carries transporterId, expectedClassName, actualClassName) | 409 carrier_class_mismatch |
TransporterApiException | Wraps any \Throwable from upstream carrier/geocoding API | 502 transporter_unavailable |
Carrier Data in REST Checkout
/rest/checkout/shipping returns three additional discriminators per option from ShippingCalculator: className (transporter class_name), deliveryOptionType (Registry-configured), requiresCarrierData (true iff DHL/ASAP/SmartPoint-capable AND delivery option allows pickup). After order placement, CarrierDataDispatcher routes the transporterExternalData blob to the matching persister (DhlVoucherPersister → shop_order_dhl_vouchers, AsapDataPersister → shop_order_asap_data, SmartPointPersister → shop_order_smart_point). Full detail in CF-06.
Known Issues & Security Gaps
[RESOLVED — commit
5128ad7d5] BoxNow/Skroutz/ACS provider shape mismatch caused 502 onGET /rest/transporter/{id}/smart-point.SmartPointCatalogService::normalize()(src/Domains/Transporter/SmartPointCatalog/Service.php:92-109) unwraps the nested-by-class_nameprovider shape to flatSmartPointDTO[]before theisMalformed()guard. Belt-and-suspenders guard remains at:121-124.[NOTE — by design, commit
5128ad7d5] Five transporter class names have no entry in$config['smartPoints']and will always return409 no_providerfromGET /rest/transporter/{id}/smart-point:CYPRUSPOST,DAILYCOURIER,ASAP,DHL, andTAXYDEMA. These are intentional omissions — they are cost-calculation/home-delivery carriers with no pickup-point provider class.TAXYDEMAis superseded byTAXYDEMAV2. Behavior is consistent with legacyAdv_order::smartPointsInitialize(). Documented inline atapplication/config/app.php:415-424.[NOTE]
getPickingPoints()terminology in legacy docs/code refers to the same underlying operation asfetch()inSmartPointsInterface.php:20. The REST endpoint surfaces the modern interface name.
Tests
tests/Integration/Domains/Transporter/SmartPointCatalog/ServiceTest.phpcovers nested-shape normalization and provider-map assertions (commit34e9d7620, #270).tests/Unit/Domains/Transporter/ExternalRates/DhlRatesServiceTest.phpandAsapServicesServiceTest.phpprovide unit coverage for the external-rate services. No live-API integration tests exist for DHL/ASAP.tests/Unit/Rest/Transporter/Controllers/SmartPointTest.php,tests/Unit/Rest/Transporter/Resources/SmartPoint/ResourceTest.php— unit coverage for the smart-point REST surface.
Related Flows
- AD-06 Transporter Admin — credential/pricing configuration
- AD-03 Order Management — voucher lifecycle (create/close/cancel)
- CF-06 Order Preview — transporter selection at checkout; carrier data persistence after placement