Skip to content

Wishlist

Flow ID: CF-17 | Module(s): eshop, Product domain | Complexity: Low | Last Updated: 2026-07-12

Business Overview

Logged-in customers save products for future purchase. Items added/removed via AJAX or REST, with automatic sync to Advisable AI bookmarks and Facebook Conversion API for retargeting.

What customers experience:

  • Click a heart icon on any product listing or detail page to save it
  • View saved products on /customer/wishlist (paginated, 36 per page)
  • Remove individual items or clear the entire wishlist
  • Toggle add/remove via AJAX with instant UI feedback (icon swap, toast notification)

Key business behaviors:

  • Requires login (redirects to homepage if not authenticated)
  • One entry per customer-product pair (duplicates silently ignored)
  • Syncs with AI bookmarks on add/remove (deferred task)
  • Facebook AddToWishlist event fired server-side on add via Meta Conversions API
  • Google GA4 add_to_wishlist event fired client-side via ecomnTag
  • Paginated display (36 per page) with product code parsing
  • Loyalty points calculated on wishlist page via calcLoyalty() hook

API Reference

REST Endpoints

MethodPathAuthDescription
GET/rest/product/wishlistCustomer JWTList wishlist items (filterable by id, customerId, productId); ?with=product,customer returns the nested relations
GET/rest/product/wishlist/{id}Customer JWTGet single wishlist entry
GET/rest/product/wishlist/itemAdmin/backend JWTGet entry by filter
POST/rest/product/wishlistCustomer JWTAdd product (customer ID from JWT)
POST/rest/product/wishlist/{id}Admin/backend JWTUpdate entry
DELETE/rest/product/wishlist/{id}Customer JWTRemove (verifies ownership via resourceContext->getUserId())

Filters: id, customerId, productId Sorts: id, customerId, productId Relations: customer, product — ?with=product,customer is now fully serialized on index/show/item (ProductWishlistResource previously dropped it)

Data scoping (fixed #418; mechanism updated #452): index() and show() are customer-scoped — enforceWishlistScope() forces the customerId filter to the caller's own JWT id for non-backend requests via the domain-layer mandatory-filter mechanism (withMandatoryFilter(), backed by GenerateListRequest::forceFilter()) rather than a $_GET write, so a customer can no longer pass another customer's id to read their wishlist. Additionally, show() 404s on a row whose customer_id doesn't match the caller. item() is backend-only (RBAC-guarded via rest_policies.php) with no per-row ownership check. Backend/admin access is unfiltered.

Legacy AJAX

MethodPathDescription
POST (AJAX)/customer/wishlistAction/add/{productId}Add product — fires Facebook CAPI + AI bookmark
POST (AJAX)/customer/wishlistAction/remove/{productId}Remove product — removes AI bookmark
GET/POST/customer/wishlist_add/{productId}Add product (older endpoint)
GET/POST/customer/wishlist_remove/{productId}Remove product (older endpoint)
GET/POST/customer/wishlist_remove_allClear entire wishlist

Storefront Pages

URLDescription
/customer/wishlist/{offset}Paginated wishlist display (36/page)

Code Flow

Step 1: Add to Wishlist (AJAX via wishlistAction)

File: ecommercen/eshop/controllers/Adv_customer.php (line ~1131)

  1. Guard: Must be AJAX request with valid session (customer_id)
  2. Duplicate check: getCustomerProductIds($customerId) — skip if already in list
  3. Insert: wishlist_model->add_record({customer_id, product_id})
  4. Facebook CAPI: reportWishlistFacebookConversion($productId, 1) — deferred task
  5. AI bookmark: bookmarkToAdvisableAI($productId) — deferred task
  6. Response: JSON {status: 'success'}

Step 2: Facebook Conversion API Event

File: ecommercen/eshop/controllers/Adv_customer.php (line ~1163)

  1. Guard: Check FACEBOOK_CONVERSION.ENABLED registry setting
  2. Product data: Fetch product name and calculated final price from product_model
  3. Customer data: Fetch from customer_model by session ID
  4. Build payload: content_ids (with tracking prefix), content_name, content_type=product, currency, value
  5. Dispatch: Deferred task via FacebookConversionService->dispatchEvent(EventType::ADD_TO_WISHLIST, ...)

Step 3: Remove from Wishlist

File: ecommercen/eshop/controllers/Adv_customer.php (line ~1081)

  1. Delete: wishlist_model->remove($customerId, $productId)
  2. AI unbookmark: removeBookmarkFromAdvisableAI($productId) — deferred task
  3. Response: OK (text) or JSON depending on endpoint used

Step 4: Display Wishlist Page

File: ecommercen/eshop/controllers/Adv_customer.php (line ~955)

  1. Auth check: Redirect to homepage if no customer_id in session
  2. Fetch products: wishlist_model->get_products_with_paths($customerId, 36, $offset) — joins with product model for full product data
  3. Count: countProductsWithPaths({customer_id}) for pagination
  4. Hook: wishListExtra() — calls calcLoyalty() by default
  5. Render: Product grid with pagination, product code parsing

Step 5: Client-Side Behavior

File: assets/main/js/components/wishlist-actions.js

  1. Click handler: .wishlist_action elements with data-action, data-product_id, data-view_form
  2. AJAX POST: To /customer/wishlistAction/{action}/{productId} with optional AI recommendation ID
  3. GA4 event: On add, fires ecomnTag.add_to_wishlist_GA4() with product data from Vue state
  4. UI toggle: Swaps action data attribute, changes icon (heart/heart-broken), shows toast notification

Domain Layer

ComponentPath
REST Controllersrc/Rest/Product/Controllers/Wishlist.php
Servicesrc/Domains/Product/Wishlist/Service.php
WriteServicesrc/Domains/Product/Wishlist/WriteService.php
Validatorsrc/Domains/Product/Wishlist/Validator.php
WriteDatasrc/Domains/Product/Wishlist/WriteData.php
Entitysrc/Domains/Product/Wishlist/Repository/Entity.php
Repositorysrc/Domains/Product/Wishlist/Repository/Repository.php
WriteRepositorysrc/Domains/Product/Wishlist/Repository/WriteRepository.php
Legacy Modelecommercen/eshop/models/Adv_wishlist_model.php
JS Componentassets/main/js/components/wishlist-actions.js

Dual path: Legacy AJAX for storefront + REST for headless/admin.


Data Model

wishlist

ColumnTypeDescription
idint (PK, AI)Wishlist entry ID
customer_idint (FK)Customer who saved the product
product_idint (FK)Saved product reference

No timestamps on this table.


Client Extension Points

  • wishListExtra() hook: Override to add loyalty, upsell data (default calls calcLoyalty())
  • Facebook tracking: Override reportWishlistFacebookConversion() for custom event data
  • AI sync: Override bookmarkToAdvisableAI() / removeBookmarkFromAdvisableAI() methods
  • GA4 tracking: Client-side via ecomnTag.add_to_wishlist_GA4() in wishlist-actions.js
  • View: {client_views}/customer/wishlist/ — templates for wishlist page
  • REST ownership: destroy() in Wishlist controller verifies customer_id matches JWT user via enforceWishlistScope(). For reads, index() and show() are customer-scoped through the domain-layer mandatory-filter mechanism (withMandatoryFilter() / GenerateListRequest::forceFilter(), #452) instead of a $_GET mutation; show() additionally 404s on per-row ownership mismatch. item() is backend-only (RBAC-guarded, no per-row check). A client fork that overrides src/Rest/Product/Controllers/Wishlist.php must use the mandatory-filter mechanism (or equivalent per-row ownership checks) to re-scope reads, and must preserve the show() per-row ownership 404, or it will reintroduce the cross-customer wishlist data leak fixed in #418

Known Issues & Security Gaps

  1. #451 — Wishlist::update() OA annotation contradicts enforced RBAC (src/Rest/Product/Controllers/Wishlist.php:176-195, application/config/rest_policies.php:736-744, src/Rest/Support/Controllers/HandlesWriteActions.php:54-87): The OpenAPI annotation for the update endpoint claims the method is customer-scoped (summary "Update a wishlist entry (customer-scoped)", bearerAuth, 401 "Customer authentication required"), but rest_policies.php restricts it to backend admins (AUTH_ROLE_ADMIN, AUTH_ROLE_PRODUCTS). The implementation in doUpdate() performs only an existence check with no per-row customer_id ownership guard, unlike destroy() which validates ownership. If the policy were ever aligned to the OA annotation (allowing customers), arbitrary wishlist rows would be writable by any authenticated customer — a write-layer analogue of the #418 read leak. Currently mitigated by RBAC enforcement.