Skip to content

Recommendation Algorithm Priority

Flow ID: AD-56 | Module(s): plus | Complexity: High Last Updated: 2026-04-06

Business Context

The recommendation algorithm priority settings let administrators configure which recommendation engines run (and in which order) for two distinct storefront contexts:

  1. Product page recommendations ("Combo"): algorithms that populate the combo/related carousel on the product detail page.
  2. After add-to-cart recommendations: algorithms that populate the modal shown when a customer adds an item to the cart.

Each context owns an independently ordered list of algorithms. The storefront runs a first-match-wins cascade at request time — the dispatcher iterates the configured order, calls each algorithm in turn, and stops at the first one that returns a non-empty result. Admins reorder via drag-and-drop (jQuery sortable), bind curated related-product groups to each algorithm, and choose the input mode (single product vs. entire cart) for AI, smart, and related arms.

The cascade pattern means the priority list is effectively a fallback chain: if the AI engine is down or returns nothing, the next arm (e.g. manually curated related groups, or DB best-sellers) takes over silently.


API Reference

REST Endpoints

No REST API. Algorithm priority is managed exclusively through the legacy admin interface.

Legacy Admin Routes

RouteControllerMethodHTTPDescription
settings/plus_algo_priorityAdvPlusAlgoPrioritySettingsindex()GET/POSTMain settings page with sortable algorithm lists
settings/plus_algo_priority/priorityAdvPlusAlgoPrioritySettingspriority()POST (AJAX)Save product page algorithm order
settings/plus_algo_priority/addToCartPriorityAdvPlusAlgoPrioritySettingsaddToCartPriority()POST (AJAX)Save add-to-cart algorithm order

Route definitions: application/config/routes.php:194-195, 209-210.


Algorithm Types

The four algorithms are PascalCase strings stored in the ALGO_PRIORITY registry arrays. The cascade dispatcher uses each string to build a method name at runtime — getRecommendation{Algo} on the cart controller and renderProductPageComboProducts{Algo} on the product controller. The canonical set is seeded by InitialSeed::plusAlgoPriority() at database/seeders/InitialSeed.php:70-79:

RelatedRecommendations | AdvisableAi | SmartRecommendations | Custom

1. AdvisableAi

External ML engine call. The cart arm uses frequently-bought-together (FBT); the product page arm reads pre-fetched AI results from $this->render['aiRecommendations']['product-page-combo'].

LayerFile:Line
Cart armecommercen/api/controllers/AdvApiCartController.php:310-349 (getRecommendationAdvisableAi())
PDP armecommercen/eshop/controllers/Adv_products.php:1057-1065 (renderProductPageComboProductsAdvisableAi())
HTTP façadeecommercen/ai/libraries/AdvAIConnector.php
High-level wrapperecommercen/ai/libraries/AdvAdvisableAI.php (circuit breaker)
Entry adapterecommercen/core/Adv_front_controller.php:436-506 (purchasedProductsWithProducts(), purchasedProductsWithProduct(), similarItemsForItem(), productsBasedOnCustomerHistoryCore())

The cart arm calls AdvAdvisableAI with an FbtParameterBuilder(4, false, FBT_DAYS) — 4-limit, unordered, configurable look-back (AdvApiCartController.php:323-330).

AdvAIRecommendationTypes sub-types

ecommercen/ai/libraries/AdvAIRecommendationTypes.php defines nine sub-types. These values are written to the ai_positions.recommendation_type column; Adv_front_controller::itemsRecommendationAI() switches on them to pick the right connector call.

ConstantValueDescription
SIMILAR_ITEMS_FOR_ITEMsimilarItemsForItemSimilar-product carousel for a single item
ITEMS_PURCHASED_WITH_ITEMpurchasedProductsWithProductFBT for one anchor product
ITEMS_PURCHASED_WITH_ITEMSpurchasedProductsWithProductsFBT for multi-item cart (used by getRecommendationAdvisableAi)
CUSTOMER_PRODUCTS_BASED_ON_THEIR_HISTORYproductsBasedOnCustomerHistoryEngine-picked history algorithm (auto)
CUSTOMER_PRODUCTS_BASED_ON_THEIR_HISTORY_VBFproductsBasedOnCustomerHistoryVbfView-based filtering
CUSTOMER_PRODUCTS_BASED_ON_THEIR_HISTORY_CBFproductsBasedOnCustomerHistoryCbfContent-based filtering
CUSTOMER_PRODUCTS_BASED_ON_THEIR_HISTORY_CFproductsBasedOnCustomerHistoryCfCollaborative filtering
CUSTOMER_PRODUCTS_BASED_ON_THEIR_HISTORY_MPproductsBasedOnCustomerHistoryMpMost popular (personalised)
MOST_POPULAR_ITEMSmostPopularItemsGlobal most popular

Only ITEMS_PURCHASED_WITH_ITEMS is wired into the cart cascade. The other eight power ai_positions-driven page recommendations (home, category, checkout, etc.) — see CF-34.

2. SmartRecommendations

"Smart" means DB-only best-sellers from tmp_shop_order_basket, a cron-maintained snapshot table. No external service.

LayerFile:Line
Cart armecommercen/api/controllers/AdvApiCartController.php:351-379 (getRecommendationSmartRecommendations())
PDP armecommercen/eshop/controllers/Adv_products.php:1067-1081 (renderProductPageComboProductsSmartRecommendations())
Modelecommercen/eshop/models/Adv_products_in_cart_model.phpgetBestSellersUnion() (110), getBestSellers() (50), getBestSellersById() (78)
Cron jobecommercen/job/libraries/AdvUpdateTempOrderBasketTable.php
ID enumapplication/core/SmartRecommendationId.php
php
// ecommercen/api/controllers/AdvApiCartController.php:351-379
protected function getRecommendationSmartRecommendations(): array
{
    if (!$this->registry->value('SMART_RECOMMENDATIONS', 'IS_ENABLED_BASED_ON_CART')) {
        return [];
    }
    $productIds = $this->getInputOptions((int)$this->registry->value('ALGO_PRIORITY', 'AFTER_ADD_TO_CART_INPUT_SMART'));
    if (empty($productIds)) { return []; }

    $this->load->model('eshop/products_in_cart_model');
    $recommendation = $this->pscache->model(
        'products_in_cart_model', 'getBestSellersUnion', [$productIds], $this->pscache_ttl
    );
    if (!$recommendation) { return []; }

    return array_map(function ($product) {
        $product->recommendationId   = SmartRecommendationId::cartRelatedProducts()->value;
        $product->recommendationType = BasketTrackType::smartRecommendation()->label;
        return $product;
    }, $this->product_model->getProductsByIdsOrderedFromInput($recommendation));
}

SmartRecommendationId defines four UUIDs (application/core/SmartRecommendationId.php):

MethodUUIDUsed for
bestSellingProducts978ae181-3592-420e-8958-b3e61fdf022ageneral best-sellers widget
cartRelatedProductsbf86a9c4-3d77-41b1-98db-fca145e3e00bcart best-sellers (assigned in cart cascade)
customerRecommendationse95a107e-4df1-49b6-81c6-9b52c6b0d35ecustomer recommendations
cartBestSellers5eeb84de-afa2-43b0-9b52-7e617cb6d738cart best-sellers variant

Sister smart toggles live in the same registry group: IS_ENABLED_CUSTOMER_TAG and IS_ENABLED_LAST_SEEN (configured at ecommercen/plus/controllers/AdvGeneralSettings.php:62-80).

3. RelatedRecommendations

Manually curated related-product groups — merchants define groups in admin and bind specific group IDs to each context.

LayerFile:Line
Cart armecommercen/api/controllers/AdvApiCartController.php:381-407 (getRecommendationRelatedRecommendations())
PDP armecommercen/eshop/controllers/Adv_products.php:1083-1107 (renderProductPageComboProductsRelatedRecommendations())
Modelecommercen/eshop/models/Adv_related_product_model.phprelatedMultipleProductsFromGroups() (664), relatedProductsFromGroups() (680), getRelatedProductGroups() (admin)
php
// ecommercen/api/controllers/AdvApiCartController.php:381-407
protected function getRecommendationRelatedRecommendations(): array
{
    $relatedGroupIds = $this->registry->getArray('AFTER_ADD_TO_CART_RELATED_ID', 'ALGO_PRIORITY');
    if (!$this->registry->value('RELATED_RECOMMENDATIONS', 'ENABLED') || empty($relatedGroupIds)) {
        return [];
    }
    $productIds = $this->getInputOptions((int)$this->registry->value('ALGO_PRIORITY', 'AFTER_ADD_TO_CART_INPUT_RELATED'));
    if (empty($productIds)) { return []; }

    $relatedProducts = $this->related_product_model->relatedMultipleProductsFromGroups(
        $relatedGroupIds, $productIds,
        $this->cartResource->getData()['productIds'] ?? [],
        new DbLimit(24)
    );
    // ... assigns recommendationType = BasketTrackType::relatedRecommendation()->label
    //     and recommendationId = groupId of the winning related group
}

Attribution records the related group ID as recommendationId so analytics can trace conversions back to the curator's group choice. Related-group management is documented in AD-38.

4. Custom

Empty by default — an explicit extension point for per-client business logic. Clients subclass AdvApiCartController / Adv_products (or alias via DI, or use the legacy override at application/modules/api/controllers/Api_cart.php) and implement real logic in getRecommendationCustom() / renderProductPageComboProductsCustom().

php
// ecommercen/api/controllers/AdvApiCartController.php:409-413
protected function getRecommendationCustom(): array
{
    //you can fit here custom client implementations
    return [];
}
php
// ecommercen/eshop/controllers/Adv_products.php:1109-1113
protected function renderProductPageComboProductsCustom(): array
{
    //you can fit here custom client implementations
    return [];
}

BasketTrackType::clientRecommendation() (label 'cl', value 4) is reserved for these. The input-mode dropdown is intentionally hidden for Custom in the admin view (application/views/admin/plus/plus_algo_priority.php:84).


First-Match-Wins Cascade Dispatcher

Two independent cascades, one per context, both following the same pattern: iterate the configured priority list, call $this->{"prefix$algo"}(), break on the first non-empty result. Both pull the order from the registry — fully configurable, no hardcoded precedence.

After-add-to-cart cascade

ecommercen/api/controllers/AdvApiCartController.php:415-434:

php
protected function getRecommendation(): array
{
    if (!$this->registry->value('ECOMMERCEN_PLUS', 'AFTER_ADD_TO_CART_RECOMMENDATIONS_ENABLED')) {
        return [];
    }
    $result = [];
    foreach ($this->getRecommendationAlgoPriority() as $algo) {
        $result = $this->{"getRecommendation$algo"}();
        if ($result) { break; }
    }
    return $result;
}

protected function getRecommendationAlgoPriority(): array
{
    return $this->registry->getArray('AFTER_ADD_TO_CART', 'ALGO_PRIORITY');
}

The dispatch loop is at lines 422-427. The cascade is invoked from two call sites with different gating semantics:

  • update() at :112-114 — gated by the ?aatcr=1 query string (constant AFTER_ADD_TO_CARD_RECOMMENDATION = 'aatcr' at application/config/constants.php:198) and by a requestRecommendation flag that cartHandle() sets only when a new product row is actually added to the cart.
  • massUpdate() at :162 — calls setCartRecommendationToJsonState() unconditionally, regardless of aatcr or requestRecommendation. Any batch cart update will run the cascade and serialize the result.

Kill-switch: ECOMMERCEN_PLUS.AFTER_ADD_TO_CART_RECOMMENDATIONS_ENABLED.

Product page combo cascade

ecommercen/eshop/controllers/Adv_products.php:1034-1055:

php
protected function renderProductPageComboProducts(): void
{
    if (!$this->registry->value('ECOMMERCEN_PLUS', 'PRODUCT_PAGE_COMBO_ENABLED')) {
        $this->render['productComboProducts'] = [];
        return;
    }
    $products = [];
    foreach ($this->renderProductPageComboProductsPriority() as $algo) {
        $products = $this->{"renderProductPageComboProducts$algo"}();
        if ($products) { break; }
    }
    $this->render['productComboProducts'] = $products;
}

Dispatch loop: lines 1042-1047. Kill-switch: ECOMMERCEN_PLUS.PRODUCT_PAGE_COMBO_ENABLED.

Configurable order

The order is entirely data-driven via drag-and-drop in AdvPlusAlgoPrioritySettings. The seeded default (database/seeders/InitialSeed.php:72) is:

RelatedRecommendations → AdvisableAi → SmartRecommendations → Custom

Removing an algorithm from the sortable list disables it in that context (it simply won't be iterated).


Registry Configuration

The system reuses the string ALGO_PRIORITY both as a reggroup (for scalar config values like input modes) and as a regkey (for ordered-list arrays like the cascade itself). The two senses are orthogonal.

Main keys

reggroupregkeyTypePurpose
ALGO_PRIORITYPRODUCT_PAGE_COMBOarray (pipe-delim)Ordered list for PDP cascade
ALGO_PRIORITYAFTER_ADD_TO_CARTarray (pipe-delim)Ordered list for cart cascade
ALGO_PRIORITYPRODUCT_PAGE_COMBO_RELATED_IDarrayRelated group IDs for PDP RelatedRecommendations
ALGO_PRIORITYAFTER_ADD_TO_CART_RELATED_IDarrayRelated group IDs for cart RelatedRecommendations
ALGO_PRIORITYAFTER_ADD_TO_CART_INPUT_ADVISABLE_AIint (0/1)Input mode for AdvisableAi (0=single product, 1=cart)
ALGO_PRIORITYAFTER_ADD_TO_CART_INPUT_SMARTint (0/1)Input mode for SmartRecommendations
ALGO_PRIORITYAFTER_ADD_TO_CART_INPUT_RELATEDint (0/1)Input mode for RelatedRecommendations

The same strings PRODUCT_PAGE_COMBO, AFTER_ADD_TO_CART, PRODUCT_PAGE_COMBO_RELATED_ID, and AFTER_ADD_TO_CART_RELATED_ID also appear as reggroup with regkey = 'ALGO_PRIORITY' when the pipe-delimited list is stored (see InitialSeed.php:75-76).

Feature gates / kill-switches

reggroupregkeyPurpose
ECOMMERCEN_PLUSAFTER_ADD_TO_CART_RECOMMENDATIONS_ENABLEDMaster kill-switch for cart cascade
ECOMMERCEN_PLUSPRODUCT_PAGE_COMBO_ENABLEDMaster kill-switch for PDP cascade
ADVISABLE_AIENABLEDGlobal AI engine gate (Adv_front_controller::shouldLoadAdvisableAI())
ADVISABLE_AIBASE_URI, VERSION, USERNAME, PASSWORD, APP_ID, APP_SECRET, TIMEOUT, CONNECT_TIMEOUTHTTP client config
ADVISABLE_AIFBT_DAYSLook-back window for FBT cart call (default 1)
SMART_RECOMMENDATIONSIS_ENABLED_BASED_ON_CARTGate for SmartRecommendations cart arm
SMART_RECOMMENDATIONSIS_ENABLED_CUSTOMER_TAGGate for audience/tag-based smart features
SMART_RECOMMENDATIONSIS_ENABLED_LAST_SEENGate for last-seen widget
RELATED_RECOMMENDATIONSENABLEDGate for RelatedRecommendations arm

Per-context priority is only supported for two contexts (PRODUCT_PAGE_COMBO and AFTER_ADD_TO_CART). Other pages (home, cart, minicart, category, checkout) use AdvAIRecommendationPages-keyed positions out of the ai_positions table and do not participate in the first-match-wins cascade — see CF-34.


Input Modes

The cart cascade passes different inputs to each arm based on its _INPUT_* registry value. The helper lives at ecommercen/api/controllers/AdvApiCartController.php:435-449:

php
protected function getInputOptions(int $option): array
{
    if ($option === 1) {
        $cart = $this->cartResource->getData()['productIds'] ?? [];
        if ($cart) { return $cart; }
    }
    $productIds = $this->product_model->getProductIdByProductCodeId([$this->lastProductCodeIdPosted]);
    if (empty($productIds)) { return []; }
    return [$productIds[array_key_first($productIds)]];
}
ModeIntMeaningInput passed
Product0Single-product modeLast product code added (lastProductCodeIdPosted from update() line 102) — single ID
Cart1Cart-wide modeEntire cart's product IDs; falls back to single-product if cart is empty
  • Dropdown labels: eshop.admin.settings.plus_algo_priority.input.product / .cart at application/views/admin/plus/plus_algo_priority.php:88-89.
  • Custom has no input dropdown — plus_algo_priority.php:84 (if ($algo !== 'Custom')).
  • AdvisableAi hands IDs to FbtParameterBuilder(4, false, FBT_DAYS) — 4-limit, unordered, configurable look-back (AdvApiCartController.php:323-330).
  • SmartRecommendations passes them to getBestSellersUnion($productIds) — union of "best sellers co-occurring with these" + "global best sellers excluding these".
  • RelatedRecommendations uses them as anchors AND excludes current cart contents via the third argument to relatedMultipleProductsFromGroups.
  • Customer context/history input is NOT part of the algo-priority cascade. History and segment data are only used in AdvAdvisableAI::productsBasedOnCustomerHistoryCore() — a distinct path fired from page positions, not the cart cascade.
  • The PDP cascade does not use getInputOptions(). Each algorithm inherently knows its anchor: AdvisableAi is pre-fetched by itemsRecommendationAI(); SmartRecommendations reads cart_best_sellers_related_products populated in getComboCartProducts() (around line 366); RelatedRecommendations uses productData->product_id.

Attribution & Tracking

BasketTrackType enum

application/core/BasketTrackType.php:

php
class BasketTrackType extends \Spatie\Enum\Enum
{
    protected static function values(): array {
        return [
            'none'                  => 0,
            'aiRecommendation'      => 1,
            'smartRecommendation'   => 2,
            'relatedRecommendation' => 3,
            'clientRecommendation'  => 4,
        ];
    }
    protected static function labels(): array {
        return [
            'none'                  => '',
            'aiRecommendation'      => 'ai',
            'smartRecommendation'   => 'smart',
            'relatedRecommendation' => 'rlr',
            'clientRecommendation'  => 'cl',
        ];
    }
    public static function tryFromLabel(string $label): ?\Spatie\Enum\Enum { /* … */ }
}

The integer is persisted in shop_order_basket.track_type; the label is transmitted over URL querystrings and cart payloads.

recommendationId / recommendationType

ArmrecommendationIdrecommendationTypeSource
AdvisableAi (cart)from AI responseBasketTrackType::aiRecommendation()->label ('ai')AdvApiCartController.php:343-348
SmartRecommendations (cart)SmartRecommendationId::cartRelatedProducts()->value (UUID)'smart'AdvApiCartController.php:374-378
RelatedRecommendations (cart)winning related group id'rlr'AdvApiCartController.php:401-406
RelatedRecommendations (PDP)winning related group idenum object (not ->label)Adv_products.php:1101-1106
Customconventionally 'cl', not enforced

Click-through round trip

  1. The cart-recommendation modal renders each product link with querystring params rt (type label) and ri (id). See assets/main/vue/AfterAddToCartModal.vue:86-87, 170-173. The querystring keys come from window.advAppContext.advAppData.url.recommendationTypeQuerystring / recommendationIdQuerystring.
  2. On the landed page, Adv_front_controller::checkForRecommendationIdInQuery() reads rt/ri, populates $this->recommendationTypeUsed (re-hydrated via BasketTrackType::tryFromLabel) and $this->recommendationIdUsed. Referenced from Adv_products.php::attachProductTracking() at :1115-1123.
  3. AdvApiCartController::update() forwards into cartHandle() (:107), which stores these as options['recommendation'] = ['id' => …, 'type' => …] on the cart item.
  4. At order creation: track_id + track_type are persisted to shop_order_basket. AdvAdvisableAI::purchaseOrder() includes recommendationId in the AI purchase event for items with track_type == aiRecommendation, closing the attribution loop for ML retraining.

Caching Strategy

ArmCached?Notes
AdvisableAiNoCalls through AdvAdvisableAI on every cascade evaluation. A circuit breaker (Advisable\CircuitBreaker\CircuitBreaker at AdvAdvisableAI.php:3, 19, 444-454) prevents hammering a downed service, but there is no positive caching. This matches the platform rule that ad-serving / AI-serving responses must not be cached.
SmartRecommendationsYespscache->model('products_in_cart_model', 'getBestSellersUnion', [$productIds], $this->pscache_ttl) (AdvApiCartController.php:364-369). Page-scoped model-call cache, global (not per-customer).
RelatedRecommendationsNoNot cached at the cascade level.
CustomEmpty by default.
History-based AI recs (separate path)SessionSession-level caching of the recommendation ID rotated once per day (Adv_front_controller::productsBasedOnCustomerHistoryCore() :466-506).

The Registry library itself has an in-process cache, so repeated getArray() / value() calls within the same request are cheap.

Agora intersection: none. ProjectAgora v1/v2 is an ad-serving integration and does not participate in the recommendation cascade. A previously-existing caching layer for ad responses was removed per Agora docs prohibition.


External AI Service

Three-layer wrapper around the ecommercen/ai-connector Composer package:

LayerFileRole
CoreAIConnectorecommercen/ai/libraries/CoreAIConnector.phpLoads ADVISABLE_AI.* registry config into EcommerceN\AIConnector\Config\Config; attaches CoreLogger on the advisable-ai Monolog channel
AdvAIConnectorecommercen/ai/libraries/AdvAIConnector.phpPublic façade: createUser, addToCart, removeFromCart, itemsPurchasedWithItem(s), similarItemsForItem, customerProductsBasedOnTheirHistory, batchPurchase, etc.
AdvAdvisableAIecommercen/ai/libraries/AdvAdvisableAI.phpHigh-level wrapper: maps product ↔ AI item IDs, injects circuit breaker, manages session/customer user identity, deferred tasks

Failure & fallback behaviour

  1. Master gate: Adv_front_controller::shouldLoadAdvisableAI() returns false if ADVISABLE_AI_FORCE_DISABLE env is set, the user agent is a bot, or ADVISABLE_AI.ENABLED is false → all AI entrypoints return [] silently.
  2. User gate: shouldCallAdvisableAI() requires cSData['ai_user']->id to be populated — no AI user means the arm returns [].
  3. Circuit breaker: AdvAdvisableAI holds a CircuitBreaker (:19) lazily resolved via di()->get('circuit_breaker.advisable_ai') inside the circuitBreaker() method at :444-454 (not the constructor). When the breaker is OPEN, connector calls short-circuit to null / [].
  4. Cascade fall-through: a silent [] from AdvisableAi lets the next arm run. Customers see DB best-sellers or curated related products rather than an empty modal.
  5. Logging: all AI traffic logs on the advisable-ai Monolog channel (CoreAIConnector::setLogger() :31-36).
  6. Deferred tasks: all write events (view, add-to-cart, purchase, bookmark, rating, session switch) are queued to DeferredTaskRunner with maxCostSeconds = TIMEOUT + CONNECT_TIMEOUT. Recommendation reads are synchronous — they happen inside the request lifecycle.

Frontend Consumption

After-add-to-cart modal

  • Component: assets/main/vue/AfterAddToCartModal.vue — renders the modal from the Vuex state afterAddToCartRecommendationProducts. Each product link carries rt / ri via :recommendation-type / :recommendation-id props.

  • Vuex action: assets/vue/store/actions.js:122-132:

    cartData.recommendation = { products: [...], liveData: [...] }
    → commit('setAfterAddToCartRecommendationProducts', products)
    → commit('setAfterAddToCartRecommendationModal', !!products)
    → commit('setAfterAddToCartRecommendationRequest', !products)
  • Trigger: cart update requests append ?aatcr=1 (constant at application/config/constants.php:198).

  • Endpoint: POST api/cartapi/api_cart/indexAdvApiCartController::update() at :92. Routes at application/config/routes.php:568-571.

  • Response shape: jsonState.recommendation = { products: [...], liveData: [...] }. The payload is assembled by setCartRecommendationToJsonState() at AdvApiCartController.php:292-308.

Product page combo

Adv_products::renderProductPageComboProducts() stores the result in $this->render['productComboProducts'], which is consumed by the PHP templates (no AJAX). The rendered cards carry recommendationType / recommendationId attributes for click-through attribution.

Other AI-recommendation pages

Home, cart, minicart, category, and checkout recommendations bypass the cascade entirely. They render position-by-position from Adv_front_controller::itemsRecommendationAI(), driven by rows in the ai_positions table, not by ALGO_PRIORITY. See CF-34 for the full position-based flow.


Code Flow

Loading Settings

AdvPlusAlgoPrioritySettings::index()
  |
  +--> On POST submit: setRelated() (save related group assignments)
  |
  +--> Load current priorities from registry:
  |    - registry->getArray('PRODUCT_PAGE_COMBO', 'ALGO_PRIORITY')
  |    - registry->getArray('AFTER_ADD_TO_CART', 'ALGO_PRIORITY')
  |
  +--> Load related product groups:
  |    - related_product_model->getRelatedProductGroups($lang)
  |    - Indexed by ID for dropdown selection
  |
  +--> Load related group associations:
  |    - registry->getArray('PRODUCT_PAGE_COMBO_RELATED_ID', 'ALGO_PRIORITY')
  |    - registry->getArray('AFTER_ADD_TO_CART_RELATED_ID', 'ALGO_PRIORITY')
  |
  +--> Load AI/smart recommendation input modes:
  |    - AFTER_ADD_TO_CART_INPUT_ADVISABLE_AI
  |    - AFTER_ADD_TO_CART_INPUT_SMART
  |    - AFTER_ADD_TO_CART_INPUT_RELATED
  |
  +--> Render with jQuery sortable JS (js_listing_sortable helper) for drag-and-drop

Saving Algorithm Order (AJAX)

priority() / addToCartPriority()
  |
  +--> Validate AJAX request
  +--> Extract listItem[] from POST
  +--> registry->setArray(GROUP, 'ALGO_PRIORITY', '', $data)
  +--> Return success message
setRelated()
  |
  +--> registry->setArray('PRODUCT_PAGE_COMBO_RELATED_ID', 'ALGO_PRIORITY', '', $productPageRelatedIds)
  +--> registry->setArray('AFTER_ADD_TO_CART_RELATED_ID', 'ALGO_PRIORITY', '', $afterAddToCartRelatedIds)
  +--> Save AI input parameters:
       - AFTER_ADD_TO_CART_INPUT_ADVISABLE_AI
       - AFTER_ADD_TO_CART_INPUT_SMART
       - AFTER_ADD_TO_CART_INPUT_RELATED

Domain Layer

No modern domain layer. Algorithm priorities are stored entirely in the registry.


Architecture

ComponentPathPurpose
AdvPlusAlgoPrioritySettingsecommercen/plus/controllers/AdvPlusAlgoPrioritySettings.phpAdmin controller (79 lines)
Admin viewapplication/views/admin/plus/plus_algo_priority.phpjQuery-sortable lists + input dropdowns
AdvApiCartControllerecommercen/api/controllers/AdvApiCartController.phpCart cascade dispatcher + per-algo methods
Adv_productsecommercen/eshop/controllers/Adv_products.phpPDP cascade dispatcher + per-algo methods
Adv_products_in_cart_modelecommercen/eshop/models/Adv_products_in_cart_model.phpBest-sellers queries over tmp_shop_order_basket
Adv_related_product_modelecommercen/eshop/models/Adv_related_product_model.phpRelated-group queries
AdvUpdateTempOrderBasketTableecommercen/job/libraries/AdvUpdateTempOrderBasketTable.phpCron refreshing tmp_shop_order_basket
AdvAIConnector / AdvAdvisableAI / CoreAIConnectorecommercen/ai/libraries/Three-layer AI HTTP stack
BasketTrackTypeapplication/core/BasketTrackType.phpTrack-type enum (int + label)
SmartRecommendationIdapplication/core/SmartRecommendationId.phpStable UUIDs for smart widgets
Registryregistry tableStores all algorithm priorities, gates, and input modes
Routesapplication/config/routes.php:194-195, 209-210Route definitions
Admin menuapplication/config/admin_menu.php:92-98ECOMMERCEN group entry

Data Model

No dedicated tables. All configuration is stored in the registry table — see the Registry Configuration section above for the full key map. The runtime data (tracked recommendations) flows into shop_order_basket.track_id / track_type and, for AdvisableAi, into the external AI engine via AdvAdvisableAI::purchaseOrder().

Snapshot table read by SmartRecommendations: tmp_shop_order_basket, refreshed by AdvUpdateTempOrderBasketTable cron.


Configuration

  • Routes: application/config/routes.php:194-195, 209-210.
  • Admin menu: application/config/admin_menu.php:92-98 (ECOMMERCEN group).
  • Constants: AFTER_ADD_TO_CARD_RECOMMENDATION = 'aatcr' at application/config/constants.php:198.

RBAC

Constructor at ecommercen/plus/controllers/AdvPlusAlgoPrioritySettings.php:5-13:

php
public function __construct()
{
    parent::__construct();
    if (!allowRole([AUTH_ROLE_ADVISABLE, AUTH_ROLE_DEVELOPER, AUTH_ROLE_ADMIN], $this->sessionRoles)) {
        $this->error_401();
        return;
    }
    // …
}

Both index() and the AJAX endpoints (priority(), addToCartPriority()) share this gate. The role list matches the admin menu entry at application/config/admin_menu.php:92-98.

RoleConstantMenu visiblePage accessible
AdvisableAUTH_ROLE_ADVISABLE (1)YesYes
DeveloperAUTH_ROLE_DEVELOPER (2)YesYes
AdminAUTH_ROLE_ADMIN (3)YesYes
All othersNoNo

Role constants: application/config/constants.php:99-107.


Client Extension Points

  • Custom algorithm arm: implement getRecommendationCustom() / renderProductPageComboProductsCustom() in a client subclass or DI alias of AdvApiCartController / Adv_products. Use BasketTrackType::clientRecommendation() ('cl') for attribution.
  • Override controllers: extend AdvPlusAlgoPrioritySettings in application/modules/plus/controllers/ for admin-side customisation.
  • Legacy override path: application/modules/api/controllers/Api_cart.php can subclass the cart controller in client repos without touching ecommercen/.
  • Custom related groups: client repos can seed their own related-product groups that become bindable in the algorithm configuration page — see AD-38.
  • Cascade reordering: no code change needed — reorder via drag-and-drop, or seed a different order in a client InitialSeed.

Business Rules

  1. Two recommendation contexts: product page combo and after-add-to-cart each have independent priority lists and independent kill-switches.
  2. Four algorithms: RelatedRecommendations, AdvisableAi, SmartRecommendations, Custom — seeded by InitialSeed::plusAlgoPriority().
  3. First-match-wins cascade: the dispatcher iterates the configured order and stops at the first non-empty result. Empty result from any arm means the next arm runs.
  4. Drag-and-drop ordering: algorithm priority is set via jQuery sortable, saved through AJAX to registry->setArray(context, 'ALGO_PRIORITY', '', $data).
  5. Related-group binding: RelatedRecommendations uses curated group IDs bound per context (*_RELATED_ID registry arrays). The winning group id is recorded as recommendationId.
  6. Input modes (cart cascade only): AdvisableAi, SmartRecommendations, and RelatedRecommendations can each be fed either the last-added product or the full cart; Custom has no dropdown. PDP cascade does not use input modes — each arm has its own anchor.
  7. Attribution: every rendered recommendation carries recommendationType (ai / smart / rlr / cl) and recommendationId, propagated via rt / ri URL params, stored on the cart item, and persisted to shop_order_basket.track_type / track_id at order creation.
  8. AdvisableAi is never positively cached: the cart arm always calls the AI engine (protected only by a circuit breaker). This matches the platform rule that ad/AI responses must not be cached.
  9. SmartRecommendations is cached: pscache->model(...) wraps getBestSellersUnion with $this->pscache_ttl.
  10. Graceful degradation: any failure in AdvisableAi (master gate, user gate, circuit breaker, timeout) returns [] and lets the next arm serve the recommendation.
  11. Cascade scope: only PRODUCT_PAGE_COMBO and AFTER_ADD_TO_CART use the cascade. Home / minicart / category / checkout recommendations are ai_positions-driven and bypass ALGO_PRIORITY entirely — see CF-34.
  12. Registry-based storage: all configuration uses the registry pattern — no dedicated tables.

Known Issues & Security Gaps

  1. Custom algorithm arm is empty by default -- getRecommendationCustom() at ecommercen/api/controllers/AdvApiCartController.php:409-413 and renderProductPageComboProductsCustom() at ecommercen/eshop/controllers/Adv_products.php:1109-1113 both return []. The arm exists only as a client extension point; enabling it without a client override produces no recommendations.
  2. AdvisableAi arm is never positively cached -- every cascade evaluation calls through AdvAdvisableAI live. Only a circuit breaker (ecommercen/ai/libraries/AdvAdvisableAI.php:444-454) prevents hammering a downed service. There is no TTL or response cache for successful AI responses.
  3. massUpdate() invokes the cascade unconditionally -- ecommercen/api/controllers/AdvApiCartController.php:162 calls setCartRecommendationToJsonState() without the aatcr query-string gate or the requestRecommendation flag that update() applies at :112-114. Any batch cart update triggers the full recommendation cascade.
  4. BasketTrackType::clientRecommendation() reserved but not enforced -- clients using the Custom arm conventionally use label 'cl' (value 4) from application/core/BasketTrackType.php, but nothing in the platform validates that custom implementations actually set this track type.
  5. Only 2 of 6 recommendation contexts use the cascade -- the product page and after-add-to-cart contexts have the first-match-wins dispatcher; home, cart page, minicart, category, and checkout bypass it and render directly from ai_positions table rows. Admins configuring ALGO_PRIORITY may assume it affects all recommendation surfaces, but it does not.