Appearance
Order Confirmation (Checkout View)
Flow ID: CF-07 | Module(s): eshop, Checkout domain | Complexity: Very High Last Updated: 2026-05-29
Business Overview
After the preview form (CF-06), the checkout confirmation parses the cart for final totals, validates stock one last time, creates the order record, and dispatches to the payment gateway.
What happens at this step:
- Cart parsed with live pricing, VAT, coupon discount
- Transport and delivery costs calculated based on address/weight
- Loyalty points deducted from customer account (if redeemed)
- Gift items processed (including Rule 13 cheapest-free)
- Order created in PENDING status with serialized cart
- Payment gateway initialized → customer redirected or sees success page
API Reference
Modern REST Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /rest/checkout/place-order | Guest/Customer | Create order from cart, initialize payment, return order ID + redirect URL |
| GET | /rest/checkout/payment-status/{orderId} | Customer | Check payment result after redirect |
| POST | /rest/checkout/confirm-payment/{orderId} | Customer | Verify payment with gateway (Stripe/Viva/PayPal) and confirm via PaymentConfirmationService |
Browse endpoints interactively in the API Reference.
PlaceOrderService handles the entire confirmation + order creation + payment init in one call. Key supporting services: OrderBasketBuilder (cart-to-basket row transformation with pricing/VAT snapshots), StockService (atomic stock reservation at placement for every payway via reduceStockForOrder(), and restoration on cancellation via restoreStockForOrder() — #282), OrderBasketWriteService (transactional basket row inserts), and CouponCodeWriteRepository (coupon usage tracking). Offline payment adapters (DeliveryAdapter, BankTransferAdapter, PaidAtStoreAdapter) now set order status to PENDING_ACCEPTED (not PENDING). For the full modern flow, see CF-06 Order Preview.
Legacy
| Trigger | Controller | Method |
|---|---|---|
POST /checkout with checkout=1 | Adv_order.php | checkout() (line 865) |
Code Flow
checkout() (line 865-933)
- Form validation +
recheckCart()— blocks if any item has insufficient stock - Gift packaging:
calculateGiftPackaging()— cost fromGIFT_PACKAGING.COSTregistry - Loyalty points:
deductLoyaltyPoints()— removes from customer BEFORE payment - VAT setup:
setVatForOrder()— configures by delivery country - Order creation:
create_order($orderData)→processOrder()→ insert to DB - Payment dispatch:
checkout->run($checkoutData)→ routes to gateway
parseCartForCheckout() (model line 636-725)
Final total: cart_total_vat + transport + delivery - points_reward + gift_packaging
- Transport cost: 0 for store pickup, calculated via
transfer_cost_admin()otherwise - Delivery cost: only for COD (
payway === 'delivery'), 0 otherwise - Rule 13 gifts: cheapest product's
paidQtyreduced but actualquantityunchanged
processOrder() (model line 801-843)
- Insert order to DB, generate serial via
createSerial() - Get gift product data, choose gift product codes
- Adjust cart for Rule 13 gifts
- Merge gift items into cart
- Insert all items via
order_basket_model->add_records_with_order()
Business Rules
| Rule | Description |
|---|---|
| Stock re-checked at submit | recheckCart() blocks if insufficient |
| Transport = 0 for pickup | useAddress == ORDER_ADDRESS_ESHOP |
| Delivery only for COD | payway === 'delivery' |
| Points deducted before payment | removePointsFromCustomer() called pre-order |
| Cart serialized for audit | Stored in shop_order.cart_contents |
| Order serial auto-generated | createSerial() with configurable prefix |
Client Extension Points
| Hook | Purpose |
|---|---|
checkoutViewExtra() (line 811) | Custom assets for confirmation page |
afterOrderSuccessHooks() | ERP sync after payment success. Note: This is an admin-side hook defined in Adv_orders_admin.php, not a front-checkout hook. |
afterOrderCancelHooks() | Cleanup after payment failure. Note: This is an admin-side hook defined in Adv_orders_admin.php, not a front-checkout hook. |
Data Model
For the full shop_order column schema (60+ columns), see AD-03 Order Management.
For the full shop_order_basket column schema, see AD-03 Order Management.
Other tables involved
| Table | Purpose |
|---|---|
shop_customer | Customer record (updated on checkout, is_guest=1 for guests) |
shop_order_klarna_payment | Klarna payment session data |
shop_order_dhl_voucher | DHL external rate selection |
shop_prices_view — The only database VIEW in the system
shop_prices_view is the only database view in the entire application. It pre-computes product pricing with VAT and discount logic, eliminating repetitive price calculations across multiple modules.
Price formulas:
sql
original_price = price + (VAT% / 100 * price)
final_price = CASE
WHEN special_discount IS active THEN discounted_price
WHEN regular_discount > 0 THEN discounted_price
ELSE original_price
ENDUsage across the platform:
- Product browsing (CF-01) — storefront price display and sorting
- Feeds (IN-01) — marketplace feed price output
- Admin sorting — admin product listing ordered by computed price
Legacy naming note: The discount percentage column is named
discount_persent(notdiscount_percent) — this is a legacy typo preserved for backward compatibility. All code referencing this column uses the misspelled name.
VAT fragility: The view uses
INNER JOIN shop_product_vats— any product whose VAT rate row is deleted silently disappears from all view-based queries. For the canonicalshop_product_vatsschema,INNER JOINfragility details, and VAT rate rules, see AD-50 VAT Management.
Modern REST Path — Order Event Bus
After an order is confirmed on the REST path, side-effects (email, ERP, analytics, loyalty) are delivered through OrderEventDispatcher — a synchronous fanout bus that isolates listener failures with a per-listener try/catch and logs a warning on failure. No single listener can block or abort the others.
Trigger points
Online payment (gateway redirect): PaymentConfirmationService::confirmPayment() (src/Domains/Checkout/PaymentConfirmationService.php:95-108) reloads the freshly-written order row and dispatches OrderPaid:
php
// PaymentConfirmationService.php:95-108
$fresh = $this->orderService->get($orderId);
if ($fresh !== null) {
$this->orderEventDispatcher->dispatchPaid(new OrderPaid(
orderId: (int) $fresh->id,
customerId: (int) (isset($fresh->customer_id) ? $fresh->customer_id : 0),
orderSerial: (string) (isset($fresh->order_serial) ? $fresh->order_serial : ''),
payway: (string) (isset($fresh->payway) ? $fresh->payway : ''),
total: (float) (isset($fresh->total_vat) ? $fresh->total_vat : 0.0),
currencyCode: (string) (isset($fresh->order_currency) ? $fresh->order_currency : ''),
isOffline: false,
transactionId: $options['transactionId'] ?? null,
));
}Offline payment (no redirect): PlaceOrderService::placeOrder() (src/Domains/Checkout/PlaceOrderService.php:315-326) dispatches OrderPaid synchronously when $paymentResult->status === 'PENDING_ACCEPTED' (offline adapters only), with isOffline: true. Stock is reserved unconditionally at :307 before this dispatch, regardless of payway (#282).
Cancellation: PaymentConfirmationService::cancelPayment() (src/Domains/Checkout/PaymentConfirmationService.php:162-168) dispatches OrderCanceled after writing status = CANCELED and restoring stock via StockService::restoreStockForOrder() (:156, #282).
Dispatcher
src/Domains/Order/Event/OrderEventDispatcher.php — synchronous fanout; each listener runs inside try { ... } catch (\Throwable $e). Failures are logged via $this->logger->warning(...) and do not propagate to callers or sibling listeners.
OrderPaid listeners (7, wired in declaration order)
Wiring: src/Domains/Order/container.php:125-132 via ->call('addPaidListener', ...).
| # | Listener | Deferred? | Role |
|---|---|---|---|
| 1 | SendOrderConfirmationEmailListener | No (inline) | Calls legacy Adv_mailer::order_complete() to send the customer confirmation email |
| 2 | FireErpWebhookListener | Yes — PRIORITY_CRITICAL | Fires InternalApiOrderForErpHook outbound HTTP POST to ERP (Singular/SoftOne). No-op when internalApi.apiOrderWebHooks.erpReady is not configured |
| 3 | CheckLowStockListener | No (inline) | Emails admin when any basket product hits or drops below the low-stock threshold. Gated by Registry EMAIL.NOTIFICATION_LOW_STOCK |
| 4 | TrackMatomoOrderListener | Yes — PRIORITY_LOW | Tracks ecommerce order in Matomo. Gated by Registry MATOMO.ENABLED + credentials; production-only (ENVIRONMENT=production) |
| 5 | DispatchMetaCapiPurchaseListener | Yes — PRIORITY_CRITICAL | Sends purchase event to Meta Conversions API. Gated by Registry FACEBOOK_CONVERSION.ENABLED + pixel credentials |
| 6 | DispatchManagoOrderListener | Yes — PRIORITY_CRITICAL | Posts PURCHASE contactExtEvent to Manago CRM. Gated by Registry MANAGO.ENABLE_API + MANAGO.ENABLE_PURCHASE_REPORT. Defaults forceOptOut: true on the REST path (no session consent state) |
| 7 | DispatchProjectAgoraOrderListener | Yes — PRIORITY_CRITICAL | Reports order to Project Agora ad-attribution service. Gated by Registry AGORA.IS_ENABLED. REST orders dispatch with $adds = [] (no session ad-impression map), so Agora silently produces a no-op payload |
OrderCanceled listeners (2, wired in declaration order)
Wiring: src/Domains/Order/container.php:133-134 via ->call('addCanceledListener', ...).
| # | Listener | Role |
|---|---|---|
| 1 | RestoreSpentPointsListener | Refunds points_spend to shop_customer.total_points when the canceled order had redeemed loyalty points |
| 2 | RestoreCouponUsageListener | Decrements coupons.is_used for the coupon applied to the canceled order, restoring one redemption slot |
MarketingConsentCaptured channel
PlaceOrderService::placeOrder() dispatches MarketingConsentCaptured (src/Domains/Checkout/PlaceOrderService.php:252-261) when PlaceOrderData::$marketingConsent is true and a non-empty customer email is present. Zero listeners are wired (src/Domains/Order/container.php registers no addMarketingConsentListener call). The channel is open for future CRM-sync subscribers; the absence of listeners is intentional for the initial shipping state. See Known Issues item 2 below.
DeferredTaskRunner integration
Slow side-effects (ERP, Matomo, Meta CAPI, Manago, Project Agora) wrap their work in DeferredTaskRunner::defer() before returning from the listener. The POST response is returned to the client before deferred tasks execute. See SY-27 Deferred Tasks for execution semantics and priority ordering.
Known Issues & Security Gaps
Modern vs legacy status divergence for offline payments. The modern REST
PlaceOrderServicesets offline payment orders toPENDING_ACCEPTED(via adapters), while the legacycheckout()sets them toPENDING. See CF-06 Known Issues for details.MarketingConsentCapturedhas zero listeners.PlaceOrderService.php:252-261dispatches the event when a customer opts in at checkout, butsrc/Domains/Order/container.phpwires noaddMarketingConsentListenercalls. Consent signals captured via REST are silently dropped — no CRM sync (ManagosaveContact, Moosend, Mailchimp) fires. Intended for a follow-up CRM-sync integration.Project Agora ad-attribution is always empty on the REST path.
DispatchProjectAgoraOrderListenerdispatches with$adds = []because the REST path has no session ad-impression map (src/Domains/Order/Event/Listeners/DispatchProjectAgoraOrderListener.php:78). Orders placed via the REST API never carry ad attribution to Agora. Tracked as a follow-up requiringadImpressionsonPlaceOrderData.Manago consent defaults to
forceOptOut: trueon the REST path.DispatchManagoOrderListenercannot readcustomer_in_managosession state (REST is stateless), so every REST-placed order is reported with opt-out posture (src/Domains/Order/Event/Listeners/DispatchManagoOrderListener.php:28-35). Customers who had opted in through the storefront lose their opt-in attribution for REST orders untilcustomerOptedInToManagois added toPlaceOrderData.
Related Flows
- CF-05 Cart Management — cart data parsed for final totals
- CF-06 Order Preview — full
PlaceOrderService13-step flow; whereOrderPaidoriginates for offline adapters - CF-08 Payment Processing — receives created order
- CF-09 Payment Webhooks — async payment callbacks;
PaymentConfirmationServicecalled from webhook handlers to dispatchOrderPaid - CF-13 Coupons — coupon discount in final totals;
RestoreCouponUsageListenerrestores usage on cancel - CF-14 Gift Rules — Rule 13 adjustments
- CF-32 Loyalty Points — points deduction at place-order;
RestoreSpentPointsListenerrefunds on cancel - SY-02 Order Status Emails — confirmation email after payment
- SY-27 Deferred Tasks — ERP, Matomo, Meta CAPI, Manago, Project Agora listeners defer slow work post-response
- AD-03 Order Management — admin order lifecycle
- AD-50 VAT Management — VAT rate catalog and
shop_prices_viewfragility rules
Wiki Guides: Payment adapter setup — see Stripe Guide. Deferred ERP sync after success — see Deferred Task Guide.
Shared Patterns
- SY-24 Email Dispatch — order confirmation emails dispatched after successful payment
- SY-27 Deferred Tasks — ERP sync and post-order hooks run as deferred tasks
- SY-26 Circuit Breaker — protects external payment gateway calls from cascading failures