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) withstatus=created - Order Locking: Happens at step 3 (
/checkout/payment) when status changes topending - Cart Modifications: Allowed while order is in
createdstate, blocked afterpending - 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
createdorders:- Orders stay in
createdstate indefinitely - No prices are locked
- Customers can return anytime and continue checkout
- Cart gets re-validated with current prices/stock when they return
- Orders stay in
- For
pendingorders (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
pendingorders - ✅ 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:
-
Via Account Dashboard
- Customer logs into their account
- Sees list of incomplete orders with "Continue Checkout" button
- Clicking the button:
- For
createdorders → redirects to/checkout/payment-selection?order_id=12345 - For
pendingorders → redirects to/checkout/payment-selection?order_id=12345(shows retry/change method options)
- For
-
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
-
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 (
createdstatus):- 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 (
pendingstatus):- 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 (
abandonedstatus):- Order is locked and cannot be resumed directly
- System creates a new order with
createdstatus - 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