Appearance
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
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/product/waiting-list | Backend | List entries (filterable, sortable, paginated) |
| GET | /rest/product/waiting-list/{id} | Backend | Get single entry by ID |
| GET | /rest/product/waiting-list/item | Backend | Get single entry by filter |
| POST | /rest/product/waiting-list | Customer (JWT) | Subscribe to back-in-stock notifications; email derived from the authenticated customer account; body accepts only productId |
| POST | /rest/product/waiting-list/{id} | Backend | Update an existing entry |
| DELETE | /rest/product/waiting-list/{id} | Backend | Delete 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 accountwaiting_status— forced to0(pending)creation_date— forced tonow()lang— derived from$this->language_abbremail_sent_date— forced tonull
Only productId is read from the request body.
Dedup — createForCustomer() 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-only — index, 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
| Method | Path | Description |
|---|---|---|
| POST (AJAX) | /waiting_list/add | Subscribe to product notification |
Legacy Admin
| URL | Description |
|---|---|
/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
- Guard: Reject non-AJAX requests with generic error
- Validate:
product(natural number, required),email(valid email, required), reCAPTCHA viasetCaptchaValidationRule() - Product verify:
getMasterRecords([$productId])— error if product not found - Duplicate check:
isInWaitingList($email, $productId)— checks only pending entries (waiting_status=0) - Insert: via
waiting_list_model->add()— setswaiting_status=0,lang=current,creation_date=now() - AI bookmark:
bookmarkToAdvisableAI($productId)— deferred task for recommendation personalization - Response: JSON
{status: 'success'|'warning'|'danger', title, msg, db_id}
Step 2: Notification Job Sends Emails
File: ecommercen/job/libraries/AdvSendEmailForWaitingList.php
- Language param: Accepts optional
langoption (defaults to site default language) - Query eligible:
getRecordsToSendEmails($lang)— joinsshop_waiting_listwithshop_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 - Group by email: Collects all products per email address to send a single multi-product email
- Send:
adv_mailer->sendEmailForWaitingList($email, ['products' => $data], $lang)per customer - Mark sent:
markAsSent($ids)— setswaiting_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, orAUTH_ROLE_PRODUCTS index(): Paginated list of unsent entries (50/page) — shows email, product name, creation date, statusgroup_products(): Grouped view showing product name, customer count, product codes, barcodes (300/page) — sorted by subscriber count descendingdelete_waiting_list_row($id): Setswaiting_status=2(soft delete), then redirects to index
Domain Layer
| Component | Path |
|---|---|
| Service | src/Domains/Product/WaitingList/Service.php |
| WriteService | src/Domains/Product/WaitingList/WriteService.php |
| Entity | src/Domains/Product/WaitingList/Repository/Entity.php |
| Legacy Controller | ecommercen/eshop/controllers/Adv_waiting_list.php |
| Admin Controller | ecommercen/eshop/controllers/Adv_waiting_list_admin.php |
| Legacy Model | ecommercen/eshop/models/Adv_waiting_list_model.php |
| Notification Job | ecommercen/job/libraries/AdvSendEmailForWaitingList.php |
Data Model
shop_waiting_list
| Column | Type | Description |
|---|---|---|
id | int (PK, AI) | Entry ID |
email | varchar(254) | Subscriber email address |
lang | varchar(3) | Language code at time of subscription |
product_id | int (FK) | Product being watched |
creation_date | datetime | When the subscription was created |
waiting_status | tinyint | 0=pending, 1=email sent, 2=deleted/dropped |
email_sent_date | datetime (nullable) | When the notification email was sent |
Indexes: email, email+lang, product_id+waiting_status
Client Extension Points
- Validation: Override
validation()inAdv_waiting_listfor custom rules - AI tracking: Override
bookmarkToAdvisableAI()on the front controller - Notification job: Override
AdvSendEmailForWaitingListby extending inapplication/modules/job/libraries/ - Email template: Customize via
adv_mailer->sendEmailForWaitingList()template - Feature toggle: Registry
enable_waiting_list - Admin views:
{client_views}/waiting_list/—waiting_listandgroup_productstemplates
Known Issues & Security Gaps
Backend
store/updatevalidation is a no-op.Validator::validateForCreate()andvalidateForUpdate()both build an empty$errorsarray and throw nothing (src/Domains/Product/WaitingList/Validator.php:9-25). Admin CRUD paths (create()inWriteService.php:20-30) therefore accept any values foremail,waiting_status,product_id, etc. without enforcement. OnlycreateForCustomer()enforces rules (WriteService.php:42-62).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 theWriteServicelayer viacreateForCustomer().
Related Flows
- CF-02 Product Detail — waiting list form on out-of-stock products
- CF-11 Customer Account — customers may see waiting list status in their account area
- SY-13 Waiting List Notifications — cron job that sends stock-back emails
- SY-24 Email Dispatch —
adv_maileremail delivery infrastructure