Skip to content

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

#ProviderClassProtocolAuth
1ACS CourierAcsREST JSONMulti-credential (Company+User+API Key)
2ACS (SOAP fallback)AcsSoapSOAP 1.1Same as ACS
3Geniki Taxydromiki v1GenikiSOAP 1.1 (curl)Username + Password
4Geniki Taxydromiki v2GenikiV2SOAP 1.2 (SoapClient)Username + Password + App Key
5DHL ExpressDhlREST JSON (Guzzle)HTTP Basic Auth
6ELTA CouriersEltaSOAP 1.1 (custom XML)Embedded in SOAP
7SpeedexSpeedexSOAP 1.2 (SoapClient)Session-based (login → SessionID)
8Courier CenterCenterREST JSON (QualcoProvider)Context object (UserAlias+Credential+ApiKey)
9EasyMailEasyMailSOAP 1.1 (SoapClient)Credentials object
10FISFisSOAP 1.1 (SoapClient)Web Service Code
11Box NowBoxNowREST JSONOAuth 2.0 (Client Credentials)
12Taxydema v1TaxydemaSOAP 1.1 (custom)Embedded in SOAP
13Taxydema v2TaxydemaV2REST JSON (QualcoProvider)Context object
14Cyprus PostCyprusPostREST JSON (Guzzle)Bearer Token
15ASAP (Last Mily)AsapREST JSONHMAC-SHA256 signed
16Daily CourierDailyCourierREST JSON (Guzzle)Bearer Token
17Skroutz Last MileSkroutzREST JSON (Guzzle)Bearer Token

Common Operations

OperationMethodDescription
Create vouchercreateVoucher()Generate shipping label with tracking code
Cancel vouchercancelVoucher() / deleteVoucher()Cancel/delete shipment
Track & tracetrackAndTrace()Get shipment status history
Print vouchergetVoucherPdf()Download shipping label PDF
Close pendingclosePendingJobs()Create pickup list/dispatch
Get picking pointsgetPickingPoints()Locker/collection point locations
Live pickup-point catalogSmartPointCatalogService::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 with getFieldConfiguration() for admin UI schema and initialize() 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):

ColumnTypeDescription
idint (PK, AI)Row identifier
order_idint (FK)Associated order
transporter_idint (FK)Transporter providing the smart point
shop_idint (FK)Shop/store location
json_datatextSmart 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:

EndpointPurposeAuthStorage
GET /rest/order/smart-pointPer-order saved pickup-point rowauth=backend, roles [ADMIN, ORDERS] (application/config/rest_policies.php:563)shop_order_smart_point DB row
GET /rest/transporter/{id}/smart-pointLive pickup-point catalog for one transporterauth=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:

  1. Transporter row exists in DB AND active=1 — else 404 unknown_transporter
  2. class_name is in $config['smartPoints'] (application/config/app.php:402-425, 12 providers) — else 409 no_provider
  3. provider->areSmartPointsEnabledForThisProvider() returns true — else 409 not_enabled_for_provider
  4. provider->shouldFetchSmartPoints() returns true — else 409 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):

FieldTypeNullable
idstringno
namestringno
addressstringno
citystringyes
countrystringno
latitudefloatno
longitudefloatno
postalCodeintegerno
imagestringyes
notestringyes
titlestringyes
typestringyes
emailstringyes
workingHoursstringyes
phonestringyes
stationDestinationstringyes
stationBranchDestinationintegeryes

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). Returns list<{productName, productCode, price, pickupDateTime}>.
  • Query params:
    • destinationCountryCode (required)
    • weight in grams (required, >0; divided by 1000 for DHL kg at DhlRatesService.php:77)
    • destinationPostalCode (optional)
  • Status codes: 200 list; 400 missing_query_params/invalid_weight; 404 unknown_transporter (row missing or active != 1); 409 carrier_class_mismatch (transporter class_name !== 'DHL'); 502 transporter_unavailable
  • Testability: $dhlFactory is injectable (DhlRatesService.php:34-40); production default builds new 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 at AsapServicesService.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):

ExceptionTriggerHTTP status
UnknownTransporterExceptionTransporter row missing or active != 1404 unknown_transporter
CarrierClassMismatchExceptionclass_name doesn't match endpoint (carries transporterId, expectedClassName, actualClassName)409 carrier_class_mismatch
TransporterApiExceptionWraps any \Throwable from upstream carrier/geocoding API502 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 (DhlVoucherPersistershop_order_dhl_vouchers, AsapDataPersistershop_order_asap_data, SmartPointPersistershop_order_smart_point). Full detail in CF-06.

Known Issues & Security Gaps

  1. [RESOLVED — commit 5128ad7d5] BoxNow/Skroutz/ACS provider shape mismatch caused 502 on GET /rest/transporter/{id}/smart-point. SmartPointCatalogService::normalize() (src/Domains/Transporter/SmartPointCatalog/Service.php:92-109) unwraps the nested-by-class_name provider shape to flat SmartPointDTO[] before the isMalformed() guard. Belt-and-suspenders guard remains at :121-124.

  2. [NOTE — by design, commit 5128ad7d5] Five transporter class names have no entry in $config['smartPoints'] and will always return 409 no_provider from GET /rest/transporter/{id}/smart-point: CYPRUSPOST, DAILYCOURIER, ASAP, DHL, and TAXYDEMA. These are intentional omissions — they are cost-calculation/home-delivery carriers with no pickup-point provider class. TAXYDEMA is superseded by TAXYDEMAV2. Behavior is consistent with legacy Adv_order::smartPointsInitialize(). Documented inline at application/config/app.php:415-424.

  3. [NOTE] getPickingPoints() terminology in legacy docs/code refers to the same underlying operation as fetch() in SmartPointsInterface.php:20. The REST endpoint surfaces the modern interface name.

Tests

  • tests/Integration/Domains/Transporter/SmartPointCatalog/ServiceTest.php covers nested-shape normalization and provider-map assertions (commit 34e9d7620, #270).
  • tests/Unit/Domains/Transporter/ExternalRates/DhlRatesServiceTest.php and AsapServicesServiceTest.php provide 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.