Appearance
Transporter / Shipping Management (Admin)
Flow ID: AD-06 Module(s): eshop, application Complexity: High Last Updated: 2026-09-29
Business Overview
The Transporter Management system is the central hub for configuring all shipping providers in an Ecommercen installation. It supports 17 courier/logistics providers common in the Greek and Cypriot e-commerce market, each with dedicated API credential management, and provides a three-tier pricing model with geographic availability controls.
Key capabilities:
- Provider lifecycle: Add, edit, activate/deactivate, and reorder shipping providers. Each provider is identified by a
class_nameconstant that links it to its integration code. - Per-provider API settings: Each of the 17 supported providers has a dedicated settings form storing credentials and configuration as key-value pairs in
transporters_settings. - Multi-level pricing: Three independent pricing tiers -- county-level flat rates, postal-code overrides, and weight-based option parameters -- allow fine-grained shipping cost control per country/region.
- Geographic availability: Two-level availability controls (county-level checkboxes and postal-code lists) determine where each transporter can deliver.
- Marketplace mapping: For shops using the Public Marketplace integration, internal transporters can be mapped to standardized marketplace courier codes.
- Voucher generation support: The
has_vouchersflag marks transporters that support automated voucher/label generation via their API. - Cost calculation modes: The
eshop_calculated_costflag determines whether shipping cost is calculated internally (via pricing tables) or externally via the provider's API.
Supported Providers
| class_name | Display Name | Settings Method | Key Credentials |
|---|---|---|---|
GT | Geniki Taxydromiki (v1) | settings_gt | USER, PWD |
GTV2 | Geniki Taxydromiki (v2) | settings_gt_v2 | ENV, USER, PWD, APPKEY, FRMT |
ACS | ACS Courier | settings_acs | CID, CPWD, UID, UPWD, BCODE, APIKEY, PRINT_TYPE, DELIVERY_OPTION_TYPE |
ACSSoap | ACS SOAP | settings_soap | CUS, CPWD, USE, PWD |
ELTA | ELTA Courier | settings_elta | SCD, SSD, SECD, UCD, PS |
SPEEDEX | Speedex Courier | settings_speedex | ENVIRONMENT, USERNAME, PASSWORD, AGREEMENT_ID, CUSTOMER_ID, + 5 more |
CENTER | Center | settings_center | USERALIAS, CREDENTIALVALUE, APIKEY, TEMPLATE |
EASYMAIL | EasyMail | settings_easy_mail | ENVIRONMENT, USERNAME, PASSWORD |
FIS | FIS Courier | settings_fis | WEBSERVICE_CODE, PRINT_TYPE |
BOXNOW | BOX NOW (locker) | settings_box_now | ENVIRONMENT, CLIENTID, CLIENTSECRET, CONTACTNUMBER, CONTACTEMAIL, CONTACTNAME, LOCATIONID, DELIVERY_OPTION_TYPE, USE_TRANSPORTER_WIDGET, DELIVERY_OPTION, PARCEL_SIZE, PAPER_SIZE, LABELS_PER_PAGE |
CYPRUSPOST | Cyprus Post | settings_cyprus_post | APIKEY |
DHL | DHL Express | settings_dhl | ENVIRONMENT, APIKEY, APISECRET, ACCOUNT_NUMBER, + 11 address/signature fields |
TAXYDEMA | Taxydema (v1) | settings_taxydema | PRINT_TYPE, A_PEL_CODE, A_PEL_SUBCODE, A_USER_CODE, A_USER_PASS |
DAILYCOURIER | Daily Courier | settings_daily_courier | BEARER_TOKEN, PRINT_TYPE, POINT_NAME, POINT_ADDRESS, POINT_ZIP_CODE, POINT_PHONE, POINT_EMAIL |
SKROUTZ | Skroutz Last Mile | settings_skroutz | Production/Test API tokens, pickup location codes, map/base URLs, DELIVERY_OPTION_TYPE, USE_TRANSPORTER_WIDGET, PAPER_SIZE |
ASAP | ASAP Delivery | settings_asap | ENVIRONMENT, CLIENT_ID, CLIENT_SECRET, COMPANY_ID, CUTOFF_TIME, CONTACT_NAME, CONTACT_NUMBER |
TAXYDEMAV2 | Taxydema (v2) | settings_taxydema_V2 | USERALIAS, CREDENTIALVALUE, APIKEY, TEMPLATE |
API Reference
Admin Routes (Legacy CI)
All admin routes are under the eshop/transporters_admin controller. Authorization requires AUTH_ROLE_ADVISABLE or AUTH_ROLE_ADMIN.
| Method | Route | Action | Description |
|---|---|---|---|
| GET | eshop/transporters_admin | index() | List all transporters (sorted by sort column) |
| GET | eshop/transporters_admin/add/{provider} | add($provider) | Show add form for a specific provider class |
| POST | eshop/transporters_admin/add/{provider} | add($provider) | Create new transporter |
| GET | eshop/transporters_admin/edit/{id} | edit($id) | Show edit form for transporter |
| POST | eshop/transporters_admin/edit/{id} | edit($id) | Update transporter record + MUI translations |
| GET | eshop/transporters_admin/markActive/{id} | markActive($id) | Activate transporter and redirect |
| GET | eshop/transporters_admin/markInactive/{id} | markInactive($id) | Deactivate transporter and redirect |
| POST (AJAX) | eshop/transporters_admin/updateOrder | updateOrder() | Reorder transporters via drag-and-drop |
| GET/POST | eshop/transporters_admin/settings_{method}/{id} | settings_{method}($id) | View/save provider-specific settings (17 methods) |
| GET | eshop/transporters_admin/pricing/{id} | pricing($id) | View county + postal code pricing |
| POST | eshop/transporters_admin/postPricing/{id} | postPricing($id) | Save county + postal code pricing |
| GET | eshop/transporters_admin/pricingOptions/{id} | pricingOptions($id) | View weight-based pricing options |
| POST | eshop/transporters_admin/postPricingOptions/{id} | postPricingOptions($id) | Save weight-based pricing options |
| GET | eshop/transporters_admin/availabilities/{id} | availabilities($id) | View county + postal code availability |
| POST | eshop/transporters_admin/postAvailabilities/{id} | postAvailabilities($id) | Save county + postal code availability |
| GET/POST | eshop/transporters_admin/public_marketplace_mapping | public_marketplace_mapping() | View/save marketplace transporter mappings |
REST API Endpoints
Guest-accessible endpoints: index, show, item on Transporter (guest reads only). DhlRates and AsapServices are fully guest-accessible. All writes require JWT authentication with AUTH_ROLE_ADMIN or AUTH_ROLE_BACKEND. Each sub-entity supports locale-prefixed routes (e.g., /el/rest/transporter).
Transporter (core entity)
| Method | Path | Action | Description |
|---|---|---|---|
GET | /rest/transporter | index | List transporters (paginated, filterable) |
GET | /rest/transporter/{id} | show | Get transporter by ID |
GET | /rest/transporter/item | item | Get single transporter by filter |
POST | /rest/transporter | store | Create transporter |
POST | /rest/transporter/{id} | update | Update transporter |
DELETE | /rest/transporter/{id} | destroy | Delete transporter (cascades MUI) |
Filters: id (exact), slug (partial), active (exact), hasVouchers (exact), sendWeight (exact), eshopCalculatedCost (exact), name.{locale} (partial, MUI join) Sorts: id, slug, active, sort, name.{locale}Relations (via ?with=): translations, settings, pricing, optionPricing, postAvailabilities, postPricing, countyAvailabilities, publicMappings — scope-gated (#614): Transporter::class declares a relations allow-list in application/config/rest_policies.php. A backend caller gets all eight. A customer or public (guest) caller — including the guest reads on index/show/item above — gets translations only; the other seven (settings, pricing, optionPricing, postAvailabilities, postPricing, countyAvailabilities, publicMappings) are silently stripped from ?with= before the repository loads them, so those keys are absent from the response body, not present-and-empty. Each of the seven is itself a backend + AUTH_ROLE_ADMIN endpoint (see the sub-resource tables below), so the allow-list is defence-in-depth on top of that policy, not a new permission.
Setting
| Method | Path | Action | Description |
|---|---|---|---|
GET | /rest/transporter/setting | index | List settings (paginated) |
GET | /rest/transporter/setting/item | item | Get single setting by filter |
POST | /rest/transporter/setting | store | Create setting |
DELETE | /rest/transporter/setting | destroy | Delete setting |
Filters: transporterId (exact), regKey (partial), regValue (partial) Sorts: transporterId, regKeyRelations: transporter (BELONGS_TO)
regKey and regValue — the courier integration credentials, e.g. PASSWORD, CLIENTSECRET, APIKEY — are serialized only when the caller's ResourceContext::isBackend() (Advisable\Rest\Transporter\Resources\Setting\Resource). A customer or guest/public caller gets transporterId alone, whether hitting this endpoint directly or reaching a setting row through ?with=settings on Transporter above (#614).
Pricing (county-level)
| Method | Path | Action | Description |
|---|---|---|---|
GET | /rest/transporter/pricing | index | List pricing entries |
GET | /rest/transporter/pricing/item | item | Get single pricing entry |
POST | /rest/transporter/pricing | store | Create pricing entry |
DELETE | /rest/transporter/pricing | destroy | Delete pricing entry |
Filters: transporterId (exact), countryAlpha2 (exact), countyAlpha (exact) Sorts: transporterId, countryAlpha2, countyAlphaRelations: transporter (BELONGS_TO)
Option Pricing (weight-based parameters)
| Method | Path | Action | Description |
|---|---|---|---|
GET | /rest/transporter/option-pricing | index | List option pricing entries |
GET | /rest/transporter/option-pricing/item | item | Get single entry |
POST | /rest/transporter/option-pricing | store | Create entry |
DELETE | /rest/transporter/option-pricing | destroy | Delete entry |
Filters: transporterId (exact), countryAlpha2 (exact), regKey (partial) Sorts: transporterId, countryAlpha2, regKeyRelations: transporter (BELONGS_TO)
Post Availability (postal-code level)
| Method | Path | Action | Description |
|---|---|---|---|
GET | /rest/transporter/post-availability | index | List postal code availability |
GET | /rest/transporter/post-availability/item | item | Get single entry |
POST | /rest/transporter/post-availability | store | Create entry |
DELETE | /rest/transporter/post-availability | destroy | Delete entry |
Filters: transporterId (exact), countryAlpha2 (exact), post (exact) Sorts: transporterId, countryAlpha2, postRelations: transporter (BELONGS_TO)
Post Pricing (postal-code level)
| Method | Path | Action | Description |
|---|---|---|---|
GET | /rest/transporter/post-pricing | index | List postal code pricing |
GET | /rest/transporter/post-pricing/item | item | Get single entry |
POST | /rest/transporter/post-pricing | store | Create entry |
DELETE | /rest/transporter/post-pricing | destroy | Delete entry |
Filters: transporterId (exact), countryAlpha2 (exact), post (exact) Sorts: transporterId, countryAlpha2, postRelations: transporter (BELONGS_TO)
County Availability
| Method | Path | Action | Description |
|---|---|---|---|
GET | /rest/transporter/county-availability | index | List county availability |
GET | /rest/transporter/county-availability/item | item | Get single entry |
POST | /rest/transporter/county-availability | store | Create entry |
DELETE | /rest/transporter/county-availability | destroy | Delete entry |
Filters: transporterId (exact), countryAlpha2 (exact), countyAlpha (exact) Sorts: transporterId, countryAlpha2, countyAlphaRelations: transporter (BELONGS_TO)
Public Mapping
| Method | Path | Action | Description |
|---|---|---|---|
GET | /rest/transporter/public-mapping | index | List public mappings |
GET | /rest/transporter/public-mapping/item | item | Get single mapping |
POST | /rest/transporter/public-mapping | store | Create mapping |
DELETE | /rest/transporter/public-mapping | destroy | Delete mapping |
Filters: transporterId (exact), code (partial) Sorts: transporterId, codeRelations: transporter (BELONGS_TO)
SmartPoint Catalog — guest-accessible live pickup-point catalog
| Endpoint | Action | Description |
|---|---|---|
GET /rest/transporter/{id}/smart-point | index | Live pickup-point catalog for one transporter. No sorts/relations; four optional query keys (below). Aggregates external provider APIs via Advisable\Domains\Transporter\SmartPointCatalog\Service. |
Optional query keys (src/Rest/Transporter/Controllers/SmartPoint.php:56-77, parsed by src/Domains/Transporter/SmartPointCatalog/CatalogFilter.php:55-99):
| Key | Meaning |
|---|---|
country | ISO 3166-1 alpha-2 code; only points in that country |
near | "lat,lng"; only points within radius of it, returned distance-ascending |
radius | Metres, default 25000; ignored without near |
limit | Maximum number of points, applied after filtering and sorting |
- Auth: guest (matches legacy
Adv_order::smartPointsInitialize()exposure) - Controller:
src/Rest/Transporter/Controllers/SmartPoint.php— extends base class directly (noHandlesRestfulActions; no repository) - HTTP envelopes: 200 (success), 400 (a present query key does not parse —
invalid_query_paramswitherror.key,SmartPoint.php:98-100), 404 (unknown/inactive transporter), 409 (smart points disabled — 2 reasons:no_provider,not_enabled_for_provider), 502 (upstream API failure) - Caching: the normalised catalog is cached 600 s per transporter under
smart-point-catalog:{id}; the query filters are applied after the cache (src/Domains/Transporter/SmartPointCatalog/Service.php:21,68-85,98-125) - Note: distinct from
GET /rest/order/smart-point(per-order saved row,auth=backend)
DHL Rates — guest-accessible external rate query
| Endpoint | Action | Description |
|---|---|---|
GET /rest/transporter/{id}/dhl-rates | index | Query DHL's live rates API for a shipment. Required query params: destinationCountryCode, weight. Optional: destinationPostalCode. Returns paginated rates. |
- Auth: guest
- Controller:
src/Rest/Transporter/Controllers/DhlRates.php - Routed via
application/config/rest_routes.php:1057-1058, DI atsrc/Rest/Transporter/container.php:76-77 - Policy:
application/config/rest_policies.php:851-852
ASAP Services — guest-accessible service availability and pricing
| Endpoint | Action | Description |
|---|---|---|
GET /rest/transporter/{id}/asap-services | index | Query ASAP's live services and pricing for a delivery. Required query params: country, weight, address, city. Optional: postalCode, county. Returns available service tiers and costs. |
- Auth: guest
- Controller:
src/Rest/Transporter/Controllers/AsapServices.php - Routed via
application/config/rest_routes.php:1059-1060, DI atsrc/Rest/Transporter/container.php:78-80 - Policy:
application/config/rest_policies.php:853-854
Total REST endpoints: 37 (6 parent + 4x7 sub-entities + 1 SmartPoint + 1 DHL Rates + 1 ASAP Services = 37)
Code Flow
Transporter CRUD (Admin)
Admin visits eshop/transporters_admin
|
+--> __construct(): auth check (AUTH_ROLE_ADVISABLE | AUTH_ROLE_ADMIN)
| load transporters_helper, transporters_model
|
+--> index(): model->getAdminList() joins transporters + transporters_mui
| renders sortable list with jQuery drag-and-drop
|
+--> add($provider):
| ├── GET: render create form with class_name = $provider
| └── POST: validation() -> form_validation->run()
| ├── Build $data: active, slug, has_vouchers, send_weight, class_name, eshop_calculated_cost
| ├── Build $dataMui: name per language
| ├── beforeAddData() hook (extensible in subclasses)
| └── model->addRecord($data, $dataMui) -> INSERT transporters + transporters_mui
|
+--> edit($id):
| ├── model->getAdminRecord($id) -> master + captions
| └── POST: validation() -> form_validation->run()
| ├── Build $data (same as add minus class_name)
| ├── Build $dataMui per language
| ├── beforeUpdateData() hook
| └── model->updateRecord() -> UPDATE transporters + UPSERT transporters_mui
|
+--> markActive/markInactive($id): model->update active=true/false -> redirect
|
+--> updateOrder(): AJAX, receives listItem[] -> updates sort column per IDProvider Settings Flow
Admin clicks "Settings" for a transporter
|
+--> settings_{provider}($transporterId):
|
├── Verify transporter exists AND class_name matches expected value
|
├── Load provider-specific validation rules (validateSettings{PROVIDER}())
|
├── POST: form_validation->run()
| └── model->save{Provider}Settings($transporterId, $post)
| ├── DELETE all from transporters_settings WHERE transporter_id
| └── INSERT BATCH new key-value pairs
|
└── GET: Render settings form
└── new {Provider}Config(model->settings($transporterId))
Config class extends BaseTransporterConfig
Uses filterObject() to map DB records to typed propertiesBoxNow settings render a DELIVERY_OPTION checkbox at application/views/admin/transporters/settings/boxNowSettings.php:93-107, pre-checked from $settings->deliveryOption.
BoxNow settings also render PAPER_SIZE and LABELS_PER_PAGE dropdowns (boxNowSettings.php:123-156). These carry in_list validation — PAPER_SIZE as in_list[A4,A6] and LABELS_PER_PAGE as in_list[1,2,4] (ecommercen/eshop/controllers/Adv_transporters_admin.php:1261-1262) — the ONLY two rules in validateSettingsBOXNOW() that can fail. saveBOXNOWSettings() runs only when validation passes, so a rejected value discards the WHOLE submit, client id and secret included. Both dropdowns print form_error() for exactly that reason (boxNowSettings.php:134, :151).
Shipping Cost Calculation (Frontend Integration)
Customer selects shipping at checkout
|
+--> Transporters library (AdvTransporters)
|
├── getAvailable($country, $county, $postalCode, $orderAmount, $cartItems)
| ├── TransporterAvailability checks county + postal code tables
| └── Skips eshop-calculated transporters with no resolvable price
| (canResolveTransportCost() check; if fails, transporter omitted)
|
+--> transportCost($transporterId, $country, $county, $postalCode, $weight, $totalWithVat)
|
├── if !eshop_calculated_cost:
| └── calculateCostExternally() via provider API (CYPRUSPOST, DHL, ASAP)
| └── THROWS TransporterPriceUnavailableException if unresolvable
|
├── Check postal code override pricing (transporters_posts_pricing)
| └── If postal-code row exists: use it, ignoring county base price
|
├── Get base price from county pricing (transporters_pricing)
|
├── Get options: weightLimit, pricePerKg, transferCostLimit
| └── from transporters_options_pricing
|
├── if totalWithVat > transferCostLimit AND weight < weightLimit:
| └── Free shipping (return 0)
|
├── if weight > weightLimit:
| └── price += pricePerKg * ceil((weight - weightLimit) / 1000)
|
└── Return final price#558 Coverage: getAvailable() now calls isOfferable() (ecommercen/libraries/AdvTransporters.php:62-79) to check whether an eshop-calculated transporter can resolve a cost; if not, the transporter is dropped from the available list. transportCost() THROWS TransporterPriceUnavailableException (defined at ecommercen/core/exceptions/TransporterPriceUnavailableException.php:19) instead of returning a null price if cost is unresolvable, preventing silent fallback-to-free shipping. A third client extension seam, canResolveTransportCost() (ecommercen/libraries/AdvTransporters.php:90-93), lets clients customize the "is this transporter available?" logic without redefining the whole calculator.
REST parity (#568): Advisable\Domains\Checkout\ShippingCalculator::calculate() (src/Domains/Checkout/ShippingCalculator.php) is now a branch-for-branch port of this same transportCost() — including the DELIVERY_COST/DELIVERY_COST_MIN_FREE cash-on-delivery surcharge and the TRANS_FREE_ALL flag, both of which this simplified sketch omits — so POST /rest/checkout/shipping, POST /rest/checkout/totals, and POST /rest/checkout/place-order price a cart identically to the legacy storefront. Previously the REST calculator ignored $cartTotal entirely and always charged the raw pricing-row cost. See CF-06 Order Preview for the full breakdown, and the security note below on availableTransporters[].options.
Marketplace Mapping Flow
Admin visits public_marketplace_mapping
|
├── Guard: registry->value('PUBLIC_MARKETPLACE', 'ENABLED') required
|
├── Load Transporters library + TransporterData
|
├── Fetch: active transporters (internal) + marketplace transporters (via Factories::publicMarketplace())
|
├── Display mapping form: each internal transporter -> dropdown of marketplace codes
| Special options: "Select transporter" (empty), "None of the rest" (code='none'->'other')
|
└── POST: model->savePublicTransportersMapping($selectedTransporters)
├── Validate: all IDs are int, all codes are string
├── Transaction: DELETE existing for these IDs, INSERT BATCH new mappings
└── Redirect with success flashDomain Layer
The Transporter domain is the largest in the system with 8 sub-entities (plus one non-DDD aggregation service), all under src/Domains/Transporter/. Each sub-entity follows the full DDD pattern: Entity, Repository, RepositoryConfigurator, Service, ListRequest, WriteData, WriteRepository, Validator, and WriteService.
Transporter (parent entity)
- Entity:
Advisable\Domains\Transporter\Transporter\Repository\Entity-- ImplementsFilterTranslationfor MUI-aware filtering. Properties:id,slug,class_name,has_vouchers,send_weight,active,eshop_calculated_cost,sort. - MuiEntity:
...\MuiEntity-- Properties:id,transporter_id,name,lang. - Repository: Table
transporters, withRepositoryConfiguratordefining 8ONE_TO_MANYrelations to all sub-entity repositories viatransporter_idFK. - MuiRepository: Table
transporters_mui, configured withNullRelationConfigurator(no further relations). - WriteService: Transactional create/update/delete. On create: insert transporter row, then batch-insert MUI translations. On update: partial update (null-excluded), then replace all translations. On delete: delete MUI first, then master row.
- WriteData: Accepts both snake_case (
class_name) and camelCase (className) input. Required for create:slug. - MuiWriteData: Required fields:
lang,name. - Validator: Validates translation array (lang is required per entry). Create/update validators are extensible but currently pass-through.
- ListRequest: Allowed filters include all boolean flags plus MUI name filtering per locale. Sorts include
id,slug,active,sort, andname.{locale}.
Setting
- Entity: Composite key (
transporter_id,reg_key). Properties:transporter_id,reg_key,reg_value(nullable). - RepositoryConfigurator:
BELONGS_TOrelation to Transporter. - WriteData: Required:
transporterId,regKey. Optional:regValue.
Pricing
- Entity: Composite key (
transporter_id,country_alpha_2,county_alpha). Properties: those three +cost. - RepositoryConfigurator:
BELONGS_TOTransporter. - WriteData: All four fields required for create.
OptionPricing
- Entity: Composite key (
transporter_id,country_alpha_2,reg_key). Properties: those three +reg_value. - RepositoryConfigurator:
BELONGS_TOTransporter. - WriteData: All four fields required.
- Standard option keys are stored in
transporters_options_pricing— see Data Model below for the full key list, 5-key non-empty gate enforcement, and the REST checkout options-leak fix.
PostAvailability
- Entity: Composite key (
transporter_id,country_alpha_2,post). Three-column entity marking which postal codes a transporter can deliver to. - WriteData: All three fields required.
PostPricing
- Entity: Composite key (
transporter_id,country_alpha_2,post). Same as PostAvailability but addscostfor postal-code-specific pricing overrides. - WriteData: All four fields required.
CountyAvailability
- Entity: Composite key (
transporter_id,country_alpha_2,county_alpha). Marks which counties a transporter serves. - WriteData: All three fields required.
PublicMapping
- Entity: Composite key (
transporter_id,code). Maps internal transporters to standardized marketplace courier codes. - WriteData: Both fields required.
SmartPointCatalog (non-DDD aggregation service)
No Entity, Repository, or ListRequest — this is a thin aggregation layer over src/SmartPoints/Transporters/* providers.
Service::fetch(int $transporterId, ?CatalogFilter $filter = null): SmartPointDTO[](Service.php:37) — applies a 3-stage provider activation gate before calling the external API (see IN-09 Transporter Integrations §Provider activation gating for the full gate logic and HTTP response codes).TransporterSettingsLoader— bridge to legacytransporters_model->settings()viaget_instance()(contained antipattern, acknowledged in method docblock onload()atsrc/Domains/Transporter/SmartPointCatalog/TransporterSettingsLoader.php:7-16).CatalogFilter— value object parsed from the raw query (fromQuery()) and applied to the normalised list after the cache (src/Domains/Transporter/SmartPointCatalog/CatalogFilter.php:55-99,112-139).- Four typed exceptions:
UnknownTransporterException,SmartPointsDisabledException(2 reasons:no_provider,not_enabled_for_provider),TransporterApiException,InvalidCatalogQueryException(src/Domains/Transporter/SmartPointCatalog/Exceptions/). - DI registration:
Servicereceives$providerMapfromconfig_item('smartPoints')and$cache=service('cache.l2')(src/Domains/Transporter/container.php:91-94); REST controller atsrc/Rest/Transporter/container.php:72-73.
Architecture
Two-Layer Architecture
The transporter system spans both legacy and modern layers:
┌─────────────────────────────────────────────────────────────────────┐
│ Admin UI (Legacy CI) │
│ Adv_transporters_admin (1402 lines) │
│ ├── CRUD: index, add, edit, activate/deactivate, reorder │
│ ├── 17 provider settings methods │
│ ├── Pricing: county + postal code │
│ ├── Availability: county + postal code │
│ └── Marketplace mapping │
├─────────────────────────────────────────────────────────────────────┤
│ Legacy Model Layer │
│ Adv_transporters_model (1079 lines) │
│ ├── CRUD on transporters + transporters_mui │
│ ├── 17 save{Provider}Settings() methods (delete+insert pattern) │
│ ├── Pricing save/get for all 3 tiers │
│ └── Availability save/get for counties + postal codes │
├─────────────────────────────────────────────────────────────────────┤
│ Frontend Cost Calculation │
│ AdvTransporters library + TransporterData + TransporterAvailability│
│ + TransporterPricing + TransporterOptions │
│ Used by checkout flow for real-time shipping cost calculation │
├─────────────────────────────────────────────────────────────────────┤
│ Provider Integration (Modern PSR-4) │
│ src/Transporters/{Provider}/ -- 17 providers │
│ ├── {Provider}Config extends BaseTransporterConfig │
│ ├── {Provider}Helper (tracking URLs, API calls) │
│ └── {Provider} (voucher generation, API integration) │
├─────────────────────────────────────────────────────────────────────┤
│ Modern Domain Layer (PSR-4) │
│ src/Domains/Transporter/ -- 8 sub-entities │
│ Full DDD: Entity, Repository, Service, WriteService, Validator │
├─────────────────────────────────────────────────────────────────────┤
│ REST API Layer │
│ src/Rest/Transporter/ -- 11 controllers, 37 endpoints │
│ HandlesRestfulActions + HandlesWriteActions │
│ OpenAPI annotated; guest reads on Transporter index/show/item, │
│ DhlRates, and AsapServices; writes require JWT + AUTH_ROLE_BACKEND │
└─────────────────────────────────────────────────────────────────────┘Provider Config Pattern
Each provider has a Config class extending BaseTransporterConfig:
php
class AcsConfig extends BaseTransporterConfig
{
// Typed properties for each setting
protected $companyId = '';
protected $apiKey = '';
// ...
// Static field configuration for form rendering
public static function getFieldConfiguration(): array { ... }
// Initialize from DB key-value records
protected function initialize(array $settings): void
{
$this->companyId = $this->filterObject($settings, 'CID');
// ...
}
}BaseTransporterConfig provides:
filterObject()-- extracts a value from an array of{reg_key, reg_value}objectscreateField()-- helper to build field configuration arrays (name, label, type, required, options, etc.)- Field type constants:
TYPE_TEXT,TYPE_PASSWORD,TYPE_NUMBER,TYPE_CHECKBOX,TYPE_SELECT
DI Container Registration
Domain layer (src/Domains/Transporter/container.php): Registers all 8 sub-entities' Repositories, RepositoryConfigurators, Services, WriteRepositories, Validators, and WriteServices. The parent Transporter entity's MuiRepository uses a NullRelationConfigurator since translations have no further relations.
REST layer (src/Rest/Transporter/container.php): Registers all 9 REST controllers, each wired with its domain Service, Resource, Collection, ListRequest, and WriteService.
SmartPointCatalog: TransporterSettingsLoader autowired; Service receives $providerMap via config_item('smartPoints') and $cache via service('cache.l2') (src/Domains/Transporter/container.php:91-94). REST controller registered at src/Rest/Transporter/container.php:72-73. TrackingUrl\Resolver is also registered (src/Domains/Transporter/container.php:109-112).
Both containers are registered in application/config/container/modules.php.
Settings Save Pattern (Delete-and-Reinsert)
All 17 provider settings methods in the legacy model follow the same pattern:
php
protected function saveSettings($transporterId, $data)
{
$this->db->delete($this->tableSettings, ['transporter_id' => $transporterId]);
if ($data) {
$this->db->insert_batch($this->tableSettings, $data);
}
}This atomic delete-then-insert approach means every save operation replaces ALL settings for a transporter. The individual save{Provider}Settings() methods simply build the correct array of [transporter_id, reg_key, reg_value] rows and call saveSettings().
Data Model
transporters
| Column | Type | Description |
|---|---|---|
id | int (PK, auto-increment) | Transporter ID |
slug | varchar | URL-safe identifier |
class_name | varchar, nullable | Provider integration class (e.g., ACS, DHL, BOXNOW) |
has_vouchers | int(1) | Whether provider supports automated voucher generation |
send_weight | int(1) | Whether to transmit package weight to provider API |
active | int(1) | Whether transporter is currently enabled |
eshop_calculated_cost | int(11) | 1 = use internal pricing tables, 0 = query provider API |
sort | int | Display order (managed via drag-and-drop) |
transporters_mui
| Column | Type | Description |
|---|---|---|
id | int (PK, auto-increment) | Row ID |
transporter_id | int (FK -> transporters.id) | Parent transporter |
name | varchar | Localized transporter name |
lang | varchar | Language code (e.g., el, en) |
transporters_settings
| Column | Type | Description |
|---|---|---|
transporter_id | int (PK part, FK) | Parent transporter |
reg_key | varchar (PK part) | Setting key (e.g., APIKEY, USERNAME) |
reg_value | text, nullable | Setting value (may contain API secrets) |
Composite PK: (transporter_id, reg_key)
transporters_pricing
| Column | Type | Description |
|---|---|---|
transporter_id | int (PK part, FK) | Parent transporter |
country_alpha_2 | varchar(2) (PK part) | ISO country code |
county_alpha | varchar (PK part) | County/region code |
cost | varchar(255) | Flat shipping cost for this zone |
Composite PK: (transporter_id, country_alpha_2, county_alpha)
transporters_options_pricing
| Column | Type | Description |
|---|---|---|
transporter_id | int (PK part, FK) | Parent transporter |
country_alpha_2 | varchar(2) (PK part) | ISO country code |
reg_key | varchar (PK part) | Option key (see below) |
reg_value | varchar | Option value |
Composite PK: (transporter_id, country_alpha_2, reg_key)
Standard option keys per country:
MIN_ORDER_AMOUNT-- Minimum order value for this pricing tier to applyDELIVERY_COST_MIN_FREE-- Order threshold for free delivery cost (cash-on-delivery waiver)WEIGHT_LIMIT-- Maximum weight (grams) before per-kg surchargePRICE_PER_KG-- Surcharge per kg over weight limitTRANS_COST_LIMIT-- Order value threshold for free transportTRANS_FREE_ALL-- When truthy (1), a cart underWEIGHT_LIMITships free even without clearingTRANS_COST_LIMIT. Rides along in the same admin form but is not part of the five-key non-empty gate below.DELIVERY_COST-- The cash-on-delivery surcharge amount, waived once the cart reachesDELIVERY_COST_MIN_FREE. Also not part of the five-key gate.
The first five must all be non-empty for a country's options to be saved at all (enforced in savePricingOptions()); TRANS_FREE_ALL and DELIVERY_COST ride along once that gate passes.
⚠️ These 7 keys are pricing configuration, not selectable shipping extras, even though they live in the same table as genuine shop-defined options (e.g. insurance, Saturday delivery). Before #568, POST /rest/checkout/shipping serialized every row in this table onto the wire as a tickable {id, name, extraCost} option — a headless client could see and select TRANS_COST_LIMIT, WEIGHT_LIMIT, PRICE_PER_KG, TRANS_FREE_ALL, DELIVERY_COST, DELIVERY_COST_MIN_FREE, and MIN_ORDER_AMOUNT as if they were purchasable add-ons. Advisable\Domains\Checkout\ShippingCalculator::extractSelectableOptions() (src/Domains/Checkout/ShippingCalculator.php:518-537) now denylists these 7 keys (PRICING_CONTROL_KEYS, :77-85) so only genuine extras reach the customer-facing options array; the admin-facing GET /rest/transporter/option-pricing REST resource is unaffected and still returns every row, as intended for admin tooling.
transporters_posts_pricing
| Column | Type | Description |
|---|---|---|
transporter_id | int (PK part, FK) | Parent transporter |
country_alpha_2 | varchar(2) (PK part) | ISO country code |
post | varchar (PK part) | Postal code |
cost | varchar(255) | Shipping cost override for this postal code |
Composite PK: (transporter_id, country_alpha_2, post)
transporters_counties_availabilities
| Column | Type | Description |
|---|---|---|
transporter_id | int (PK part, FK) | Parent transporter |
country_alpha_2 | varchar(2) | ISO country code |
county_alpha | varchar (PK part) | County/region code |
Composite PK: (transporter_id, county_alpha)
transporters_posts_availabilities
| Column | Type | Description |
|---|---|---|
transporter_id | int (PK part, FK) | Parent transporter |
country_alpha_2 | varchar(2) (PK part) | ISO country code |
post | varchar (PK part) | Postal code |
Composite PK: (transporter_id, country_alpha_2, post)
public_transporters_mapping
| Column | Type | Description |
|---|---|---|
transporter_id | int (PK part, FK) | Parent transporter |
code | varchar (PK part) | Marketplace standard courier code |
Composite PK: (transporter_id, code)
Configuration
Constants
php
// application/config/constants.php
define('TRANSPORTER_CLASSES', [
'GT', 'GTV2', 'ACS', 'ACSSoap', 'ELTA', 'SPEEDEX', 'CENTER',
'EASYMAIL', 'FIS', 'BOXNOW', 'CYPRUSPOST', 'DHL', 'TAXYDEMA',
'DAILYCOURIER', 'SKROUTZ', 'ASAP', 'TAXYDEMAV2'
]);This constant is checked by the hasProviderSettings() helper to determine if a transporter has a dedicated settings page.
Registry Keys
| Group | Key | Purpose |
|---|---|---|
PUBLIC_MARKETPLACE | ENABLED | Enables the marketplace mapping feature; controls visibility of public_marketplace_mapping admin page |
Provider-Specific Settings Keys
Settings are stored as key-value rows in transporters_settings. The exact set of keys depends on the provider (see the "Supported Providers" table above for the full list per provider). Some cross-cutting settings used by the checkout flow:
DELIVERY_OPTION_TYPE-- Controls delivery mode:0= all,1= home delivery,2= pickup point only. Used by ACS, BoxNow, Skroutz.USE_TRANSPORTER_WIDGET-- Boolean flag for providers that offer a frontend widget for point selection (BoxNow, Skroutz). The flag only affects the legacy storefront's map-vs-list rendering. Since #810 it no longer refuses the REST catalog:Service::fetch()deliberately does not callshouldFetchSmartPoints()(src/Domains/Transporter/SmartPointCatalog/Service.php:59-67), so a widget transporter'sGET /rest/transporter/{id}/smart-pointreturns 200; the oldwidget_only409 reason is gone (src/Domains/Transporter/SmartPointCatalog/Exceptions/SmartPointsDisabledException.php:7-8holds onlyno_providerandnot_enabled_for_provider).DELIVERY_OPTION(BoxNow only) — boolean. When set, allows cash-on-delivery (payWay=delivery) even for a smart-point-only BoxNow transporter (DELIVERY_OPTION_TYPE=2). Saved as atransporters_settingsreg row (ecommercen/eshop/models/Adv_transporters_model.php:505-509), read back viaallSettings()(:119-121) and intoBoxNowConfig::$deliveryOption(src/Transporters/BoxNow/BoxNowConfig.php:23,144). Validated atecommercen/eshop/controllers/Adv_transporters_admin.php:1260. Admin label keyeshop.admin.transporters.settings_overrideDelivery.PAPER_SIZE/LABELS_PER_PAGE(BoxNow only) — sheet-layout settings for batch label printing (see IN-09 Transporter Integrations for the batch-retrieval API these feed). Rendered viagetBoxNowPaperSizeDropDown()/getBoxNowLabelsPerPageDropDown()(see Helper Functions below); validation covered above. Two-mechanism upgrade-safe default:BoxNowConfig::$paperSize/$labelsPerPageare declared with the helper defaultsA4/'4'(src/Transporters/BoxNow/BoxNowConfig.php:17-18, constants atsrc/Transporters/BoxNow/BoxNowHelper.php:9-10), covering a transporter with no stored settings at all (__construct()guardsinitialize()withif ($settings)).initialize()additionally falls back with?:(BoxNowConfig.php:140-143), covering a row that IS stored but empty — a casefilterObject()'s$defaultValueargument cannot reach, since it fires only when the row is absent. Without both mechanisms, an install would render with an empty paper size orperPage: 0. This is a direct extension of Known Issue 5 below, which covers the analogous default-handling gap forDELIVERY_OPTION. Locale keys:eshop.admin.transporters.labelsPerPageis new (ecommercen/language/english/adv_advisable_lang.php:1582, all 8 locales); paper size reuses the existing.paperSizekey (:1581) rather than duplicating it, matching the SKROUTZ settings page.ENVIRONMENT--productionortestingtoggle for providers with sandbox APIs (GTV2, Speedex, EasyMail, BoxNow, DHL, ASAP).
Batch voucher printing. 'BOXNOW' was appended to $config['transportersSupportingBatchVoucherPrint'] (application/config/app.php:501), which is what makes the admin order-list bulk voucher-print action available for BoxNow (AdvPrintVoucher::printBatch() gains a BOXNOW case). The bulk action itself is documented in AD-03 Order Management; AD-06 owns only the PAPER_SIZE/LABELS_PER_PAGE settings and helpers that back it.
DHL-Specific Features
DHL settings include a signature image upload flow unique among providers:
- Admin uploads an image file via the settings form
- The image is temporarily stored, read into memory, and base64-encoded
- The encoded string is saved as the
SIGNATUREIMAGEsetting value - The temporary file is deleted
DHL also supports an isEmpty() method on its config class for checking whether required fields are populated, and maintains EU country code lists for customs handling.
Client Extension Points
Subclass Hooks
The admin controller provides two protected hooks for client repos to customize transporter creation/update:
php
// In a client's application/controllers/eshop/Transporters_admin.php:
class Transporters_admin extends Adv_transporters_admin
{
protected function beforeAddData(array $data, array $dataMui): array
{
// Modify data before insert
return [$data, $dataMui];
}
protected function beforeUpdateData(int $transporterId, array $data, array $dataMui): array
{
// Modify data before update
return [$data, $dataMui];
}
}Custom Domain Extensions
Client repos can override domain services via the custom/ directory:
php
// custom/Domains/Transporter/container.php
$services->set(\Custom\Domains\Transporter\Transporter\Repository\RepositoryConfigurator::class);
$services->alias(
\Advisable\Domains\Transporter\Transporter\Repository\RepositoryConfigurator::class,
\Custom\Domains\Transporter\Transporter\Repository\RepositoryConfigurator::class
);This allows adding custom relations, altering default filters, or injecting additional business logic.
Helper Functions
The transporters_helper.php (loaded via ecommercen/helpers/) provides:
hasProviderSettings($provider)-- Checks ifclass_nameis inTRANSPORTER_CLASSESproviderSettingsEditLink($provider)-- Returns the admin URL for a provider's settings page (switch onclass_name)getLinkForTransferProvider($provider, $gtCode)-- Returns external tracking URL via the provider's Helper class; delegates toAdvisable\Domains\Transporter\TrackingUrl\Resolver::helperFor($class_name), which owns the class_name→helper map, and keeps a FIS slug fallback (ecommercen/helpers/transporters_helper.php:94-97,99-101,src/Domains/Transporter/TrackingUrl/Resolver.php:50-68,81-88). See AD-33 Multi-Carrier TrackinggetMinifiedLinkForTransferProvider($provider)-- Returns shortened tracking URL for emails/SMS; sameResolver::helperFor()delegation (transporters_helper.php:116-119)getOrdersByTransferProviderId($orders)-- Groups orders bytransport_idgetParcelSize()/getParcelSizeDropDown()-- BoxNow parcel size optionsgetBoxNowPaperSizeDropDown()-- Thin wrapper overBoxNowHelper::paperSizes()for the admin paper-size dropdown (ecommercen/helpers/transporters_helper.php:154-159,src/Transporters/BoxNow/BoxNowHelper.php:22-36)getBoxNowLabelsPerPageDropDown()-- Thin wrapper overBoxNowHelper::labelsPerPage()for the admin labels-per-page dropdown (ecommercen/helpers/transporters_helper.php:161-166,src/Transporters/BoxNow/BoxNowHelper.php:22-36)prepareAsapGetCostData()-- Builds ASAP API request payload for cost estimation
Business Rules
Authorization: Only
AUTH_ROLE_ADVISABLEandAUTH_ROLE_ADMINcan access the transporter admin. (ecommercen/eshop/controllers/Adv_transporters_admin.php,__construct())Provider-class_name binding: When adding a transporter, the provider type (
class_name) is passed as a URL parameter and stored immutably. The edit form does not allow changing it. Each settings method validates that the transporter'sclass_namematches the expected value. (ecommercen/eshop/controllers/Adv_transporters_admin.php,add()/settings_*())Settings replace pattern: Saving provider settings deletes all existing settings for that transporter and re-inserts the full set. This means settings cannot be partially updated through the legacy admin -- only through the REST API's individual setting endpoints. (
ecommercen/eshop/models/Adv_transporters_model.php,saveSettings())Pricing tier priority: During checkout cost calculation, postal-code pricing (
transporters_posts_pricing) takes precedence over county-level pricing (transporters_pricing). A free (0) postal-code price yields0for a normal-weight cart, and the per-kg overweight surcharge ALONE (never the county base rate) for an overweight cart — the county row never reapplies once a postal row has matched. (ecommercen/libraries/AdvTransporters.php,transportCost(),:107-208; ported to REST inAdvisable\Domains\Checkout\ShippingCalculator::resolveChargedCost(), #568, see theresolveChargedCost()method docblock atsrc/Domains/Checkout/ShippingCalculator.php:265-280for why legacy's deadcheckPostalCode() === 0guard is deliberately NOT reproduced verbatim)Weight surcharge: If package weight exceeds the
WEIGHT_LIMIToption for the country, extra cost is calculated asceil((weight - weightLimit) / 1000) * PRICE_PER_KG. This is added to the base county price, or charged ALONE instead of the base price if the order exceedsTRANS_COST_LIMIT(the$checkFreereplace-vs-add distinction). (ecommercen/libraries/AdvTransporters.php,transportCost(),:107-208; ported to REST inShippingCalculator::resolveChargedCost(), #568)Free shipping logic: Shipping is free when the order total exceeds
TRANS_COST_LIMITAND weight is withinWEIGHT_LIMIT, or unconditionally belowWEIGHT_LIMITwhenTRANS_FREE_ALLis set. If weight exceeds the limit, only the overweight surcharge applies. Cash-on-delivery additionally charges a separateDELIVERY_COSTsurcharge (waived atDELIVERY_COST_MIN_FREE) that is never folded into the shipping cost itself. (ecommercen/libraries/AdvTransporters.php,transportCost():107-208/deliveryCost(), ported to REST inShippingCalculator, #568 — REST previously ignored the cart total entirely and always charged the raw pricing-row cost, overcharging carts aboveTRANS_COST_LIMITand undercharging the shop for carts aboveWEIGHT_LIMIT)External cost calculation: When
eshop_calculated_cost = 0, the system attempts to calculate cost via the provider's API (currently supported for CYPRUSPOST, DHL, and ASAP; ASAP returns0— frontend-handled). If the external call fails, it falls back to internal pricing tables. Uses SY-26 Circuit Breaker pattern for resilient API calls. (ecommercen/libraries/AdvTransporters.php,calculateCostExternally(),:378-398)Pricing options validation: All 5 option keys (
MIN_ORDER_AMOUNT,DELIVERY_COST_MIN_FREE,WEIGHT_LIMIT,PRICE_PER_KG,TRANS_COST_LIMIT) must be non-empty for a country's options to be saved. Partially filled options are silently discarded. (ecommercen/eshop/models/Adv_transporters_model.php,savePricingOptions())Marketplace mapping guards: The
public_marketplace_mappingpage is only accessible whenPUBLIC_MARKETPLACE.ENABLEDis set in the registry. The "none" UI option maps to theothercode in the database. (ecommercen/eshop/controllers/Adv_transporters_admin.php,public_marketplace_mapping())Postal code sanitization: When saving postal code availability and pricing, whitespace is stripped via
preg_replace('/\s+/', '', $post). Empty postal codes after sanitization are skipped. (ecommercen/eshop/models/Adv_transporters_model.php)Smart point transporters: Providers with
DELIVERY_OPTION_TYPE = 2are classified as "smart point only" transporters (e.g., BoxNow, Skroutz with pickup points). ThegetIsSmartPointOnlyTransporter()method checks this flag. (ecommercen/libraries/AdvTransporters.php,getIsSmartPointOnlyTransporter(),:354-361)BoxNow cash-on-delivery override. A smart-point-only BoxNow transporter (
DELIVERY_OPTION_TYPE = 2) normally has thedelivery(COD) pay way stripped at checkout. Setting the per-transporterDELIVERY_OPTIONflag (admin checkbox, label keyeshop.admin.transporters.settings_overrideDelivery) keepsdeliveryavailable for that transporter. Default off. Enforced storefront-side atassets/vue/mixins/checkoutPage.js:458and:484(gated ongetIsSmartPointOnlyTransporter && !getSelectedTransporterDeliveryOption). The flag reaches the storefront viaTransporters::protectData()(ecommercen/libraries/AdvTransporters.php:309-338,DELIVERY_OPTION → deliveryOption, bool-cast, entry at:323-326) and thegetSelectedTransporterDeliveryOptionVuex getter (assets/vue/store/getters.js:409-422). For the full client-side enforcement logic (getter implementation,isPayWayOptionAvailable(),setPayWayAfterTransporterUpdate()), see CF-06 Order Preview §Per-TransporterDELIVERY_OPTIONOverride. The legacy server side reads the same rule throughAdvTransporters::isCashOnDeliveryAllowed($transporterId)(ecommercen/libraries/AdvTransporters.php:363-376, #338), a wrapper overCashOnDeliveryPolicy::permits()used byAdv_order::paywayTransporterCheckValidation()(ecommercen/eshop/controllers/Adv_order.php:1493, call at:1508) — so the legacy storefront preview and checkout also refusedeliverywith a smart-point-only transporter whoseDELIVERY_OPTIONis off. The REST calculator publishes the same verdict ascodAllowedin eachShippingCalculator::calculate()result (src/Domains/Checkout/ShippingCalculator.php:255-258, docblock:132-135); its constructor takesCashOnDeliveryPolicy(:113).MUI requirement: Transporter name is required for all configured admin languages. The slug field is also required. These are enforced by CodeIgniter form validation on the legacy admin side only; the REST domain Validator is a pass-through. (
ecommercen/eshop/controllers/Adv_transporters_admin.php,validation(),:1141, :1147-1149)DHL signature image encoding: DHL settings include a unique image upload flow where the signature is base64-encoded and stored as a setting value. Temporary file is deleted after encoding. (
ecommercen/eshop/controllers/Adv_transporters_admin.php,settings_dhl())Client transport-cost extension seam (4.112.0, #389): A client fork customizes transport-cost pricing by subclassing
AdvTransporters(legacyapplication/libraries/Transporters.php extends AdvTransporters) and overriding twoprotectedhooks rather than copy-pasting the wholetransportCost()method.customTransportCost(...): ?float(ecommercen/libraries/AdvTransporters.php:225-234) is a pre-hook called after the postal-code serviceability guard; returning non-null short-circuits the standard free-shipping/overweight logic (e.g. admin postal pricing that bypasses free shipping).applyTransportSurcharge(float $price, ...): float(AdvTransporters.php:249-259) is a post-hook fired once before return on every serviced path — the standard calculation, the free-shipping$price = 0branches, and a non-nullcustomTransportCost()result — so a client can add a carrier/island surcharge even on otherwise-free shipping, and the two hooks compose. Both default to no-ops, so default cost output is byte-identical for shops that don't override. (ecommercen/libraries/AdvTransporters.php,transportCost())Ported to the modern REST calculator (#568):
Advisable\Domains\Checkout\ShippingCalculatorgains the identical pair ofprotectedseams —customTransportCost()(src/Domains/Checkout/ShippingCalculator.php:369-378) andapplyTransportSurcharge()(:393-403) — with the same short-circuit/compose semantics. A client fork on the modern REST checkout should override these on aCustom\…\ShippingCalculatorsubclass aliased incustom/Domains/container.php, instead of redeclaringcalculate()wholesale. Thesmile_v4fork currently redeclares the equivalent legacy method wholesale and is a candidate to migrate to the hooks.Client transporter availability gate seam (#558):
canResolveTransportCost($transporterId, $country, $county, $postalCode, $weight): bool(ecommercen/libraries/AdvTransporters.php:90-93) is a pre-hook ongetAvailable()(ecommercen/libraries/AdvTransporters.php:29-52, filter at:44-51) that gate-keeps whether an eshop-calculated transporter is included in the available list. Defaults totrue(include everything); a client override can returnfalseto dynamically hide a transporter if its required external API (e.g., address validation) fails or rates are unavailable. The hook is consulted beforetransportCost()is called, so it won't block via exception — only via omission from the available set. (Legacy only.)Client pricing-options view-data extension seam (4.115.0, #424): A client fork extends the weight-based pricing-options admin page by subclassing
Adv_transporters_adminand overriding aprotectedno-op hook rather than copying the entire action method.pricingOptionsViewData(int $transporterId, object $transporter): array(ecommercen/eshop/controllers/Adv_transporters_admin.php:1065) is called withinpricingOptions()(Adv_transporters_admin.php:1054), immediately beforedefaultRender(); its returned array is merged into$this->render. The base implementation returns[], allowing clients to inject custom view data (e.g. pricing templates, recommendations, or helptext). Default behavior is a no-op. (ecommercen/eshop/controllers/Adv_transporters_admin.php,pricingOptions())
Known Issues & Security Gaps
[OPEN] Provider map drift: 6 of 17 transporter class names are absent from
$config['smartPoints']—CYPRUSPOST,DAILYCOURIER,ASAP,DHL,TAXYDEMA, andTAXYDEMAV2— and will always return409 no_providerfromGET /rest/transporter/{id}/smart-point(see IN-09 Transporter Integrations §Known Issues for full detail).[OPEN — minor]
TransporterSettingsLoaderusesget_instance()to load the legacytransporters_model(src/Domains/Transporter/SmartPointCatalog/TransporterSettingsLoader.php:19-22). Domain-layer convention break, acknowledged in method docblock onload(). Integration-testable only.[OPEN — minor] Inactive and non-existent transporter IDs both return 404 with
code: unknown_transporter(src/Domains/Transporter/SmartPointCatalog/Service.php:40-42). Callers cannot distinguish "no such ID" from "exists but deactivated".[RESOLVED — #338]
DELIVERY_OPTIONis now covered bytests/Unit/Checkout/CashOnDeliveryPolicyTest.php,ShippingCalculatorTest(codAllowed),PlaceOrderServiceTestandtests/Legacy/Eshop/AdvOrderCashOnDeliveryTransporterTest.php. Original text: Limited test coverage for the standaloneDELIVERY_OPTIONflag (DELIVERY_OPTION_TYPEhas coverage attests/Unit/Checkout/ShippingCalculatorTest.php:40,603,615,637,647, but the BoxNow-onlyDELIVERY_OPTIONoverride that gates COD availability is untested).[OPEN — minor]
saveBOXNOWSettings()writes'reg_value' => $settings['DELIVERY_OPTION'] ?? ''(ecommercen/eshop/models/Adv_transporters_model.php:508) — an unchecked box stores'', not0; harmless (bool-cast downstream) but inconsistent with other boolean settings. (See Configuration → Provider-Specific Settings Keys for the related two-mechanism default fallback added for BoxNow'sPAPER_SIZE/LABELS_PER_PAGEsettings, which covers the analogous "row absent vs. row stored empty" gap for those two keys.)[RESOLVED — #338] The modern layer now reads
DELIVERY_OPTIONthroughAdvisable\Domains\Checkout\CashOnDeliveryPolicy(published ascodAllowedonPOST /rest/checkout/shipping, enforced by REST place-order); the settings rows themselves remain admin-only. Original text:DELIVERY_OPTIONis BoxNow/legacy-only: no modern Transporter REST or domain layer entity is aware ofdeliveryOption. OnlyBoxNowConfig,Transporters::protectData(), and checkout JS interpret it.[NEW]
AdvTransporterOptions::weightLimit()guards onisset(…->options[$country])while its six siblings guard the full key path (ecommercen/libraries/transporters/AdvTransporterOptions.php:20-26vs:12-18, 28-66). A country row set lackingWEIGHT_LIMIT— reachable viaPOST /rest/transporter/option-pricing, which inserts single rows and bypassessavePricingOptions()'s 5-key gate (ecommercen/eshop/models/Adv_transporters_model.php:994-1020) — raisesUndefined array key "WEIGHT_LIMIT"on the checkout path.[NEW] REST create can violate NOT NULL.
Validator::validateForCreate()is empty,WriteData::$slugandMuiWriteData::$nameare nullable (WriteData.php:30,MuiWriteData.php:20), andparseMuiData()usestoArray()with noexcludeNull(WriteService.php:79).POST /rest/transporter {"translations":[{"lang":"el"}]}INSERTsname => nullintotransporters_mui.name varchar(255) NOT NULL, and a body withoutslugINSERTs null intotransporters.slug varchar(50) NOT NULL— while both OA schemas declare themrequired(src/Rest/Transporter/Resources/Transporter/WriteData.php).[NEW] No
FOREIGN KEYconstraint exists on any child table, only plainKEYs (database/initial/initial.sql:2281-2334,:1026-1032), andWriteService::delete()removes MUI rows only (WriteService.php:59-65) — so settings / pricing / availability / mapping rows orphan on transporter delete. The Data Model's "FK -> transporters.id" labels above should read plain index, not FK.[NEW]
transporters_counties_availabilitiesPK omitscountry_alpha_2(database/initial/initial.sql:2285) although the column is written per-country bysaveCountiesAvailabilities()— two countries sharing acounty_alphacollide on insert.[NEW] In
application/views/admin/transporters/pricingOptions.php, everyset_value()first arg namescounty[{$countryCode}][…]while the inputs areoption[{$countryCode}][…], and three additionally hardcodeDELIVERY_COSTfor theWEIGHT_LIMIT/PRICE_PER_KG/TRANS_COST_LIMITinputs (:51,:59,:67) — so post-failure repopulation never works; the DB-value fallback masks it.[NEW — minor] The OpenAPI tag description on the SmartPoint controller still reads "guest-accessible, uncached" (
src/Rest/Transporter/Controllers/SmartPoint.php:31), contradicting the 10-minute per-transporter catalog cache (src/Domains/Transporter/SmartPointCatalog/Service.php:21) and the 200-response text atSmartPoint.php:79.
Tests
Test coverage spans unit and integration layers:
| Test File | Coverage |
|---|---|
tests/Unit/Domains/Transporter/* | 11 test directories covering Entity, Repository, Service, WriteService, Validator, and WriteData for all 8 sub-entities plus SmartPointCatalog |
tests/Integration/Domains/Transporter/* | Integration tests for domain-layer flows and DI container wiring |
tests/Unit/Checkout/ShippingCalculatorTest.php | Legacy-to-modern parity tests for shipping cost calculation; covers DELIVERY_OPTION_TYPE, postal-code priority, overweight surcharge, and free-shipping logic |
Coverage gaps:
- The standalone
DELIVERY_OPTIONBoxNow override (Known Issue 4) - External cost calculation flow (CYPRUSPOST, DHL, ASAP API calls)
- Marketplace mapping admin page and POST handler
- DHL signature image encoding/upload flow
- Postal code sanitization (
preg_replace('/\s+/', '', $post)) - The provider settings delete-and-reinsert pattern for all 17 provider types
Related Flows
Customer Flows
- CF-06 Order Preview -- transporter selection and cost calculation during checkout
- CF-07 Order Confirmation -- transporter details shown in order confirmation
- CF-08 Payment Processing -- payment flow determines SENT vs PAID_SENT status
Admin Flows
- AD-03 Order Management -- voucher generation from order detail page, shipment closure
- AD-18 Store Management -- store pickup as alternative to courier delivery
- AD-34 Voucher Generation -- bulk voucher generation workflow
- AD-33 Multi-Carrier Tracking -- tracking URL resolution via provider helpers
Integration Flows
- IN-04 Public Marketplace -- marketplace integration requiring transporter code mapping
- IN-09 Transporter Integrations -- API-level integration details for voucher generation, tracking, and external cost queries
System Flows
- SY-02 Order Status Emails -- email notifications on shipment status transitions
- SY-20 Geolocation Shipping Zones -- geolocation-based transporter availability
- SY-24 Email Dispatch -- email infrastructure for shipping notifications
- SY-23 MUI Translation Pattern — transporters_mui companion table
- SY-26 Circuit Breaker -- resilient external API calls for cost calculation and voucher generation
- SY-28 Storage Abstraction -- file storage for DHL signature images and voucher PDFs
Wiki Guides: DHL Guide | Circuit Breaker Guide