Skip to content

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

  1. Approve/reject links are guest-accessible id URLs with no signature/expiry.
  2. Approval/rejection is performed via GET endpoints (state-changing GET).
  3. Missing null-guards around VisitorOrder lookups can produce runtime failures on invalid ids.
  4. Quote retrieval assumes file existence and does not have explicit fallback behavior.
  5. Visitor-order persistence and distributor orchestration are spread across legacy controllers instead of a single workflow service.
  6. Attachment storage and quote storage are filesystem-coupled with no retention policy in this workflow.
  7. 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

  1. Visitor submissions are persisted as pre-orders before distributor decision.
  2. Approval can create either article lines or wheel-set lines depending on pre-order payload.
  3. Approval is idempotent for already-converted pre-orders.
  4. Rejection notifies visitor and does not create an order.
  5. Channel and customer context from pre-order must be preserved during conversion.

Required Improvements

  1. Replace state-changing GET approve/reject with signed, expiring action endpoints (POST).
  2. Add explicit VisitorOrder state model (pending, approved, rejected, converted) instead of implicit nullable order link.
  3. Introduce robust not-found/invalid-token handling for all distributor actions.
  4. Store quotes as managed documents (or regenerate on demand) instead of unmanaged static files.
  5. Centralize conversion logic in one application service with transaction boundaries and structured audit logs.
  6. Define retention and deletion policy for attachments and generated quote artifacts.

Suggested Target Components

  • VisitorOrderController (capture APIs)
  • DistributorDecisionController (approve/reject/quote actions)
  • VisitorOrderWorkflowService
  • VisitorOrderToOrderConverter
  • VisitorOrderQuoteService
  • VisitorOrderNotificationService

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

  1. Should distributor decisions require authenticated backoffice sessions, signed links, or both.
  2. Should quote artifacts be persisted documents or regenerated from current pricing snapshots.
  3. Should visitor pre-orders have automatic expiration and reminder policies.
  4. Should approval email and order mail become queue-driven to isolate conversion latency.