Webshop - Visitor/Distributor Pre-Order Flow¶
Overview¶
This document specifies the legacy visitor pre-order workflow used by mini-shop pages, the distributor approval/rejection/quote surface, and the VisitorOrder data model that bridges those steps.
Checklist coverage:
- OR-010 Visitor order pre-order workflow
- DM-009 Visitor order entity
- WF-028 Distributor order approve/reject/quote flow
Primary legacy evidence:
- _core/controller/classes/atx/articles/ArticleDetailController.php
- _core/controller/classes/atx/articles/sets/SetDetailController.php
- _core/view/classes/atx/articles/ArticleDetailView.php
- _core/config/router.php
- src/Atraxion/Front/Controller/DistributorController.php
- src/Atraxion/Front/Middleware/FirewallMiddleware.php
- _core/model/classes/atx/customers/VisitorOrder.php
- _core/config/doctrine/customers.VisitorOrder.orm.xml
- templates/distributors/emails/beerens/new-order.twig
- templates/distributors/emails/beerens/new-simple-order.twig
- templates/distributors/emails/beerens/order-approved.twig
- templates/distributors/emails/beerens/order-rejected.twig
Legacy Capabilities (As-Is)¶
1. Mini-Shop Pre-Order Capture (OR-010)¶
Entry points:
- article_processMiniShopOrder in ArticleDetailController
- set_processMiniShopOrder in SetDetailController
UI behavior:
1. Frontend mini-shop form submits via Ajax.
2. On success, response includes persisted visitor-order id (response.data.id).
3. Frontend exposes quote action by opening /distributors/orders/{id}/quote.
Validation behavior:
- Always required: first name, last name, email format.
- Non-distributor channel: reCAPTCHA + terms agreement required.
- Distributor channel: deliver_at, klantnaam, bestelnummer required.
- Optional payment-method validation depends on customer/channel settings.
- TPMS selection requires chassis month/year.
- Optional file upload is accepted and stored in /attachments.
Persistence behavior:
- A VisitorOrder record is persisted for both article and set mini-shop submissions.
- Record stores:
- visitor contact data
- product/set composition snapshot
- quantity and calculated prices
- expected delivery date
- customer/channel context
- optional attachment path
2. Distributor Notification And Pre-Decision State (OR-010, WF-028)¶
After persistence:
- Base URI is built as https://{host}/distributors/orders/{id}.
- Distributor emails include:
- approve URL: {base}/approve
- reject URL: {base}/reject
- Set flow also renders quote HTML and writes it to:
- {APP_STATIC_PATH}/quotes/{id}.html
Mail templates:
- New simple order: new-simple-order.twig
- New wheel-set order: new-order.twig
3. Distributor Endpoints (WF-028)¶
Routes (_core/config/router.php):
- GET /distributors/orders/{id}/approve
- GET /distributors/orders/{id}/reject
- GET /distributors/orders/{id}/quote
Access policy:
- Firewall explicitly allows guest access to ^/distributors/orders.
- Links are id-based GET URLs without signed tokens in legacy.
Approve behavior (DistributorController::approveOrder):
1. Load VisitorOrder by id.
2. Set runtime customer context from VisitorOrder.customer.
3. Idempotency check: if visitorOrder.order exists, return thank-you message with order reference.
4. Create concrete Order and transfer visitor metadata into order notes.
5. Branch:
- Wheel-set flow: reconstruct VehicleSet, validate stock (maximumOrderQuantity), create VehicleSetLineItem.
- Simple article flow: validate quantity against summed article stock, create order article line.
6. Add optional attachment URL to order note.
7. Link visitorOrder.order = order and persist.
8. Update order reference with distributor order number suffix.
9. Send "approved" mail to visitor and internal order mail dispatch.
Reject behavior (DistributorController::rejectOrder):
- Load VisitorOrder.
- Send rejection mail (order-rejected.twig) to visitor.
- No order is created.
Quote behavior (DistributorController::renderQuote):
- Loads static quote HTML by visitor-order id and streams content.
Legacy Data Model (DM-009)¶
Doctrine entity:
- atx\customers\VisitorOrder
- Table: customer_visitor_order
Core scalar fields:
- givenName, familyName, email, phone, comment
- agreedTerms
- quantity
- price, retailPrice, recytyreCost, assemblyCost
- expectedDeliveryAt
- chassisMonth, chassisYear
- customerName, orderNumber
- createdAt
- paymentMethod
- filePath
Relations:
- tyre -> Article (articleId)
- wheel -> Article (wheelId)
- rearTyre -> Article (rearTyreId)
- rearWheel -> Article (rearWheelId)
- tpms -> Article (tpmsId)
- generation -> VehicleMotor (generationId)
- customer -> Customer (customerId)
- channel -> Channel (channelId)
- order -> Order (orderId, nullable until approval)
Operational role:
- VisitorOrder is a pending/pre-order aggregate.
- Once approved, it becomes linked to a concrete Order.
Current Gaps And Risks To Resolve In Migration¶
- Approve/reject links are guest-accessible id URLs with no signature/expiry.
- Approval/rejection is performed via
GETendpoints (state-changing GET). - Missing null-guards around
VisitorOrderlookups can produce runtime failures on invalid ids. - Quote retrieval assumes file existence and does not have explicit fallback behavior.
- Visitor-order persistence and distributor orchestration are spread across legacy controllers instead of a single workflow service.
- Attachment storage and quote storage are filesystem-coupled with no retention policy in this workflow.
- Approval writes order notes from free-text visitor input; migration should standardize sanitization and auditability.
Target Migration Specification (Symfony)¶
Scope¶
In-scope:
- Mini-shop visitor pre-order capture.
- VisitorOrder persistence model.
- Distributor decision workflow (approve/reject/quote).
- Pre-order to order conversion behavior.
Out-of-scope: - Major redesign of mini-shop UI. - ERP-level fulfillment behavior after order creation.
Domain Rules To Preserve¶
- Visitor submissions are persisted as pre-orders before distributor decision.
- Approval can create either article lines or wheel-set lines depending on pre-order payload.
- Approval is idempotent for already-converted pre-orders.
- Rejection notifies visitor and does not create an order.
- Channel and customer context from pre-order must be preserved during conversion.
Required Improvements¶
- Replace state-changing
GETapprove/reject with signed, expiring action endpoints (POST). - Add explicit
VisitorOrderstate model (pending,approved,rejected,converted) instead of implicit nullable order link. - Introduce robust not-found/invalid-token handling for all distributor actions.
- Store quotes as managed documents (or regenerate on demand) instead of unmanaged static files.
- Centralize conversion logic in one application service with transaction boundaries and structured audit logs.
- Define retention and deletion policy for attachments and generated quote artifacts.
Suggested Target Components¶
VisitorOrderController(capture APIs)DistributorDecisionController(approve/reject/quote actions)VisitorOrderWorkflowServiceVisitorOrderToOrderConverterVisitorOrderQuoteServiceVisitorOrderNotificationService
Acceptance Scenarios (Gherkin)¶
Feature: Visitor and distributor pre-order flow
Scenario: Visitor mini-shop submission creates pre-order
Given a mini-shop visitor submits valid contact and order data
When the pre-order endpoint is called
Then a VisitorOrder record should be stored
And the response should include the pre-order id
Scenario: Distributor receives actionable decision links
Given a VisitorOrder exists for a distributor channel
When notification mail is sent
Then the message should include approve and reject actions bound to that VisitorOrder
Scenario: Approve converts simple article pre-order into order
Given a pending VisitorOrder with article payload and sufficient stock
When distributor approves the pre-order
Then an Order should be created and linked
And the visitor should receive an approval notification
Scenario: Approve converts wheel-set pre-order into order
Given a pending VisitorOrder with wheel-set payload and sufficient set stock
When distributor approves the pre-order
Then a vehicle-set order line should be created and linked
Scenario: Reject keeps pre-order unconverted
Given a pending VisitorOrder
When distributor rejects the pre-order
Then no Order should be created
And the visitor should receive a rejection notification
Scenario: Quote rendering returns stored quote artifact
Given a VisitorOrder with a generated quote document
When quote is requested
Then the quote content should be returned for display/print
Open Decisions¶
- Should distributor decisions require authenticated backoffice sessions, signed links, or both.
- Should quote artifacts be persisted documents or regenerated from current pricing snapshots.
- Should visitor pre-orders have automatic expiration and reminder policies.
- Should approval email and order mail become queue-driven to isolate conversion latency.