Order Life Cycle
Order models represent order transactions throughout their entire life cycles, thereby encapsulating the function of several traditional business documents (for example: purchase order, invoice, and receipt) into a single document.
Orders begin their lives driven largely by consumers. Consumers create carts, which ideally progress through checkout, but may become abandoned along the way. If not placed, orders eventually expire and are cleaned. In an effort to prevent this, abandoned orders trigger reminders to consumers, which may result in resumed carts and checkouts.
Completed checkouts result in placed orders, which unlike carts, are handled primarily by the retailer. Placed orders are indexed into search, through which admins access and manage the orders. Placed orders are permanent records that never become abandoned or expire, even when they are canceled.
Since Workarea 3.5, orders can also be suspected of fraud, and such orders are additionally indexed into Admin search.
This guide describes the preceding domain concepts in greater detail, providing as examples all the configurable values and aspects of the
Order interface that effectively define this domain logic and affect the progression of an order through it’s various statuses and states.
Copious examples use the Ruby interface provided by Workarea Core. Each example builds on those before it, as if run sequentially within the same Ruby process.
Orders begin as carts, typically created by consumers in the Storefront. The cart status is the default state of an order, generally defined as “not in any other status”.
order = Workarea::Order.create order.status # => :cart
Carts, like other application documents, have timestamps which encode the date and time at which they were created and last updated. Later examples show uses of these timestamps.
order.created_at.present? # => true order.updated_at.present? # => true
In a broader sense, carts include all orders that aren’t placed, as demonstrated in the following examples:
Workarea::Order.carts.include?(order) # => true order.placed_at.present? # => false order.placed? # => false Workarea::Order.not_placed.include?(order) # => true
Orders not placed within the configurable
order_active_period are considered abandoned. The order created above is not abandoned because its
created_at is still within the order active period.
Workarea.config.order_active_period # => 2 hours order.abandoned? # => false order.created_at > Workarea.config.order_active_period.ago # => true
The following example uses Rails’ testing time helpers to simulate the passage of time, demonstrating the effect on the order’s status. After the active period has passed, the order identifies as abandoned.
include ActiveSupport::Testing::TimeHelpers travel Workarea.config.order_active_period order.created_at > Workarea.config.order_active_period.ago # => false order.abandoned? # => true order.status # => :abandoned
Furthermore, orders that go unmodified within the configurable
order_expiration_period are considered expired. The following example queries for expired orders before and after the expiration period has elapsed.
Workarea.config.order_expiration_period # => 6 months Workarea::Order.expired.include?(order) # => false order.updated_at > Workarea.config.order_expiration_period.ago # => true travel Workarea.config.order_expiration_period order.updated_at > Workarea.config.order_expiration_period.ago # => false Workarea::Order.expired.include?(order) # => true
Workarea applications run a scheduled worker to periodically clean up (that is, destroy) expired orders. Manually running this worker removes the order from the database.
Workarea::CleanOrders.new.perform Workarea::Order.expired.include?(order) # => false Workarea::Order.all.include?(order) # => false
Checkouts are carts in the process of checking out. Checkout is the process used to transition an order from a cart to a placed order. The following examples require a new order
order = Workarea::Order.create
which embeds an item. This example assumes catalog data is already seeded.
( The following procedure for adding an item is suitable for demonstration but not ideal for production use. For the latter, review the implementation of
Storefront::CartItemsController#create (source, v3.4.17. )
product = Workarea::Catalog::Product.sample order.add_item( product_id: product.id, sku: product.variants.first.sku, quantity: 1 )
Creating and starting a checkout writes a new timestamp to the order, recording the time checkout was started. The presence of this timestamp causes the order to identify as a checkout.
checkout = Workarea::Checkout.new(order) checkout.start_as(:guest) order.checkout_started_at.present? # => true order.started_checkout? # => true order.checking_out? # => true order.status # => :checkout
However, because the order is not yet placed, it also identifies as a cart.
Workarea::Order.carts.include?(order) # => true Workarea::Order.not_placed.include?(order) # => true
If a checkout idles beyond the configurable duration,
checkout_expiration, the checkout expires. In this case, the order is returned to the cart status, and the consumer must restart checkout.
Workarea.config.checkout_expiration # => 15 minutes order.checkout_started_at > Workarea.config.checkout_expiration.ago # => true order.status # => :checkout travel Workarea.config.checkout_expiration order.checkout_started_at > Workarea.config.checkout_expiration.ago # => false order.checking_out? # => false order.status # => :cart
This does not mean the consumer must complete the entire checkout within this period, since each Storefront checkout request touches the checkout, updating the checkout start time (along with some other details). This prevents the checkout from expiring and will also “revive” an expired checkout, returning it to active checkout status.
order.touch_checkout! order.checkout_started_at > Workarea.config.checkout_expiration.ago # => true order.checking_out? # => true order.status # => :checkout
As with all carts, a checkout that is not placed within the order active period is considered abandoned. Traveling forward this duration expires the checkout and causes the order to become abandoned.
travel Workarea.config.order_active_period order.checkout? # => false order.abandoned? # => true order.status # => :abandoned
However, while the order is actively checking out, it will not identify as abandoned, despite it existing longer than the order active period.
order.touch_checkout! order.checking_out? # => true order.abandoned? # => false
After the checkout expires, the order returns to abandoned status.
travel Workarea.config.checkout_expiration order.checking_out? # => false order.abandoned? # => true order.status # => :abandoned
To help recover the potentially lost revenue of abandoned orders, Workarea applications send “reminder” emails when possible. Each email contains a token allowing the consumer to resume the cart.
For an order to be considered needs reminding, it must have started checkout, become abandoned, and have an email. Since Workarea 3.5, it must also not be suspected of fraud (see section Suspected Fraud below).
The example order does not qualify because it is an active checkout and does not have an email.
order.checking_out? # => true order.email.present? # => false Workarea::Order.need_reminding.include?(order) # => false
Expiring the checkout (by simulating the passage of time) and adding an email causes the order to match.
travel Workarea.config.checkout_expiration order.update_attributes(email: 'firstname.lastname@example.org') Workarea::Order.need_reminding.include?(order) # => true
A scheduled worker runs this query periodically and sends a reminder email for each matching order. Each order is “marked as reminded” with a
reminded_at timestamp, which prevents the order from matching the query again.
Workarea::OrderReminder.new.perform order.mark_as_reminded! order.reminded_at.present? # => true Workarea::Order.need_reminding.include?(order) # => false
Order Expiration After Starting Checkout
If the reminder email does not entice the consumer to resume the cart, the order will likely expire and be cleaned. Notice in the following example the order is returned only by
Order.expired_in_checkout, which was added in Workarea 3.3 to address the issue of orders never expiring after starting checkout.
( In Workarea versions prior to 3.3, this order will never expire unless checkout is explicitly reset, and the order is therefore never cleaned. )
travel Workarea.config.order_expiration_period order.updated_at <= Workarea.config.order_expiration_period.ago # => true order.started_checkout? # => true Workarea::Order.expired.include?(order) # => false Workarea::Order.expired_in_checkout.include?(order) # => true
One final note regarding checkout: it is possible to explicitly reset a checkout, which returns the order to the cart status and removes any record of checkout having started.
order.reset_checkout! order.checkout_started_at.present? # => false order.started_checkout? # => false
Now the order will not be reminded since it has no record of starting checkout.
Workarea::Order.need_reminding.include?(order) # => false
Resetting checkout also removes the
reminded_at timestamp, so the order will qualify as needs reminding if checkout if resumed.
order.reminded_at.present? # => false order.touch_checkout! travel Workarea.config.order_active_period Workarea::Order.need_reminding.include?(order) # => true
Checkout concludes by placing the order. Always place an order through checkout, not directly from the order. This ensures the completion of each checkout step, manages inventory, creates the corresponding
Fulfillment, and saves order analytics.
However, placing the example order through checkout fails because the checkout steps are incomplete. To continue demonstrating the placed order status without distraction, the following example places the order directly from the order model.
checkout.place_order # => false order.place # => true
place method does little more than set the
placed_at timestamp and save the order. However, it saves the order conservatively, waiting for the save to write to disk and (in hosted environments) replicate to other nodes before reporting success.
Order#place method also runs the custom
:place callback (see Callbacks Worker, which enqueues additional work to run in the background. However, those jobs are outside the scope of the
Order module and are not covered here. )
order.placed_at.present? # => true
This addition causes the order to identify as placed.
order.placed? # => true order.status # => :placed Workarea::Order.placed.include?(order) # => true Workarea::Order.recent_placed.include?(order) # => true
Moreover, the order no longer identifies as a cart, since the cart and placed statuses are exclusive.
Workarea::Order.carts.include?(order) # => false Workarea::Order.not_placed.include?(order) # => false
As a placed order, the order can no longer be abandoned or expire.
travel Workarea.config.order_expiration_period order.status # => :placed order.abandoned? # => false Workarea::Order.expired.include?(order) # => false
Creating Placed Orders with Factories
Setting up the necessary data to complete an order properly through checkout is cumbersome (hence, the process was skipped above). However, in a testing context, factories make it easier to create orders, including placed orders.
Requiring the application’s test helper and including the factories module enables factories in the current Ruby process (this isn’t necessary when writing code within a test case).
# WARNING: drops the database for the current Rails environment! require_relative 'test/test_helper' include Workarea::Factories cart = create_order placed_order = create_placed_order cart.status # => :cart placed_order.status # => :placed
You can also use
complete_checkout to place an order you've already created. The existing order must have an email and at least one item. The factory creates the shipping and payment data needed to complete checkout, and places the order.
order = create_order order.status # => :cart complete_checkout(order) # => true order.status # => :placed
Searching Placed Orders
Notably, search has been absent from this discussion so far. This is due to the fact that only placed orders are indexed into Elasticsearch, in order for administrators to manage the placed orders through the Admin interface. (This changes in Workarea 3.5, which introduces fraud analysis. Orders suspected of fraud are also indexed into Admin search. See section Suspected Fraud below.)
The next example first resets the Admin search indexes and then manually indexes the cart and placed order which were created above. Only the placed order is returned in search results.
cart = create_order placed_order = create_placed_order Workarea::Search::Admin.reset_indexes! Workarea::Search::AdminOrders.new.results.count # => 0 Workarea::IndexAdminSearch.perform(cart) Workarea::IndexAdminSearch.perform(placed_order) Workarea::Search::AdminOrders.new.results.count # => 1 Workarea::Search::AdminOrders.new.results.first.id == placed_order.id # => true
Workarea Core also allows canceling orders, however, this does not take into consideration the restocking of inventory, refunding of payment, and updating of fulfillment that may accompany such a change. Because of these additional concerns, this functionality is not exposed as a web interface in the base platform, but is available through the Workarea OMS plugin.
Order document’s perspective, canceling an order (achieved through
Order#cancel) is simply the process of recording the date and time at which the order was canceled. The presence of this additional timestamp causes the order to identify as canceled.
placed_order.cancel # => true placed_order.canceled_at.present? # => true placed_order.canceled? # => true placed_order.status # => :canceled
A canceled order also continues to identify as a placed order and continues to be accessible through search.
placed_order.placed_at.present? # => true placed_order.placed? # => true Workarea::Order.placed.include?(placed_order) # => true Workarea::Order.recent_placed.include?(placed_order) # => true Workarea::Search::AdminOrders.new.results.first.id == placed_order.id # => true
Workarea 3.5 adds fraud analysis, and therefore introduces an additional order status: suspected fraud.
The API call
Order#set_fraud_decision! embeds the given
Order::FraudDecision within the order and sets one or both of the timestamps
fraud_suspected_at. When the fraud decision is declined, the query
true, and the order's status is
order.set_fraud_decision!(declined_decision) order.fraud_decision.class # => Workarea::Order::FraudDecision order.fraud_decided_at.present? # => true order.fraud_suspected_at.present? # => true order.fraud_suspected? # => true order.status # => :suspected_fraud
Additionally, orders suspected of fraud are indexed into Admin Elasticsearch indexes.
See Add a Fraud Analyzer for more coverage of fraud analysis.
Consumers create carts in the Storefront, which may become abandoned and expire, and are eventually cleaned. Carts progress through checkouts, which also expire and may become abandoned. Abandoned orders are reported and (when possible) reminded, which may result in resumed carts and checkouts. Completed checkouts produce placed orders, which are indexed into search for management by admins, and may be canceled. Since Workarea 3.5, orders may also be suspected of fraud, and such orders are additionally indexed into Admin search.
These order statuses and states are defined largely by aspects of the
Order interface, namely:
- The destructive instance methods
- The timestamp fields
- The predicate methods
- The criteria class methods
as well as the configurable durations
Help Us Improve this Doc
Was this helpful? Open a GitHub issue to report a problem with this doc, suggest an improvement, or otherwise provide feedback. Thanks!