Skip to content

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_name constant 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_vouchers flag marks transporters that support automated voucher/label generation via their API.
  • Cost calculation modes: The eshop_calculated_cost flag determines whether shipping cost is calculated internally (via pricing tables) or externally via the provider's API.

Supported Providers ​

class_nameDisplay NameSettings MethodKey Credentials
GTGeniki Taxydromiki (v1)settings_gtUSER, PWD
GTV2Geniki Taxydromiki (v2)settings_gt_v2ENV, USER, PWD, APPKEY, FRMT
ACSACS Couriersettings_acsCID, CPWD, UID, UPWD, BCODE, APIKEY, PRINT_TYPE, DELIVERY_OPTION_TYPE
ACSSoapACS SOAPsettings_soapCUS, CPWD, USE, PWD
ELTAELTA Couriersettings_eltaSCD, SSD, SECD, UCD, PS
SPEEDEXSpeedex Couriersettings_speedexENVIRONMENT, USERNAME, PASSWORD, AGREEMENT_ID, CUSTOMER_ID, + 5 more
CENTERCentersettings_centerUSERALIAS, CREDENTIALVALUE, APIKEY, TEMPLATE
EASYMAILEasyMailsettings_easy_mailENVIRONMENT, USERNAME, PASSWORD
FISFIS Couriersettings_fisWEBSERVICE_CODE, PRINT_TYPE
BOXNOWBOX NOW (locker)settings_box_nowENVIRONMENT, CLIENTID, CLIENTSECRET, CONTACTNUMBER, CONTACTEMAIL, CONTACTNAME, LOCATIONID, DELIVERY_OPTION_TYPE, USE_TRANSPORTER_WIDGET, DELIVERY_OPTION, PARCEL_SIZE, PAPER_SIZE, LABELS_PER_PAGE
CYPRUSPOSTCyprus Postsettings_cyprus_postAPIKEY
DHLDHL Expresssettings_dhlENVIRONMENT, APIKEY, APISECRET, ACCOUNT_NUMBER, + 11 address/signature fields
TAXYDEMATaxydema (v1)settings_taxydemaPRINT_TYPE, A_PEL_CODE, A_PEL_SUBCODE, A_USER_CODE, A_USER_PASS
DAILYCOURIERDaily Couriersettings_daily_courierBEARER_TOKEN, PRINT_TYPE, POINT_NAME, POINT_ADDRESS, POINT_ZIP_CODE, POINT_PHONE, POINT_EMAIL
SKROUTZSkroutz Last Milesettings_skroutzProduction/Test API tokens, pickup location codes, map/base URLs, DELIVERY_OPTION_TYPE, USE_TRANSPORTER_WIDGET, PAPER_SIZE
ASAPASAP Deliverysettings_asapENVIRONMENT, CLIENT_ID, CLIENT_SECRET, COMPANY_ID, CUTOFF_TIME, CONTACT_NAME, CONTACT_NUMBER
TAXYDEMAV2Taxydema (v2)settings_taxydema_V2USERALIAS, 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.

MethodRouteActionDescription
GETeshop/transporters_adminindex()List all transporters (sorted by sort column)
GETeshop/transporters_admin/add/{provider}add($provider)Show add form for a specific provider class
POSTeshop/transporters_admin/add/{provider}add($provider)Create new transporter
GETeshop/transporters_admin/edit/{id}edit($id)Show edit form for transporter
POSTeshop/transporters_admin/edit/{id}edit($id)Update transporter record + MUI translations
GETeshop/transporters_admin/markActive/{id}markActive($id)Activate transporter and redirect
GETeshop/transporters_admin/markInactive/{id}markInactive($id)Deactivate transporter and redirect
POST (AJAX)eshop/transporters_admin/updateOrderupdateOrder()Reorder transporters via drag-and-drop
GET/POSTeshop/transporters_admin/settings_{method}/{id}settings_{method}($id)View/save provider-specific settings (17 methods)
GETeshop/transporters_admin/pricing/{id}pricing($id)View county + postal code pricing
POSTeshop/transporters_admin/postPricing/{id}postPricing($id)Save county + postal code pricing
GETeshop/transporters_admin/pricingOptions/{id}pricingOptions($id)View weight-based pricing options
POSTeshop/transporters_admin/postPricingOptions/{id}postPricingOptions($id)Save weight-based pricing options
GETeshop/transporters_admin/availabilities/{id}availabilities($id)View county + postal code availability
POSTeshop/transporters_admin/postAvailabilities/{id}postAvailabilities($id)Save county + postal code availability
GET/POSTeshop/transporters_admin/public_marketplace_mappingpublic_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) ​

MethodPathActionDescription
GET/rest/transporterindexList transporters (paginated, filterable)
GET/rest/transporter/{id}showGet transporter by ID
GET/rest/transporter/itemitemGet single transporter by filter
POST/rest/transporterstoreCreate transporter
POST/rest/transporter/{id}updateUpdate transporter
DELETE/rest/transporter/{id}destroyDelete 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 ​

MethodPathActionDescription
GET/rest/transporter/settingindexList settings (paginated)
GET/rest/transporter/setting/itemitemGet single setting by filter
POST/rest/transporter/settingstoreCreate setting
DELETE/rest/transporter/settingdestroyDelete 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) ​

MethodPathActionDescription
GET/rest/transporter/pricingindexList pricing entries
GET/rest/transporter/pricing/itemitemGet single pricing entry
POST/rest/transporter/pricingstoreCreate pricing entry
DELETE/rest/transporter/pricingdestroyDelete pricing entry

Filters: transporterId (exact), countryAlpha2 (exact), countyAlpha (exact) Sorts: transporterId, countryAlpha2, countyAlphaRelations: transporter (BELONGS_TO)

Option Pricing (weight-based parameters) ​

MethodPathActionDescription
GET/rest/transporter/option-pricingindexList option pricing entries
GET/rest/transporter/option-pricing/itemitemGet single entry
POST/rest/transporter/option-pricingstoreCreate entry
DELETE/rest/transporter/option-pricingdestroyDelete entry

Filters: transporterId (exact), countryAlpha2 (exact), regKey (partial) Sorts: transporterId, countryAlpha2, regKeyRelations: transporter (BELONGS_TO)

Post Availability (postal-code level) ​

MethodPathActionDescription
GET/rest/transporter/post-availabilityindexList postal code availability
GET/rest/transporter/post-availability/itemitemGet single entry
POST/rest/transporter/post-availabilitystoreCreate entry
DELETE/rest/transporter/post-availabilitydestroyDelete entry

Filters: transporterId (exact), countryAlpha2 (exact), post (exact) Sorts: transporterId, countryAlpha2, postRelations: transporter (BELONGS_TO)

Post Pricing (postal-code level) ​

MethodPathActionDescription
GET/rest/transporter/post-pricingindexList postal code pricing
GET/rest/transporter/post-pricing/itemitemGet single entry
POST/rest/transporter/post-pricingstoreCreate entry
DELETE/rest/transporter/post-pricingdestroyDelete entry

Filters: transporterId (exact), countryAlpha2 (exact), post (exact) Sorts: transporterId, countryAlpha2, postRelations: transporter (BELONGS_TO)

County Availability ​

MethodPathActionDescription
GET/rest/transporter/county-availabilityindexList county availability
GET/rest/transporter/county-availability/itemitemGet single entry
POST/rest/transporter/county-availabilitystoreCreate entry
DELETE/rest/transporter/county-availabilitydestroyDelete entry

Filters: transporterId (exact), countryAlpha2 (exact), countyAlpha (exact) Sorts: transporterId, countryAlpha2, countyAlphaRelations: transporter (BELONGS_TO)

Public Mapping ​

MethodPathActionDescription
GET/rest/transporter/public-mappingindexList public mappings
GET/rest/transporter/public-mapping/itemitemGet single mapping
POST/rest/transporter/public-mappingstoreCreate mapping
DELETE/rest/transporter/public-mappingdestroyDelete mapping

Filters: transporterId (exact), code (partial) Sorts: transporterId, codeRelations: transporter (BELONGS_TO)

SmartPoint Catalog — guest-accessible live pickup-point catalog ​

EndpointActionDescription
GET /rest/transporter/{id}/smart-pointindexLive 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):

KeyMeaning
countryISO 3166-1 alpha-2 code; only points in that country
near"lat,lng"; only points within radius of it, returned distance-ascending
radiusMetres, default 25000; ignored without near
limitMaximum 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 (no HandlesRestfulActions; no repository)
  • HTTP envelopes: 200 (success), 400 (a present query key does not parse — invalid_query_params with error.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 ​

EndpointActionDescription
GET /rest/transporter/{id}/dhl-ratesindexQuery 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 at src/Rest/Transporter/container.php:76-77
  • Policy: application/config/rest_policies.php:851-852

ASAP Services — guest-accessible service availability and pricing ​

EndpointActionDescription
GET /rest/transporter/{id}/asap-servicesindexQuery 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 at src/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 ID

Provider 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 properties

BoxNow 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 flash

Domain 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 -- Implements FilterTranslation for 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, with RepositoryConfigurator defining 8 ONE_TO_MANY relations to all sub-entity repositories via transporter_id FK.
  • MuiRepository: Table transporters_mui, configured with NullRelationConfigurator (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, and name.{locale}.

Setting ​

  • Entity: Composite key (transporter_id, reg_key). Properties: transporter_id, reg_key, reg_value (nullable).
  • RepositoryConfigurator: BELONGS_TO relation 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_TO Transporter.
  • 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_TO Transporter.
  • 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 adds cost for 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 legacy transporters_model->settings() via get_instance() (contained antipattern, acknowledged in method docblock on load() at src/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: Service receives $providerMap from config_item('smartPoints') and $cache = service('cache.l2') (src/Domains/Transporter/container.php:91-94); REST controller at src/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} objects
  • createField() -- 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 ​

ColumnTypeDescription
idint (PK, auto-increment)Transporter ID
slugvarcharURL-safe identifier
class_namevarchar, nullableProvider integration class (e.g., ACS, DHL, BOXNOW)
has_vouchersint(1)Whether provider supports automated voucher generation
send_weightint(1)Whether to transmit package weight to provider API
activeint(1)Whether transporter is currently enabled
eshop_calculated_costint(11)1 = use internal pricing tables, 0 = query provider API
sortintDisplay order (managed via drag-and-drop)

transporters_mui ​

ColumnTypeDescription
idint (PK, auto-increment)Row ID
transporter_idint (FK -> transporters.id)Parent transporter
namevarcharLocalized transporter name
langvarcharLanguage code (e.g., el, en)

transporters_settings ​

ColumnTypeDescription
transporter_idint (PK part, FK)Parent transporter
reg_keyvarchar (PK part)Setting key (e.g., APIKEY, USERNAME)
reg_valuetext, nullableSetting value (may contain API secrets)

Composite PK: (transporter_id, reg_key)

transporters_pricing ​

ColumnTypeDescription
transporter_idint (PK part, FK)Parent transporter
country_alpha_2varchar(2) (PK part)ISO country code
county_alphavarchar (PK part)County/region code
costvarchar(255)Flat shipping cost for this zone

Composite PK: (transporter_id, country_alpha_2, county_alpha)

transporters_options_pricing ​

ColumnTypeDescription
transporter_idint (PK part, FK)Parent transporter
country_alpha_2varchar(2) (PK part)ISO country code
reg_keyvarchar (PK part)Option key (see below)
reg_valuevarcharOption 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 apply
  • DELIVERY_COST_MIN_FREE -- Order threshold for free delivery cost (cash-on-delivery waiver)
  • WEIGHT_LIMIT -- Maximum weight (grams) before per-kg surcharge
  • PRICE_PER_KG -- Surcharge per kg over weight limit
  • TRANS_COST_LIMIT -- Order value threshold for free transport
  • TRANS_FREE_ALL -- When truthy (1), a cart under WEIGHT_LIMIT ships free even without clearing TRANS_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 reaches DELIVERY_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 ​

ColumnTypeDescription
transporter_idint (PK part, FK)Parent transporter
country_alpha_2varchar(2) (PK part)ISO country code
postvarchar (PK part)Postal code
costvarchar(255)Shipping cost override for this postal code

Composite PK: (transporter_id, country_alpha_2, post)

transporters_counties_availabilities ​

ColumnTypeDescription
transporter_idint (PK part, FK)Parent transporter
country_alpha_2varchar(2)ISO country code
county_alphavarchar (PK part)County/region code

Composite PK: (transporter_id, county_alpha)

transporters_posts_availabilities ​

ColumnTypeDescription
transporter_idint (PK part, FK)Parent transporter
country_alpha_2varchar(2) (PK part)ISO country code
postvarchar (PK part)Postal code

Composite PK: (transporter_id, country_alpha_2, post)

public_transporters_mapping ​

ColumnTypeDescription
transporter_idint (PK part, FK)Parent transporter
codevarchar (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 ​

GroupKeyPurpose
PUBLIC_MARKETPLACEENABLEDEnables 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 call shouldFetchSmartPoints() (src/Domains/Transporter/SmartPointCatalog/Service.php:59-67), so a widget transporter's GET /rest/transporter/{id}/smart-point returns 200; the old widget_only 409 reason is gone (src/Domains/Transporter/SmartPointCatalog/Exceptions/SmartPointsDisabledException.php:7-8 holds only no_provider and not_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 a transporters_settings reg row (ecommercen/eshop/models/Adv_transporters_model.php:505-509), read back via allSettings() (:119-121) and into BoxNowConfig::$deliveryOption (src/Transporters/BoxNow/BoxNowConfig.php:23,144). Validated at ecommercen/eshop/controllers/Adv_transporters_admin.php:1260. Admin label key eshop.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 via getBoxNowPaperSizeDropDown() / getBoxNowLabelsPerPageDropDown() (see Helper Functions below); validation covered above. Two-mechanism upgrade-safe default: BoxNowConfig::$paperSize/$labelsPerPage are declared with the helper defaults A4 / '4' (src/Transporters/BoxNow/BoxNowConfig.php:17-18, constants at src/Transporters/BoxNow/BoxNowHelper.php:9-10), covering a transporter with no stored settings at all (__construct() guards initialize() with if ($settings)). initialize() additionally falls back with ?: (BoxNowConfig.php:140-143), covering a row that IS stored but empty — a case filterObject()'s $defaultValue argument cannot reach, since it fires only when the row is absent. Without both mechanisms, an install would render with an empty paper size or perPage: 0. This is a direct extension of Known Issue 5 below, which covers the analogous default-handling gap for DELIVERY_OPTION. Locale keys: eshop.admin.transporters.labelsPerPage is new (ecommercen/language/english/adv_advisable_lang.php:1582, all 8 locales); paper size reuses the existing .paperSize key (:1581) rather than duplicating it, matching the SKROUTZ settings page.
  • ENVIRONMENT -- production or testing toggle 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:

  1. Admin uploads an image file via the settings form
  2. The image is temporarily stored, read into memory, and base64-encoded
  3. The encoded string is saved as the SIGNATUREIMAGE setting value
  4. 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 if class_name is in TRANSPORTER_CLASSES
  • providerSettingsEditLink($provider) -- Returns the admin URL for a provider's settings page (switch on class_name)
  • getLinkForTransferProvider($provider, $gtCode) -- Returns external tracking URL via the provider's Helper class; delegates to Advisable\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 Tracking
  • getMinifiedLinkForTransferProvider($provider) -- Returns shortened tracking URL for emails/SMS; same Resolver::helperFor() delegation (transporters_helper.php:116-119)
  • getOrdersByTransferProviderId($orders) -- Groups orders by transport_id
  • getParcelSize() / getParcelSizeDropDown() -- BoxNow parcel size options
  • getBoxNowPaperSizeDropDown() -- Thin wrapper over BoxNowHelper::paperSizes() for the admin paper-size dropdown (ecommercen/helpers/transporters_helper.php:154-159, src/Transporters/BoxNow/BoxNowHelper.php:22-36)
  • getBoxNowLabelsPerPageDropDown() -- Thin wrapper over BoxNowHelper::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 ​

  1. Authorization: Only AUTH_ROLE_ADVISABLE and AUTH_ROLE_ADMIN can access the transporter admin. (ecommercen/eshop/controllers/Adv_transporters_admin.php, __construct())

  2. 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's class_name matches the expected value. (ecommercen/eshop/controllers/Adv_transporters_admin.php, add() / settings_*())

  3. 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())

  4. 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 yields 0 for 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 in Advisable\Domains\Checkout\ShippingCalculator::resolveChargedCost(), #568, see the resolveChargedCost() method docblock at src/Domains/Checkout/ShippingCalculator.php:265-280 for why legacy's dead checkPostalCode() === 0 guard is deliberately NOT reproduced verbatim)

  5. Weight surcharge: If package weight exceeds the WEIGHT_LIMIT option for the country, extra cost is calculated as ceil((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 exceeds TRANS_COST_LIMIT (the $checkFree replace-vs-add distinction). (ecommercen/libraries/AdvTransporters.php, transportCost(), :107-208; ported to REST in ShippingCalculator::resolveChargedCost(), #568)

  6. Free shipping logic: Shipping is free when the order total exceeds TRANS_COST_LIMIT AND weight is within WEIGHT_LIMIT, or unconditionally below WEIGHT_LIMIT when TRANS_FREE_ALL is set. If weight exceeds the limit, only the overweight surcharge applies. Cash-on-delivery additionally charges a separate DELIVERY_COST surcharge (waived at DELIVERY_COST_MIN_FREE) that is never folded into the shipping cost itself. (ecommercen/libraries/AdvTransporters.php, transportCost() :107-208 / deliveryCost(), ported to REST in ShippingCalculator, #568 — REST previously ignored the cart total entirely and always charged the raw pricing-row cost, overcharging carts above TRANS_COST_LIMIT and undercharging the shop for carts above WEIGHT_LIMIT)

  7. 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 returns 0 — 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)

  8. 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())

  9. Marketplace mapping guards: The public_marketplace_mapping page is only accessible when PUBLIC_MARKETPLACE.ENABLED is set in the registry. The "none" UI option maps to the other code in the database. (ecommercen/eshop/controllers/Adv_transporters_admin.php, public_marketplace_mapping())

  10. 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)

  11. Smart point transporters: Providers with DELIVERY_OPTION_TYPE = 2 are classified as "smart point only" transporters (e.g., BoxNow, Skroutz with pickup points). The getIsSmartPointOnlyTransporter() method checks this flag. (ecommercen/libraries/AdvTransporters.php, getIsSmartPointOnlyTransporter(), :354-361)

  12. BoxNow cash-on-delivery override. A smart-point-only BoxNow transporter (DELIVERY_OPTION_TYPE = 2) normally has the delivery (COD) pay way stripped at checkout. Setting the per-transporter DELIVERY_OPTION flag (admin checkbox, label key eshop.admin.transporters.settings_overrideDelivery) keeps delivery available for that transporter. Default off. Enforced storefront-side at assets/vue/mixins/checkoutPage.js:458 and :484 (gated on getIsSmartPointOnlyTransporter && !getSelectedTransporterDeliveryOption). The flag reaches the storefront via Transporters::protectData() (ecommercen/libraries/AdvTransporters.php:309-338, DELIVERY_OPTION → deliveryOption, bool-cast, entry at :323-326) and the getSelectedTransporterDeliveryOption Vuex 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-Transporter DELIVERY_OPTION Override. The legacy server side reads the same rule through AdvTransporters::isCashOnDeliveryAllowed($transporterId) (ecommercen/libraries/AdvTransporters.php:363-376, #338), a wrapper over CashOnDeliveryPolicy::permits() used by Adv_order::paywayTransporterCheckValidation() (ecommercen/eshop/controllers/Adv_order.php:1493, call at :1508) — so the legacy storefront preview and checkout also refuse delivery with a smart-point-only transporter whose DELIVERY_OPTION is off. The REST calculator publishes the same verdict as codAllowed in each ShippingCalculator::calculate() result (src/Domains/Checkout/ShippingCalculator.php:255-258, docblock :132-135); its constructor takes CashOnDeliveryPolicy (:113).

  13. 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)

  14. 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())

  15. Client transport-cost extension seam (4.112.0, #389): A client fork customizes transport-cost pricing by subclassing AdvTransporters (legacy application/libraries/Transporters.php extends AdvTransporters) and overriding two protected hooks rather than copy-pasting the whole transportCost() 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 = 0 branches, and a non-null customTransportCost() 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\ShippingCalculator gains the identical pair of protected seams — customTransportCost() (src/Domains/Checkout/ShippingCalculator.php:369-378) and applyTransportSurcharge() (:393-403) — with the same short-circuit/compose semantics. A client fork on the modern REST checkout should override these on a Custom\…\ShippingCalculator subclass aliased in custom/Domains/container.php, instead of redeclaring calculate() wholesale. The smile_v4 fork currently redeclares the equivalent legacy method wholesale and is a candidate to migrate to the hooks.

  16. Client transporter availability gate seam (#558): canResolveTransportCost($transporterId, $country, $county, $postalCode, $weight): bool (ecommercen/libraries/AdvTransporters.php:90-93) is a pre-hook on getAvailable() (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 to true (include everything); a client override can return false to dynamically hide a transporter if its required external API (e.g., address validation) fails or rates are unavailable. The hook is consulted before transportCost() is called, so it won't block via exception — only via omission from the available set. (Legacy only.)

  17. 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_admin and overriding a protected no-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 within pricingOptions() (Adv_transporters_admin.php:1054), immediately before defaultRender(); 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 ​

  1. [OPEN] Provider map drift: 6 of 17 transporter class names are absent from $config['smartPoints'] — CYPRUSPOST, DAILYCOURIER, ASAP, DHL, TAXYDEMA, and TAXYDEMAV2 — and will always return 409 no_provider from GET /rest/transporter/{id}/smart-point (see IN-09 Transporter Integrations §Known Issues for full detail).

  2. [OPEN — minor] TransporterSettingsLoader uses get_instance() to load the legacy transporters_model (src/Domains/Transporter/SmartPointCatalog/TransporterSettingsLoader.php:19-22). Domain-layer convention break, acknowledged in method docblock on load(). Integration-testable only.

  3. [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".

  4. [RESOLVED — #338] DELIVERY_OPTION is now covered by tests/Unit/Checkout/CashOnDeliveryPolicyTest.php, ShippingCalculatorTest (codAllowed), PlaceOrderServiceTest and tests/Legacy/Eshop/AdvOrderCashOnDeliveryTransporterTest.php. Original text: Limited test coverage for the standalone DELIVERY_OPTION flag (DELIVERY_OPTION_TYPE has coverage at tests/Unit/Checkout/ShippingCalculatorTest.php:40,603,615,637,647, but the BoxNow-only DELIVERY_OPTION override that gates COD availability is untested).

  5. [OPEN — minor] saveBOXNOWSettings() writes 'reg_value' => $settings['DELIVERY_OPTION'] ?? '' (ecommercen/eshop/models/Adv_transporters_model.php:508) — an unchecked box stores '', not 0; 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's PAPER_SIZE/LABELS_PER_PAGE settings, which covers the analogous "row absent vs. row stored empty" gap for those two keys.)

  6. [RESOLVED — #338] The modern layer now reads DELIVERY_OPTION through Advisable\Domains\Checkout\CashOnDeliveryPolicy (published as codAllowed on POST /rest/checkout/shipping, enforced by REST place-order); the settings rows themselves remain admin-only. Original text: DELIVERY_OPTION is BoxNow/legacy-only: no modern Transporter REST or domain layer entity is aware of deliveryOption. Only BoxNowConfig, Transporters::protectData(), and checkout JS interpret it.

  7. [NEW] AdvTransporterOptions::weightLimit() guards on isset(…->options[$country]) while its six siblings guard the full key path (ecommercen/libraries/transporters/AdvTransporterOptions.php:20-26 vs :12-18, 28-66). A country row set lacking WEIGHT_LIMIT — reachable via POST /rest/transporter/option-pricing, which inserts single rows and bypasses savePricingOptions()'s 5-key gate (ecommercen/eshop/models/Adv_transporters_model.php:994-1020) — raises Undefined array key "WEIGHT_LIMIT" on the checkout path.

  8. [NEW] REST create can violate NOT NULL. Validator::validateForCreate() is empty, WriteData::$slug and MuiWriteData::$name are nullable (WriteData.php:30, MuiWriteData.php:20), and parseMuiData() uses toArray() with no excludeNull (WriteService.php:79). POST /rest/transporter {"translations":[{"lang":"el"}]} INSERTs name => null into transporters_mui.name varchar(255) NOT NULL, and a body without slug INSERTs null into transporters.slug varchar(50) NOT NULL — while both OA schemas declare them required (src/Rest/Transporter/Resources/Transporter/WriteData.php).

  9. [NEW] No FOREIGN KEY constraint exists on any child table, only plain KEYs (database/initial/initial.sql:2281-2334, :1026-1032), and WriteService::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.

  10. [NEW] transporters_counties_availabilities PK omits country_alpha_2 (database/initial/initial.sql:2285) although the column is written per-country by saveCountiesAvailabilities() — two countries sharing a county_alpha collide on insert.

  11. [NEW] In application/views/admin/transporters/pricingOptions.php, every set_value() first arg names county[{$countryCode}][…] while the inputs are option[{$countryCode}][…], and three additionally hardcode DELIVERY_COST for the WEIGHT_LIMIT / PRICE_PER_KG / TRANS_COST_LIMIT inputs (:51, :59, :67) — so post-failure repopulation never works; the DB-value fallback masks it.

  12. [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 at SmartPoint.php:79.

Tests ​

Test coverage spans unit and integration layers:

Test FileCoverage
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.phpLegacy-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_OPTION BoxNow 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

Customer Flows ​

Admin Flows ​

Integration Flows ​

System Flows ​

Wiki Guides: DHL Guide | Circuit Breaker Guide