Skip to content

Waiting List (Notify When Available)

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

Business Overview

The waiting list lets customers subscribe to receive email notifications when out-of-stock products return to inventory. When stock is replenished, a scheduled cron job emails all subscribers with a direct purchase link.

What customers experience:

  • On an out-of-stock product page, enter email to subscribe
  • reCAPTCHA prevents spam subscriptions
  • System confirms subscription (one per email+product pair)
  • When stock returns, customer receives email in their preferred language

Key business behaviors:

  • One subscription per email-product pair (duplicate prevention via isInWaitingList())
  • Language-scoped notifications (stored per entry, used when sending)
  • Advisable AI bookmark logged for recommendation personalization
  • Notification job groups products by customer email to reduce duplicate emails
  • Admin panel offers two views: per-entry listing and grouped-by-product view
  • Soft delete: admin deletion sets waiting_status=2 (not a hard delete)

API Reference

REST Endpoints

MethodPathAuthDescription
GET/rest/product/waiting-listBackendList entries (filterable, sortable, paginated)
GET/rest/product/waiting-list/{id}BackendGet single entry by ID
GET/rest/product/waiting-list/itemBackendGet single entry by filter
POST/rest/product/waiting-listCustomer (JWT)Subscribe to back-in-stock notifications; email derived from the authenticated customer account; body accepts only productId
POST/rest/product/waiting-list/{id}BackendUpdate an existing entry
DELETE/rest/product/waiting-list/{id}BackendDelete entry

Policy source: application/config/rest_policies.php:583-591

Customer opt-in path (POST /rest/product/waiting-list)

The store() override in src/Rest/Product/Controllers/WaitingList.php implements a customer-specific creation path that differs from the generic backend CRUD:

Auth guard — Returns 401 if the caller is not customer-type (WaitingList.php:142-145). The policy (rest_policies.php:589) pins the method to auth=customer, providing defense-in-depth.

Email derivation — The email is never read from the request body. It is looked up from customer.mail via the JWT-authenticated customer record (WaitingList.php:149-153). Returns 404 if the customer account has no mail. This prevents a customer from subscribing an arbitrary third-party address.

Server-forced fields — The following fields are always overwritten server-side regardless of what the body contains (WaitingList.php:155-164):

  • email — derived from the authenticated customer account
  • waiting_status — forced to 0 (pending)
  • creation_date — forced to now()
  • lang — derived from $this->language_abbr
  • email_sent_date — forced to null

Only productId is read from the request body.

DedupcreateForCustomer() calls Repository::existsOpenForEmailAndProduct() before inserting (WriteService.php:57; Repository.php:25-32). The check matches only OPEN (status=0) opt-ins for the same email + product_id pair, mirroring the legacy isInWaitingList() behavior. A previously-notified (status=1) or dropped (status=2) row does not block a fresh opt-in. Returns 422 on duplicate or invalid product.

Reads stay backend-onlyindex, show, and item remain backend-only because the resource exposes subscriber email addresses (PII). See policy comment at application/config/rest_policies.php:583-585.

Email-keyed design note

The shop_waiting_list table has no customer_id column (legacy design). Deduplication is therefore keyed on (email, product_id, waiting_status=0), not on customer id. This is deliberate parity with the legacy isInWaitingList behavior and means that a customer who unsubscribes (status=2) or was already notified (status=1) can re-subscribe.

Legacy AJAX

MethodPathDescription
POST (AJAX)/waiting_list/addSubscribe to product notification

Legacy Admin

URLDescription
/waiting_list_admin/index/{offset}Paginated list of all pending subscriptions (50/page)
/waiting_list_admin/group_products/{offset}Pending subscriptions grouped by product with counts (300/page)
/waiting_list_admin/delete_waiting_list_row/{id}Soft-delete a waiting list entry

Code Flow

Step 1: Customer Subscribes (AJAX)

File: ecommercen/eshop/controllers/Adv_waiting_list.php

  1. Guard: Reject non-AJAX requests with generic error
  2. Validate: product (natural number, required), email (valid email, required), reCAPTCHA via setCaptchaValidationRule()
  3. Product verify: getMasterRecords([$productId]) — error if product not found
  4. Duplicate check: isInWaitingList($email, $productId) — checks only pending entries (waiting_status=0)
  5. Insert: via waiting_list_model->add() — sets waiting_status=0, lang=current, creation_date=now()
  6. AI bookmark: bookmarkToAdvisableAI($productId) — deferred task for recommendation personalization
  7. Response: JSON {status: 'success'|'warning'|'danger', title, msg, db_id}

Step 2: Notification Job Sends Emails

File: ecommercen/job/libraries/AdvSendEmailForWaitingList.php

  1. Language param: Accepts optional lang option (defaults to site default language)
  2. Query eligible: getRecordsToSendEmails($lang) — joins shop_waiting_list with shop_product, product_codes, shop_product_mui, shop_vendor/shop_vendor_mui — filters: waiting_status=0, product.active=1, stock > 0 OR negative_stock=1, matching language
  3. Group by email: Collects all products per email address to send a single multi-product email
  4. Send: adv_mailer->sendEmailForWaitingList($email, ['products' => $data], $lang) per customer
  5. Mark sent: markAsSent($ids) — sets waiting_status=1, email_sent_date=now() for all processed IDs

Step 3: Admin Views

File: ecommercen/eshop/controllers/Adv_waiting_list_admin.php

  • Authorization: Requires AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN, or AUTH_ROLE_PRODUCTS
  • index(): Paginated list of unsent entries (50/page) — shows email, product name, creation date, status
  • group_products(): Grouped view showing product name, customer count, product codes, barcodes (300/page) — sorted by subscriber count descending
  • delete_waiting_list_row($id): Sets waiting_status=2 (soft delete), then redirects to index

Domain Layer

ComponentPath
Servicesrc/Domains/Product/WaitingList/Service.php
WriteServicesrc/Domains/Product/WaitingList/WriteService.php
Entitysrc/Domains/Product/WaitingList/Repository/Entity.php
Legacy Controllerecommercen/eshop/controllers/Adv_waiting_list.php
Admin Controllerecommercen/eshop/controllers/Adv_waiting_list_admin.php
Legacy Modelecommercen/eshop/models/Adv_waiting_list_model.php
Notification Jobecommercen/job/libraries/AdvSendEmailForWaitingList.php

Data Model

shop_waiting_list

ColumnTypeDescription
idint (PK, AI)Entry ID
emailvarchar(254)Subscriber email address
langvarchar(3)Language code at time of subscription
product_idint (FK)Product being watched
creation_datedatetimeWhen the subscription was created
waiting_statustinyint0=pending, 1=email sent, 2=deleted/dropped
email_sent_datedatetime (nullable)When the notification email was sent

Indexes: email, email+lang, product_id+waiting_status


Client Extension Points

  • Validation: Override validation() in Adv_waiting_list for custom rules
  • AI tracking: Override bookmarkToAdvisableAI() on the front controller
  • Notification job: Override AdvSendEmailForWaitingList by extending in application/modules/job/libraries/
  • Email template: Customize via adv_mailer->sendEmailForWaitingList() template
  • Feature toggle: Registry enable_waiting_list
  • Admin views: {client_views}/waiting_list/waiting_list and group_products templates

Known Issues & Security Gaps

  1. Backend store/update validation is a no-op. Validator::validateForCreate() and validateForUpdate() both build an empty $errors array and throw nothing (src/Domains/Product/WaitingList/Validator.php:9-25). Admin CRUD paths (create() in WriteService.php:20-30) therefore accept any values for email, waiting_status, product_id, etc. without enforcement. Only createForCustomer() enforces rules (WriteService.php:42-62).

  2. No HTTP-layer unit test for the store() override. The 401-guard (WaitingList.php:142-145), 404-on-missing-mail (WaitingList.php:150-153), and JWT→email derivation (WaitingList.php:149-153) have no controller-level test coverage. Business rules are exercised only at the WriteService layer via createForCustomer().