Appearance
Product Detail Page
Flow ID: CF-02 | Module(s): eshop, variations, Product domain | Complexity: High Last Updated: 2026-05-12
Business Overview
The product detail page (PDP) is the primary conversion page. It displays complete product information — pricing with discount logic, image galleries, variation matrices (size/color switchers), customer reviews, related products, and bundle offers — driving the "Add to Cart" action.
What customers experience:
- Full product details: name, description, pricing (original vs. discounted), VAT
- Image gallery with per-SKU images
- Variation switcher (e.g., click "Red" → loads different product with red images/price)
- Star rating average + individual reviews
- Related products (fallback chain: curated list → product line → category → vendor)
- Bundle offers, AI recommendations, blog articles
- "Add to Cart" with variant/quantity selection
Key business behaviors:
- Price calculation:
final_price = price × (1 - discount%) × (1 + VAT%) - Special discounts override regular discounts when active (
special_from/todate range) - Stock = sum of all active product code stocks;
negative_stockflag allows pre-orders cart_limitfield caps per-product quantity- Hit counter incremented on every view
- Canonical URL format:
/vendors/{vendor_slug}/{product_slug}
API Reference
REST Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/product/product | Guest | List products (filterable, sortable, paginated) |
| GET | /rest/product/product/{id} | Guest | Get product by ID with relations |
| GET | /rest/product/product/item | Guest | Get single product by filter |
| POST | /rest/product/product | Backend (Products) | Create product |
| POST | /rest/product/product/{id} | Backend (Products) | Update product |
| DELETE | /rest/product/product/{id} | Backend (Products) | Delete product |
Filters: id, vatId, vendorId, badgeId, vendorCode, active, softDelete, negativeStock, name.{locale}, slug.{locale}, barcode
Sorts: id, price, hits, dateChanged, name.{locale}, slug.{locale}
Relations: translations, vendor, vat, shelfcode, badge, productCodes, barcodes, categories, tags, lines, variationValues, videos, events, articles
Browse endpoints interactively in the API Reference.
Legacy Storefront
| URL | Controller | Method |
|---|---|---|
/{category-slug}/{product-slug} | Adv_eshop.php → Adv_products.php | index($catSlug, $slug) |
/vendors/{vendor-slug}/{product-slug} | same | Canonical format |
Code Flow
Step 1: URL Resolution
File: Adv_eshop.php::baseProduct() (line 57-81)
Splits URL at last /. Checks product exists via existsBySlug() (cached, slug_lang index). Falls back to findSlugsByUrl() for old URL redirects.
Step 2: Product Data Loading
File: Adv_products.php::index() (line 45-217)
product_model->get_mui_record() joins 8 tables: shop_product → shop_product_mui → shop_product_vats → shop_vendor → shop_vendor_mui → shop_line → shop_line_mui. Returns 43+ fields.
Step 3: Price Calculation
File: Adv_product_parser_model.php::setPrices() (line 464-479)
original_price = price × (1 + VAT%)
If discount: final_price = price × (1 - discount%) × (1 + VAT%)
save_price = original_price - final_priceSpecial discount: if ENABLE_SPECIAL_DISCOUNTS AND now between special_from/to, uses special_discount_percent instead.
Step 4: Variations & SKUs
File: AdvVariationModel.php::getVariationByProductId() (line 19-66)
Loads variation matrix. If product in a variation group, getVariationGroupProducts() loads ALL sibling products with live data for frontend switcher.
Step 5: Related Products
File: Adv_related_product_model.php::getRelatedProductsFront() (line 85-103)
Fallback chain: product list → product line → category → vendor. All filtered by: active=1, soft_delete=0, stock>0, price>0.
Step 6: View Assembly
indexExtras() (line 222) orchestrates: reviews, hit tracking, last-seen, related products, Criteo/Agora/Manago integrations, AI recommendations, bundle offers, blog articles.
Domain & REST Architecture
| Component | Path |
|---|---|
| Service | src/Domains/Product/Product/Service.php |
| Repository | src/Domains/Product/Product/Repository/Repository.php |
| Entity | src/Domains/Product/Product/Repository/Entity.php |
| MUI Repository | src/Domains/Product/Product/Repository/MuiRepository.php |
| WriteService | src/Domains/Product/Product/WriteService.php |
| Configurator | src/Domains/Product/Product/Repository/RepositoryConfigurator.php |
Not yet migrated to Domain: Adv_product_parser_model (pricing), Adv_related_product_model (relations), AdvVariationModel (variations).
Client Extension Points
Controller Hooks
| Hook | Purpose |
|---|---|
indexExtras() (line 222) | Add reviews, recommendations, tracking, bundles |
Model Overrides
| Model | Methods |
|---|---|
Adv_product_parser_model | setPrices(), setAvailabilityStock(), setDiscountDisclosure() |
Adv_related_product_model | getRelatedProductsFront() and individual fallback methods |
AdvVariationModel | getVariationByProductId(), getVariationGroupProducts() |
Configuration
| Key | Purpose |
|---|---|
ENABLE_SPECIAL_DISCOUNTS | Time-limited special discount feature |
OTHER.RELATED_PRODUCTS_LIMIT | Max related products displayed |
CRITEO.IS_ENABLED | Criteo tracking |
AGORA.DSPL_PRODUCT_IS_ENABLED | Product page ads |
MANAGO.ENABLE_RECOMMENDATIONS | Manago product suggestions |
Data Model
For the full shop_product column schema (40+ columns), see AD-02 Product Management.
shop_product_mui — Product translations
| Column | Type | Description |
|---|---|---|
product_id | INT | FK to shop_product |
lang | VARCHAR(2) | Language code |
name | VARCHAR(255) | Translated product name |
slug | VARCHAR(255) | URL slug (indexed with lang as slug_lang) |
description | TEXT NULL | Full product description (HTML) |
short_description | TEXT NULL | Summary description |
meta_title | VARCHAR(255) NULL | SEO title |
meta_description | TEXT NULL | SEO description |
For the full product_codes column schema, see AD-02 Product Management.
Other tables involved
| Table | Purpose |
|---|---|
shop_product_vats | VAT rates (vat_percent) |
shop_vendor / shop_vendor_mui | Vendor data and translations |
product_variations / product_variation_values | Variation matrix (size, color groups) |
product_media | Images and videos per product/product code |
shop_product_reviews | Customer reviews (rating, text, approved flag) |
shop_product_related_group_lp | Related product links (curated groups) |
shop_line / shop_line_mui | Product lines (for related product fallback) |
Vue Frontend Features
Product Price Chart
Renders a historical price evolution chart on the product detail page using ApexCharts. The feature is part of the SY-04 price tracking system and is gated by registry settings (productChartSettings.enabled, productChartSettings.enabledGraphs or enabledAllGraphs). The PHP view conditionally renders the component when $priceTrackingOptions->enabledGraphOnProductPage is true.
What customers see: A "Price Evolution" button (line-chart icon) near the product price area. Clicking it opens a modal containing a smooth area chart of historical prices over the configured tracking period, with dates on the X-axis and prices on the Y-axis. The chart highlights the most recent data point with an orange marker.
Component architecture:
| Component | Path | Purpose |
|---|---|---|
ProductPriceChartModal | assets/main/vue/product/ProductPriceChartModal.vue | Button + GenericModal wrapper; controls visibility via shouldDisplay computed and Vuex settings |
ProductPriceChart | assets/main/vue/product/ProductPriceChart.vue | ApexCharts area chart renderer; fetches and processes price data |
productChartModule | assets/vue/store/productChart/productChartModule.js | Namespaced Vuex module storing per-product chart state (loading, options, series, prices, referencePrice) |
Data flow:
ProductPriceChartModalchecksproductChartSettingsfrom Vuex (initialized fromwindow.advAppData.productChartSettings) to decide whether to render.- On button click,
openModal()opens theGenericModaland emitsupdateChartto the child. ProductPriceChart.updateChart()first checks if data was already fetched (deduplication). If initial data exists in the Vuexproductsarray (server-renderedpriceTracking.prices), it uses that directly viasetFromInitialData().- Otherwise, it fetches from
GET /rest/product/price-tracking/graph/{productId}(guest-accessible REST endpoint routed toAdvisable\Rest\Product\Controllers\PriceTracking::graph(), which delegates toAdvisable\Domains\Product\PriceTracking\GraphService::productPrices()). - The response contains
prices(array of{date, price}) andreferencePrice. These are stored in the Vuex module keyed by product ID and rendered as an ApexCharts area series.
Price tracking backend:
Advisable\Domains\Product\PriceTracking\GraphService(src/Domains/Product/PriceTracking/GraphService.php) loads price changes from theprice_trackingRepository, limits them to the configured tracking period, builds a daily price timeline, and determinesreferencePrice,saveValue, anddiscountPassper EU Omnibus rules.Advisable\Domains\Product\PriceTracking\Options(src/Domains/Product/PriceTracking/Options.php) reads registry settings for the tracking period length, enabled status, and per-page graph toggles. Resolved viadi()->get(Options::class)— same public property shape as the legacy DTO so view templates are unchanged.
Integration with AdvProductCard: The AdvProductCard component also reads productChartSettings to determine discount display behavior. When price tracking is enabled and priceTracking.shouldDisplay and discountPass are true, the card shows the reference price and save amount from price tracking data instead of the simple old/new price comparison.
PHP view integration:
php
<!-- application/views/main/layouts/product_page/product.php -->
<?php if ($priceTrackingOptions->enabledGraphOnProductPage) : ?>
<product-price-chart-modal :product-id="<?= $productData->product_id; ?>"></product-price-chart-modal>
<?php endif; ?>Video Reels (Stream Slider)
Displays short-form video reels with linked products, similar to social media story/reel formats. Videos are hosted on Bunny CDN and played via HLS streaming. The feature appears on the homepage as a horizontal slider and has a dedicated /reels page with pagination.
What customers see: A horizontally scrollable row of video thumbnail cards with titles. Clicking a thumbnail opens a fullscreen modal with the video playing on the left and a list of featured products (with images, prices, and "Add to Cart" buttons) on the right. Users can navigate between videos using prev/next buttons, mouse wheel scrolling, or touch swipe gestures. On mobile, the product list appears as a horizontal slider overlaying the bottom of the video.
Component architecture:
| Component | Path | Purpose |
|---|---|---|
HomeStreamSlider | assets/main/vue/streamSlider/HomeStreamSlider.vue | Homepage slider variant; reads data from window.advAppContext.advAppData.homeStreamSliderData; uses NativeSlider for horizontal scrolling |
Reels | assets/main/vue/streamSlider/Reels.vue | Dedicated /reels page variant with server-side pagination; fetches pages via GET /api/reels?page={n}&perPage=12 |
StreamSlide | assets/main/vue/streamSlider/StreamSlide.vue | Individual thumbnail card; shows video thumbnail or animated preview; handles hover/click/intersection events |
StreamModal | assets/main/vue/streamSlider/StreamModal.vue | Fullscreen playback modal; two-column layout (video + products on desktop); prev/next navigation; mute toggle; touch swipe support |
VideoPlayer | assets/main/vue/streamSlider/VideoPlayer.vue | Video.js wrapper supporting HLS (application/x-mpegURL), MP4, and DASH; hover-preview mode; autoplay with mute |
StreamSliderMixin | assets/main/vue/streamSlider/StreamSliderMixin.js | Shared mixin providing v-intersect directive, slide navigation, scroll handling, view tracking (postHit), and i18n |
Data flow:
- Server-side:
Adv_home(homepage) orAdv_reels(reels page) loads videos fromvideo_showcase_model, decorates them with Bunny CDN URLs viaVideoManager, and enriches linked products with live pricing, stock, and image data. - Video data is injected into the Vue app via
window.advAppContext.advAppData(eitherhomeStreamSliderDataorstreamSliderData). - Each video object contains:
id,name,cdn_video_id,playListUrl(HLS),playUrl,video_thumbnail,preview(animated), and aproductsarray withid,name,url,image,priceWithSign. StreamSlideuses IntersectionObserver to preload preview animations as slides scroll into view.- When a slide is clicked,
StreamModalopens fullscreen, initializesVideoPlayerwith the HLS playlist URL, and renders the product list withAdvButtonViewBuycomponents for add-to-cart functionality. - View tracking:
StreamSliderMixin.postHit()sends a POST to/video_showcase_admin/record_viewwith the video ID whenever playback starts.
Backend:
| Component | Path | Purpose |
|---|---|---|
Adv_reels | ecommercen/eshop/controllers/Adv_reels.php | Reels page controller; paginated video listing with product enrichment |
AdvVideoShowcaseAdmin | ecommercen/video/controllers/AdvVideoShowcaseAdmin.php | Admin CRUD for video showcase entries |
video_showcase_model | (loaded via CI) | Database queries for videos and linked products |
VideoManager / BunnyStream | src/VideoStream/ | CDN URL resolution for playback, thumbnails, and previews |
Routes:
| URL | Controller | Method |
|---|---|---|
/reels | Adv_reels | index() — server-rendered page |
/api/reels | Adv_reels | getPaginatedVideos() — JSON pagination API |
Registry configuration:
| Key | Purpose |
|---|---|
VIDEOSHOWCASE.ENABLED | Master toggle for the video showcase feature |
VIDEOSHOWCASE.PROVIDER | Video CDN provider (currently BUNNY) |
PHP view integration:
php
<!-- application/views/main/components/sliders/stream_slider.php -->
<home-stream-slider :show-add-to-cart="true" :see-more="false"></home-stream-slider>Quick View Modal
Provides a popup product card that lets customers view product details, select attributes/variations, and add to cart without leaving the current listing page. The modal is rendered once globally in the main layout and shared across all pages.
What customers see: On product listing pages, clicking the eye icon (quick view button) or the "Add to Cart" button on a product with attributes/customizations opens a floating modal overlay. The modal shows the product image, name, description, product code, barcode, pricing (with discount and savings), attribute/variation selectors, quantity spinner, and an "Add to Cart" button. Clicking the close icon or adding to cart dismisses the modal.
Component architecture:
| Component | Path | Purpose |
|---|---|---|
AdvProductCard | assets/main/vue/AdvProductCard.vue | The modal card itself; full mini-PDP with image, pricing, attributes, variations, customizations, and cart actions |
AdvButtonViewBuy | assets/main/vue/AdvButtonViewBuy.vue | Listing-page button that triggers the modal via openProductCardModal Vuex action; shortcuts to direct add-to-cart when product has only one code and no customizations |
Vuex state (global store):
state.productCardModal—{ open: boolean, productId: number|null }controls modal visibilityopenProductCardModal(productId)mutation — setsproductIdandopen = truecloseProductCardModal()mutation — resets to closed stateproductCardModalgetter — exposes state for the view template'sv-showbinding
Data flow:
AdvButtonViewBuyis rendered per product in listing grids. When clicked,toggleQuickView()checks if the product has multiple codes or customizations. If so, it dispatchesopenProductCardModal(productId)to Vuex.- The global
<adv-product-card>inapplication/views/main.phpis bound toproductCardModal.productIdandv-show="productCardModal.open". It renders in modal mode (:modal="true"), showing image, content, and name. AdvProductCardusesproductMixinto load product data from the Vuexproductsarray (server-rendered). It auto-selects the first product code if no attributes exist, and sets the main image per the selected product code.- Price display integrates with price tracking: when
productChartSettings.enabledis true and the product has valid tracking data (priceTracking.shouldDisplayanddiscountPass), it shows the reference price and save value from the tracking system. Otherwise, it falls back to standard old/new price display. - The card supports
AttributeSelect(product code attributes like size/color),VariationSelect(variation group switching),AdvCartProductCustomizationEditor(schema-based product customizations), andAdvProductInputSpinner(quantity with stock limits). - On "Add to Cart",
addToCard()dispatchesaddToCartClickedwith the selected product code ID, quantity, and optional recommendation tracking. If customizations are active, they are validated and included in the payload. closeModal()emitsclose-quick-view, which the parent template handles viacloseProductCardModal.
PHP view integration:
html
<!-- application/views/main.php (global, rendered once) -->
<adv-product-card
:modal="true"
:product-id="productCardModal.productId"
:show-image="true"
:show-content="true"
:show-name="true"
v-on:close-quick-view="closeProductCardModal"
v-show="productCardModal.open">
</adv-product-card>Key behaviors:
- The modal is a singleton rendered once in the layout; the displayed product changes by updating
productCardModal.productIdin Vuex. - Stock-aware: disables add-to-cart when stock is zero or cart limit is reached; shows unavailability messages for inactive or out-of-stock product codes.
- Availability date awareness: respects
availableFrom/availableUntilfields to hide the cart button for time-restricted products. - Note: On the PDP itself,
AdvProductCardis used inline (non-modal,:modal="false") as the add-to-cart widget withshow-content,show-name, andshow-imageall set to false — only the attribute selectors and cart button are visible.
Known Issues & Security Gaps
- Price chart API endpoint updated (2026-05-12): The Vue component
ProductPriceChart.vuepreviously called the legacyGET /api/priceTracking/productPrices/{productId}endpoint, which was deleted in PR #128. The component has been updated to call the new REST endpointGET /rest/product/price-tracking/graph/{productId}. File:assets/main/vue/product/ProductPriceChart.vue:232.
No other known security gaps at time of writing.
Tests
The price-chart Vue component (ProductPriceChart.vue, ProductPriceChartModal.vue) and the productChartModule Vuex store do not have automated unit tests. The backend graph logic is covered by tests/Unit/Domains/Product/PriceTracking/GraphServiceTest.php (Omnibus helpers) and tests/Unit/Domains/Product/PriceTracking/PriceCalculatorTest.php (VAT + discount math).
Related Flows
- CF-01 Product Browsing — category grid links to PDP
- CF-03 URL Routing — product URL resolution
- CF-05 Cart Management — "Add to Cart" action
- CF-12 Promotions — promo landing pages with product grids
- CF-16 Product Reviews — review display
- CF-24 Product Bundles — bundle offers
- CF-25 Variations — variation matrix
- CF-34 AI Recommendations — AI-powered related products
- AD-02 Product Management — admin product CRUD
Wiki Guides: Product queries use PSR-6 caching — see Cache System. Product images served via Storage Abstraction.
Shared Patterns
- SY-04 Price Tracking — historical price evolution powering the price chart feature
- SY-07 Cache Management — cached product data and invalidation
- SY-23 MUI Translation Pattern — multi-language product names, descriptions, and slugs
- SY-28 Storage Abstraction — product images and video thumbnail storage