Skip to content

Proposed Technical Specification

Design Status

This is the proposed technical specification for the new checkout flow. See Overview for the business rationale and Proposed Checkout Flows for detailed sequence diagrams.

Key Flow Changes:

  • Order Creation: Moved to step 2 (/checkout/payment-selection) with status=created
  • Order Locking: Happens at step 3 (/checkout/payment) when status changes to pending
  • Cart Modifications: Allowed while order is in created state, blocked after pending
  • Card Methods: Require additional step (2a or 2b) before payment execution

Checkout Steps & Controllers

Step URL Controller Purpose
0 /cart CartController View and modify cart
1 /checkout/auth CheckoutAuthController Login/Register/Guest
2 /checkout/payment-selection CheckoutPaymentController Select payment method, create/update order (status='created')
2a /checkout/creditcard-selection CreditcardController Card selection/entry (conditional - creditcard only)
2b /checkout/bancontact-selection BancontactController QR/App/Card selection (conditional - bancontact only)
3 /checkout/payment PaymentProcessController Execute payment (order → 'pending')
4 /checkout/validate PaymentValidationController Validate payment result
5 /checkout/confirmation OrderController Order confirmation

PrestaShop Back Office customization may be required

PrestaShop 1.6 does not natively support displaying multiple payment attempts per order. Custom modules or back office sections may be required to view payment attempt history and manage orders with multiple payment attempts. Further research is needed to determine the exact implementation requirements.

Database Schema

Modified: Orders Table

Add status field to track order lifecycle:

  • Status values: created, pending, confirmed, shipped, delivered, cancelled, failed, abandoned, refunded
  • Default: created
  • Purpose: Track where an order is in its journey from creation to completion

Additional fields needed:

  • recovered_from_order_id - Foreign key linking to previous abandoned order (nullable)
  • updated_at - Last modification timestamp (required for abandonment detection)

What each status means:

Status Description
created Customer selected payment method but hasn't paid yet. They can still change their cart.
pending Customer is paying right now. Cart and prices are locked. Has a timeout of 30 min.
confirmed Payment successful. Order is ready to be picked and than shipped.
shipped Order has been sent to the customer.
delivered Customer received their order.
cancelled Customer clicked cancel during payment.
failed Payment was declined too many times.
abandoned Customer left during payment and didn't return within the timeout window (30 minutes).
refunded Customer returned the order and got their money back.

What happens when customers abandon checkout?

  • For created orders:
    • Orders stay in created state indefinitely
    • No prices are locked
    • Customers can return anytime and continue checkout
    • Cart gets re-validated with current prices/stock when they return
  • For pending orders (payment in progress):
    • Prices are locked
    • After 30 minutes of inactivity, order becomes abandoned
    • If customer returns after abandonment: new order is created with current prices/stock
    • Old abandoned order stays in system for audit/reporting purposes

New: Payment Attempts Table

Track all payment attempts for each order:

Field Name Field Type Field Description
order_id Core Foreign key to orders table (one order, many attempts)
payment_method Core Which method was used (ideal, paypal, creditcard, etc.)
status Core Payment attempt state: initiated, processing, success, failed, cancelled
amount Core Payment amount
cm_payment_id CM CM.com payment identifier
cm_transaction_id CM CM.com transaction identifier
error_code Error Standardized error code from CM.com or internal
error_message Error Human-readable error description
retry_count Error Number of times this order has been retried
initiated_at Time When payment attempt started
completed_at Time When payment finished (success/failed/canceled)
created_at Time Record creation timestamp

State Machines

Order State Machine

stateDiagram-v2
    [*] --> Created: Order Created (at payment-selection)

    note right of Created
        Customer can still modify cart/address
        Order gets updated when they return to payment-selection page
        ---
        Prices re-validated on return
    end note

    Created --> Pending: First Payment Attempt (at /checkout/payment)

    note right of Pending
        Order is LOCKED - no changes allowed
        Prices locked
        Can have multiple payment attempts
        ---
        30-minute timeout → Abandoned
    end note

    Pending --> Confirmed: Payment Success
    Pending --> Failed: Payment Failed (max retries exceeded)
    Pending --> Cancelled: User Cancels
    Pending --> Abandoned: Timeout (30 min, lazy evaluation)

    note left of Abandoned
        Customer left during payment
        ---
        If customer returns: create new order with current prices/stock
    end note

    note left of Cancelled
        Customer actively clicked cancel during the payment process
    end note

    Confirmed --> Shipped: Order Shipped
    Confirmed --> Refunded: Refund Issued

    Shipped --> Delivered: Delivery Confirmed

    Abandoned --> [*]
    Cancelled --> [*]
    Failed --> [*]
    Refunded --> [*]
    Delivered --> [*]

Payment Attempt State Machine

stateDiagram-v2
    [*] --> Initiated: Create Payment Attempt

    Initiated --> Cancelled: User Cancels (before processing)
    Initiated --> Processing: Sent to CM.com

    Processing --> Cancelled: Timeout/User Cancel
    Processing --> Failed: Payment Rejected
    Processing --> Success: Payment Approved

    Cancelled --> [*]: Allow Retry
    Failed --> [*]: Allow Retry
    Success --> [*]: Update Order

    note left of Cancelled
        ***Failed and Cancelled***
        Both states allow creating
        a new payment attempt
    end note

Abandoned Order Detection (Lazy Evaluation)

Determining when an order is abandoned:

An order is considered abandoned when:

order.status = 'pending' AND order.updated_at < (NOW() - INTERVAL 30 MINUTE)

Implementation approach - Model Layer (Recommended):

The abandonment check happens automatically whenever an order is loaded from the database. This is implemented in the Order model class:

<?php
class Order extends ObjectModelCore {

    /**
     * Override hydrate to check for abandonment after loading from DB
     * This ensures every order load automatically evaluates and updates abandoned status
     */
    public function hydrate(array $data) {
        parent::hydrate($data);
        $this->checkAndUpdateAbandoned();
        return $this;
    }

    /**
     * Check if pending order has exceeded timeout and mark as abandoned
     * Only checks orders in 'pending' status for performance
     */
    private function checkAndUpdateAbandoned() {
        if ($this->status === 'pending') {
            $timeoutThreshold = date('Y-m-d H:i:s', strtotime('-30 minutes'));

            if ($this->updated_at < $timeoutThreshold) {
                $this->status = 'abandoned';
                $this->update(); // Persist to database immediately
            }
        }
    }
}

Why this approach:

  • Stateless: No background cron jobs needed
  • Automatic: Works for any code that loads orders (controllers, APIs, admin panel)
  • Efficient: Only evaluates when order is actually accessed
  • Low overhead: Check only runs for pending orders
  • Immediate persistence: State updated in database as soon as detected

Trade-off:

  • Orders sitting in database won't become "abandoned" until they're read
    • Fix: Add a job that runs less frequent to loop over all orders to fix the abandoned status. (Less frequent: Daily or even Weekly)
  • For reports: either wait for lazy evaluation or run manual query using the abandonment condition above

Resuming Incomplete Orders

How customers can continue unfinished orders:

When a customer has incomplete orders (status created or pending), they can resume in several ways:

  1. Via Account Dashboard

    • Customer logs into their account
    • Sees list of incomplete orders with "Continue Checkout" button
    • Clicking the button:
      • For created orders → redirects to /checkout/payment-selection?order_id=12345
      • For pending orders → redirects to /checkout/payment-selection?order_id=12345 (shows retry/change method options)
  2. Via Email Recovery Link

    • System can send reminder emails with direct link
    • Link format: /checkout/resume?token=abc123 (includes order reference)
    • Token validates and loads the specific order
  3. Via Session Recovery

    • Customer returns to the site (same browser/session)
    • System detects incomplete order in session
    • Shows notification banner: "You have an incomplete order. Continue checkout?"
    • Clicking continues to appropriate step

What happens when resuming:

  • Created orders (created status):

    • Load order into checkout flow
    • Re-validate cart (stock availability, prices) - see Order Re-evaluation Flow
    • If validation passes: show payment method selection
    • If validation fails: redirect to cart with error message
  • Pending orders (pending status):

    • Check if order has exceeded 30-minute timeout
    • If NOT timed out:
      • Load order (locked, cannot modify)
      • Check latest payment attempt status
      • Show options: "Try Again" (same method) or "Choose Different Method"
      • If new method selected: create new payment attempt
      • If retry selected: create new payment attempt with same method
    • If timed out (becomes abandoned):
      • See abandoned order handling below
  • Abandoned orders (abandoned status):

    • Order is locked and cannot be resumed directly
    • System creates a new order with created status
    • Link new order to old: new_order.recovered_from_order_id = abandoned_order.id
    • Re-validate cart with current prices/stock (see Order Re-evaluation Flow)
    • Show notification: "We've updated your order with current prices. New total: €55 (was €50)"
    • Continue to payment selection with new order