Appearance
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:
- Product page recommendations ("Combo"): algorithms that populate the combo/related carousel on the product detail page.
- 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
| Route | Controller | Method | HTTP | Description |
|---|---|---|---|---|
settings/plus_algo_priority | AdvPlusAlgoPrioritySettings | index() | GET/POST | Main settings page with sortable algorithm lists |
settings/plus_algo_priority/priority | AdvPlusAlgoPrioritySettings | priority() | POST (AJAX) | Save product page algorithm order |
settings/plus_algo_priority/addToCartPriority | AdvPlusAlgoPrioritySettings | addToCartPriority() | 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 | Custom1. 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'].
| Layer | File:Line |
|---|---|
| Cart arm | ecommercen/api/controllers/AdvApiCartController.php:310-349 (getRecommendationAdvisableAi()) |
| PDP arm | ecommercen/eshop/controllers/Adv_products.php:1057-1065 (renderProductPageComboProductsAdvisableAi()) |
| HTTP façade | ecommercen/ai/libraries/AdvAIConnector.php |
| High-level wrapper | ecommercen/ai/libraries/AdvAdvisableAI.php (circuit breaker) |
| Entry adapter | ecommercen/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.
| Constant | Value | Description |
|---|---|---|
SIMILAR_ITEMS_FOR_ITEM | similarItemsForItem | Similar-product carousel for a single item |
ITEMS_PURCHASED_WITH_ITEM | purchasedProductsWithProduct | FBT for one anchor product |
ITEMS_PURCHASED_WITH_ITEMS | purchasedProductsWithProducts | FBT for multi-item cart (used by getRecommendationAdvisableAi) |
CUSTOMER_PRODUCTS_BASED_ON_THEIR_HISTORY | productsBasedOnCustomerHistory | Engine-picked history algorithm (auto) |
CUSTOMER_PRODUCTS_BASED_ON_THEIR_HISTORY_VBF | productsBasedOnCustomerHistoryVbf | View-based filtering |
CUSTOMER_PRODUCTS_BASED_ON_THEIR_HISTORY_CBF | productsBasedOnCustomerHistoryCbf | Content-based filtering |
CUSTOMER_PRODUCTS_BASED_ON_THEIR_HISTORY_CF | productsBasedOnCustomerHistoryCf | Collaborative filtering |
CUSTOMER_PRODUCTS_BASED_ON_THEIR_HISTORY_MP | productsBasedOnCustomerHistoryMp | Most popular (personalised) |
MOST_POPULAR_ITEMS | mostPopularItems | Global 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.
| Layer | File:Line |
|---|---|
| Cart arm | ecommercen/api/controllers/AdvApiCartController.php:351-379 (getRecommendationSmartRecommendations()) |
| PDP arm | ecommercen/eshop/controllers/Adv_products.php:1067-1081 (renderProductPageComboProductsSmartRecommendations()) |
| Model | ecommercen/eshop/models/Adv_products_in_cart_model.php — getBestSellersUnion() (110), getBestSellers() (50), getBestSellersById() (78) |
| Cron job | ecommercen/job/libraries/AdvUpdateTempOrderBasketTable.php |
| ID enum | application/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):
| Method | UUID | Used for |
|---|---|---|
bestSellingProducts | 978ae181-3592-420e-8958-b3e61fdf022a | general best-sellers widget |
cartRelatedProducts | bf86a9c4-3d77-41b1-98db-fca145e3e00b | cart best-sellers (assigned in cart cascade) |
customerRecommendations | e95a107e-4df1-49b6-81c6-9b52c6b0d35e | customer recommendations |
cartBestSellers | 5eeb84de-afa2-43b0-9b52-7e617cb6d738 | cart 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.
| Layer | File:Line |
|---|---|
| Cart arm | ecommercen/api/controllers/AdvApiCartController.php:381-407 (getRecommendationRelatedRecommendations()) |
| PDP arm | ecommercen/eshop/controllers/Adv_products.php:1083-1107 (renderProductPageComboProductsRelatedRecommendations()) |
| Model | ecommercen/eshop/models/Adv_related_product_model.php — relatedMultipleProductsFromGroups() (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=1query string (constantAFTER_ADD_TO_CARD_RECOMMENDATION = 'aatcr'atapplication/config/constants.php:198) and by arequestRecommendationflag thatcartHandle()sets only when a new product row is actually added to the cart.massUpdate()at:162— callssetCartRecommendationToJsonState()unconditionally, regardless ofaatcrorrequestRecommendation. 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 → CustomRemoving 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
| reggroup | regkey | Type | Purpose |
|---|---|---|---|
ALGO_PRIORITY | PRODUCT_PAGE_COMBO | array (pipe-delim) | Ordered list for PDP cascade |
ALGO_PRIORITY | AFTER_ADD_TO_CART | array (pipe-delim) | Ordered list for cart cascade |
ALGO_PRIORITY | PRODUCT_PAGE_COMBO_RELATED_ID | array | Related group IDs for PDP RelatedRecommendations |
ALGO_PRIORITY | AFTER_ADD_TO_CART_RELATED_ID | array | Related group IDs for cart RelatedRecommendations |
ALGO_PRIORITY | AFTER_ADD_TO_CART_INPUT_ADVISABLE_AI | int (0/1) | Input mode for AdvisableAi (0=single product, 1=cart) |
ALGO_PRIORITY | AFTER_ADD_TO_CART_INPUT_SMART | int (0/1) | Input mode for SmartRecommendations |
ALGO_PRIORITY | AFTER_ADD_TO_CART_INPUT_RELATED | int (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
| reggroup | regkey | Purpose |
|---|---|---|
ECOMMERCEN_PLUS | AFTER_ADD_TO_CART_RECOMMENDATIONS_ENABLED | Master kill-switch for cart cascade |
ECOMMERCEN_PLUS | PRODUCT_PAGE_COMBO_ENABLED | Master kill-switch for PDP cascade |
ADVISABLE_AI | ENABLED | Global AI engine gate (Adv_front_controller::shouldLoadAdvisableAI()) |
ADVISABLE_AI | BASE_URI, VERSION, USERNAME, PASSWORD, APP_ID, APP_SECRET, TIMEOUT, CONNECT_TIMEOUT | HTTP client config |
ADVISABLE_AI | FBT_DAYS | Look-back window for FBT cart call (default 1) |
SMART_RECOMMENDATIONS | IS_ENABLED_BASED_ON_CART | Gate for SmartRecommendations cart arm |
SMART_RECOMMENDATIONS | IS_ENABLED_CUSTOMER_TAG | Gate for audience/tag-based smart features |
SMART_RECOMMENDATIONS | IS_ENABLED_LAST_SEEN | Gate for last-seen widget |
RELATED_RECOMMENDATIONS | ENABLED | Gate 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)]];
}| Mode | Int | Meaning | Input passed |
|---|---|---|---|
| Product | 0 | Single-product mode | Last product code added (lastProductCodeIdPosted from update() line 102) — single ID |
| Cart | 1 | Cart-wide mode | Entire cart's product IDs; falls back to single-product if cart is empty |
- Dropdown labels:
eshop.admin.settings.plus_algo_priority.input.product/.cartatapplication/views/admin/plus/plus_algo_priority.php:88-89. Customhas 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 byitemsRecommendationAI(); SmartRecommendations readscart_best_sellers_related_productspopulated ingetComboCartProducts()(around line 366); RelatedRecommendations usesproductData->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
| Arm | recommendationId | recommendationType | Source |
|---|---|---|---|
| AdvisableAi (cart) | from AI response | BasketTrackType::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 id | enum object (not ->label) | Adv_products.php:1101-1106 |
| Custom | conventionally 'cl', not enforced | — | — |
Click-through round trip
- The cart-recommendation modal renders each product link with querystring params
rt(type label) andri(id). Seeassets/main/vue/AfterAddToCartModal.vue:86-87, 170-173. The querystring keys come fromwindow.advAppContext.advAppData.url.recommendationTypeQuerystring/recommendationIdQuerystring. - On the landed page,
Adv_front_controller::checkForRecommendationIdInQuery()readsrt/ri, populates$this->recommendationTypeUsed(re-hydrated viaBasketTrackType::tryFromLabel) and$this->recommendationIdUsed. Referenced fromAdv_products.php::attachProductTracking()at:1115-1123. AdvApiCartController::update()forwards intocartHandle()(:107), which stores these asoptions['recommendation'] = ['id' => …, 'type' => …]on the cart item.- At order creation:
track_id+track_typeare persisted toshop_order_basket.AdvAdvisableAI::purchaseOrder()includesrecommendationIdin the AI purchase event for items withtrack_type == aiRecommendation, closing the attribution loop for ML retraining.
Caching Strategy
| Arm | Cached? | Notes |
|---|---|---|
| AdvisableAi | No | Calls 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. |
| SmartRecommendations | Yes | pscache->model('products_in_cart_model', 'getBestSellersUnion', [$productIds], $this->pscache_ttl) (AdvApiCartController.php:364-369). Page-scoped model-call cache, global (not per-customer). |
| RelatedRecommendations | No | Not cached at the cascade level. |
| Custom | — | Empty by default. |
| History-based AI recs (separate path) | Session | Session-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:
| Layer | File | Role |
|---|---|---|
CoreAIConnector | ecommercen/ai/libraries/CoreAIConnector.php | Loads ADVISABLE_AI.* registry config into EcommerceN\AIConnector\Config\Config; attaches CoreLogger on the advisable-ai Monolog channel |
AdvAIConnector | ecommercen/ai/libraries/AdvAIConnector.php | Public façade: createUser, addToCart, removeFromCart, itemsPurchasedWithItem(s), similarItemsForItem, customerProductsBasedOnTheirHistory, batchPurchase, etc. |
AdvAdvisableAI | ecommercen/ai/libraries/AdvAdvisableAI.php | High-level wrapper: maps product ↔ AI item IDs, injects circuit breaker, manages session/customer user identity, deferred tasks |
Failure & fallback behaviour
- Master gate:
Adv_front_controller::shouldLoadAdvisableAI()returns false ifADVISABLE_AI_FORCE_DISABLEenv is set, the user agent is a bot, orADVISABLE_AI.ENABLEDis false → all AI entrypoints return[]silently. - User gate:
shouldCallAdvisableAI()requirescSData['ai_user']->idto be populated — no AI user means the arm returns[]. - Circuit breaker:
AdvAdvisableAIholds aCircuitBreaker(:19) lazily resolved viadi()->get('circuit_breaker.advisable_ai')inside thecircuitBreaker()method at:444-454(not the constructor). When the breaker is OPEN, connector calls short-circuit tonull/[]. - 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. - Logging: all AI traffic logs on the
advisable-aiMonolog channel (CoreAIConnector::setLogger():31-36). - Deferred tasks: all write events (view, add-to-cart, purchase, bookmark, rating, session switch) are queued to
DeferredTaskRunnerwithmaxCostSeconds = 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 stateafterAddToCartRecommendationProducts. Each product link carriesrt/rivia:recommendation-type/:recommendation-idprops.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 atapplication/config/constants.php:198).Endpoint:
POST api/cart→api/api_cart/index→AdvApiCartController::update()at:92. Routes atapplication/config/routes.php:568-571.Response shape:
jsonState.recommendation = { products: [...], liveData: [...] }. The payload is assembled bysetCartRecommendationToJsonState()atAdvApiCartController.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-dropSaving Algorithm Order (AJAX)
priority() / addToCartPriority()
|
+--> Validate AJAX request
+--> Extract listItem[] from POST
+--> registry->setArray(GROUP, 'ALGO_PRIORITY', '', $data)
+--> Return success messageSaving Related Group Assignments
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_RELATEDDomain Layer
No modern domain layer. Algorithm priorities are stored entirely in the registry.
Architecture
| Component | Path | Purpose |
|---|---|---|
AdvPlusAlgoPrioritySettings | ecommercen/plus/controllers/AdvPlusAlgoPrioritySettings.php | Admin controller (79 lines) |
| Admin view | application/views/admin/plus/plus_algo_priority.php | jQuery-sortable lists + input dropdowns |
AdvApiCartController | ecommercen/api/controllers/AdvApiCartController.php | Cart cascade dispatcher + per-algo methods |
Adv_products | ecommercen/eshop/controllers/Adv_products.php | PDP cascade dispatcher + per-algo methods |
Adv_products_in_cart_model | ecommercen/eshop/models/Adv_products_in_cart_model.php | Best-sellers queries over tmp_shop_order_basket |
Adv_related_product_model | ecommercen/eshop/models/Adv_related_product_model.php | Related-group queries |
AdvUpdateTempOrderBasketTable | ecommercen/job/libraries/AdvUpdateTempOrderBasketTable.php | Cron refreshing tmp_shop_order_basket |
AdvAIConnector / AdvAdvisableAI / CoreAIConnector | ecommercen/ai/libraries/ | Three-layer AI HTTP stack |
BasketTrackType | application/core/BasketTrackType.php | Track-type enum (int + label) |
SmartRecommendationId | application/core/SmartRecommendationId.php | Stable UUIDs for smart widgets |
| Registry | registry table | Stores all algorithm priorities, gates, and input modes |
| Routes | application/config/routes.php:194-195, 209-210 | Route definitions |
| Admin menu | application/config/admin_menu.php:92-98 | ECOMMERCEN 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'atapplication/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.
| Role | Constant | Menu visible | Page accessible |
|---|---|---|---|
| Advisable | AUTH_ROLE_ADVISABLE (1) | Yes | Yes |
| Developer | AUTH_ROLE_DEVELOPER (2) | Yes | Yes |
| Admin | AUTH_ROLE_ADMIN (3) | Yes | Yes |
| All others | — | No | No |
Role constants: application/config/constants.php:99-107.
Client Extension Points
Customalgorithm arm: implementgetRecommendationCustom()/renderProductPageComboProductsCustom()in a client subclass or DI alias ofAdvApiCartController/Adv_products. UseBasketTrackType::clientRecommendation()('cl') for attribution.- Override controllers: extend
AdvPlusAlgoPrioritySettingsinapplication/modules/plus/controllers/for admin-side customisation. - Legacy override path:
application/modules/api/controllers/Api_cart.phpcan subclass the cart controller in client repos without touchingecommercen/. - 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
- Two recommendation contexts: product page combo and after-add-to-cart each have independent priority lists and independent kill-switches.
- Four algorithms:
RelatedRecommendations,AdvisableAi,SmartRecommendations,Custom— seeded byInitialSeed::plusAlgoPriority(). - 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.
- Drag-and-drop ordering: algorithm priority is set via jQuery sortable, saved through AJAX to
registry->setArray(context, 'ALGO_PRIORITY', '', $data). - Related-group binding:
RelatedRecommendationsuses curated group IDs bound per context (*_RELATED_IDregistry arrays). The winning group id is recorded asrecommendationId. - Input modes (cart cascade only): AdvisableAi, SmartRecommendations, and RelatedRecommendations can each be fed either the last-added product or the full cart;
Customhas no dropdown. PDP cascade does not use input modes — each arm has its own anchor. - Attribution: every rendered recommendation carries
recommendationType(ai/smart/rlr/cl) andrecommendationId, propagated viart/riURL params, stored on the cart item, and persisted toshop_order_basket.track_type/track_idat order creation. - 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.
- SmartRecommendations is cached:
pscache->model(...)wrapsgetBestSellersUnionwith$this->pscache_ttl. - Graceful degradation: any failure in AdvisableAi (master gate, user gate, circuit breaker, timeout) returns
[]and lets the next arm serve the recommendation. - Cascade scope: only
PRODUCT_PAGE_COMBOandAFTER_ADD_TO_CARTuse the cascade. Home / minicart / category / checkout recommendations areai_positions-driven and bypassALGO_PRIORITYentirely — see CF-34. - Registry-based storage: all configuration uses the registry pattern — no dedicated tables.
Known Issues & Security Gaps
Customalgorithm arm is empty by default --getRecommendationCustom()atecommercen/api/controllers/AdvApiCartController.php:409-413andrenderProductPageComboProductsCustom()atecommercen/eshop/controllers/Adv_products.php:1109-1113both return[]. The arm exists only as a client extension point; enabling it without a client override produces no recommendations.- AdvisableAi arm is never positively cached -- every cascade evaluation calls through
AdvAdvisableAIlive. 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. massUpdate()invokes the cascade unconditionally --ecommercen/api/controllers/AdvApiCartController.php:162callssetCartRecommendationToJsonState()without theaatcrquery-string gate or therequestRecommendationflag thatupdate()applies at:112-114. Any batch cart update triggers the full recommendation cascade.BasketTrackType::clientRecommendation()reserved but not enforced -- clients using theCustomarm conventionally use label'cl'(value4) fromapplication/core/BasketTrackType.php, but nothing in the platform validates that custom implementations actually set this track type.- 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_positionstable rows. Admins configuringALGO_PRIORITYmay assume it affects all recommendation surfaces, but it does not.
Related Flows
- AD-02 Product Management Admin — products receive recommendations based on these algorithms
- AD-38 Product Relations — related product groups referenced by
RelatedRecommendations - CF-02 Product Detail Page — storefront display of the PDP combo cascade
- CF-34 AI Recommendations — storefront recommendation rendering for positions that bypass the cascade (home, cart, minicart, category, checkout)