Command Conditions
This document explains, step by step, how a command condition is added to a state transition. It specifically covers the RETAIL_STORE_PACKAGING_WITH_SHIPMENT and RETAIL_STORE_PACKAGING_WITHOUT_SHIPMENT conditions used in Click & Collect scenarios.
It also clarifies an important limitation that arises because the existing conditions are unaware of the store context (see Important limitation), and describes the new VIEWING_STORE_IS_DELIVERY_STORE condition, which overcomes this limitation using the store context (the X-Store header) (see VIEWING_STORE_IS_DELIVERY_STORE).
Command Condition Mechanism
A command condition is a prerequisite that a package must satisfy for a command—and therefore a state transition—to run. If the condition is not satisfied, the transition is blocked and an exception is raised.
Basic Structure
The system has a two-layer condition structure: a most-general base condition, and a package-based condition type built on top of it. Newly written package conditions are derived from this package-based type.
Every condition has the following basic properties:
Unique identifier (slug): The unique name that identifies the condition and is used in configuration.
Readable label: The name shown in the UI in the list of available conditions.
Parameter type: Some conditions take external parameters—for example, a channel, a carrier, or a list of stock locations. In this case, the condition specifies which parameter type it expects; conditions that take no parameters leave this empty.
Exclusion info: The list of other conditions that cannot be used together with this one on the same transition because they are logically contradictory.
The condition's actual logic is defined inside a check operation. The following rule applies:
If the condition is satisfied, the check passes silently.
If it is not satisfied, an error is produced and the transition is blocked.
For package conditions, this is a standard "condition not satisfied" error. Some special conditions may also produce their own more descriptive error.
Registration
A condition class is registered using the @register_command_condition decorator. The decorator adds the condition to the REGISTERED_COMMAND_CONDITIONS dictionary keyed by slug.
If a second registration is attempted with the same slug, an error is raised. Therefore, every condition's slug must be unique.
Attaching A Condition To A Transition
A condition is attached to a transition through database configuration, not through code:
StateTransitionConditionhas a one-to-one relationship with eachStateTransitionand carries arules_configurationJSON field.At runtime, each rule in this JSON is read. The corresponding condition class is located using the full import path in the rule's
klassfield, instantiated withparams, and the result is cached.
The rules_configuration field contains a rules list. Each item contains:
klass: The full import path of the condition class.params: Parameters passed to the condition.
For parameterless conditions, params is left empty. For parameterized conditions, the relevant parameters are placed here, such as a list of channels.
klass must be the full import path. This binding does not require a code deployment; it is done purely through database configuration.
Runtime Flow
When a command is executed, it goes through a pre-validation step before performing the actual operation. During this step, the system checks whether there is a condition definition attached to the transition being performed:
If there is no condition definition attached to the transition, no check is performed and the command runs freely.
If a condition definition exists, the rules in the configuration are evaluated in order, and the package is checked against each rule.
If multiple rules are defined, all of them must be satisfied using AND logic. The first rule that is not satisfied raises an error, the transition stops, and the command does not run.
This precondition check is applied on the package side to the key commands of the packaging and shipping flow, such as completing packaging, packaging without shipment, shipping the package, and transfer delivery steps.
API Listing Available Conditions
All registered conditions are listed via the available-command-conditions endpoint. The response includes each condition's:
sluglabelklass, the import path to use in configurationparameter_typeexclusive_withinformation
This allows you to build the correct rules_configuration when attaching a condition to a transition and to detect conflicting conditions through exclusive_with.
Steps For Attaching A Command Condition To A Transition
This section covers attaching, setting up, or configuring an existing condition to a state transition without adding new code to the system. Attaching does not require a code deployment; it is done through configuration.
The condition you want to use must already be defined in the system. If the condition you need does not exist yet, it must first be defined and released by a developer. This section assumes the condition already exists.
Identify the Condition To Use
View the conditions defined in the system from the list of available conditions. This list provides each condition's identifier (slug), readable label, the reference to use in configuration (klass), the parameter type it expects, if any, and the conditions it excludes.
Select the condition that fits your need, such as RETAIL_STORE_PACKAGING_WITHOUT_SHIPMENT.
Request:
Response (200 OK):
The klass field is the exact reference you will write into the configuration in the next step.
Attach the Condition To the Transition
Create a condition configuration for the relevant state transition. In the request body, provide the state_transition ID you are attaching the condition to, and provide the klass reference obtained in the previous step inside rules_configuration, along with params if the condition takes parameters.
Multiple conditions can be added. In that case, all of them must be satisfied for the transition to occur.
Request (parameterless condition):
Response (201 Created):
For a condition that takes parameters, fill in params, for example with a list of stock locations:
To update an existing attachment, use PATCH /state-transition-conditions/{id}/.
To remove one, use DELETE /state-transition-conditions/{id}/.
Avoid Conflicting Conditions
Some conditions are mutually exclusive by logic, such as "Click & Collect orders only" versus "non-Click & Collect orders."
This information is visible in the exclusion field of the available conditions list. Do not attach two conflicting conditions to the same transition together; otherwise, no package will be able to make that transition.
RETAIL_STORE_PACKAGING_WITH_SHIPMENT and WITHOUT_SHIPMENT
Both conditions compare the same two values:
Delivery store: The order's
retail_store_idfield; the store where the customer will pick up the item.Packaging location: The package's
stock_locationsource-location ID; the location where the package was actually packed.
RETAIL_STORE_PACKAGING_WITHOUT_SHIPMENT
Passes if delivery store == packaging location
The package is packed at the same store as the order's delivery store → no shipment is needed, for on-site pickup.
RETAIL_STORE_PACKAGING_WITH_SHIPMENT
Passes if delivery store != packaging location
The package is packed at a location different from the delivery store → a shipment or transfer to the delivery store is needed.
Since these two conditions are the opposite of each other, they should not be used together on the same transition in practice.
Typical usage is to distinguish between two different transition variants of the same command, such as completing packaging:
Packaging-without-shipment transition: Attach
RETAIL_STORE_PACKAGING_WITHOUT_SHIPMENT. Only packages packed at the delivery store can use this transition.Packaging-with-shipment transition: Attach
RETAIL_STORE_PACKAGING_WITH_SHIPMENT. Only packages packed at a different location, to be shipped to the delivery store, can use this transition.
Relationship With the Click & Collect Flow
The scenario in the koton-cnc.drawio diagram illustrates why two conditions are needed.
For example, an order with delivery store M1 selected is split into multiple packages based on stock distribution. A different flow is applied depending on where each package is packed.
P1 — Package created at the delivery point (M1) → WITHOUT_SHIPMENT
The packaging order is created at the delivery store itself, M1. Items are delivered directly to the customer and are not shipped.
Since the packaging location and the delivery store are the same for this package, it passes the RETAIL_STORE_PACKAGING_WITHOUT_SHIPMENT condition.
P2 — Package created at a different location (M2) → WITH_SHIPMENT
Packaging occurs at location M2. Items are shipped to M1, and delivery to the customer happens from M1.
READY_FOR_PICKUP
Once shipment-status polling tasks confirm the package has arrived at M1, it moves to the Ready For Customer Pickup state.
The newly added VIEWING_STORE_IS_DELIVERY_STORE condition can be attached via configuration to this Shipped → Ready For Customer Pickup transition. When attached, the command is hidden at the source store where packaging took place, M2, and remains visible in all store contexts other than M2, including the delivery store M1. See VIEWING_STORE_IS_DELIVERY_STORE.
Since the packaging location differs from the delivery store for this package, it passes the RETAIL_STORE_PACKAGING_WITH_SHIPMENT condition.
Even within the same order, a different packaging and shipping flow is required depending on where a package is packed. The two conditions guarantee that the correct command only runs for the correct package type: a package packed at the delivery store is routed to the no-shipment flow, and a package packed at a different location is routed to the with-shipment flow.
Important Limitation
When considering a requirement such as showing the "Ready for Customer Pickup" button only at the store where the item will be picked up, the following behavior becomes clear:
These two conditions alone cannot deliver the behavior of "show the button only at the pickup store."
The reasons are:
stock_locationremains fixed as the source store.The package's
stock_locationfield represents the source location where packaging took place throughout the package's lifecycle. It is not updated to the delivery store during the delivery stage.Therefore, these conditions do not express "which store the package is currently physically located at," but rather "whether packaging took place at the delivery store." For a package that has arrived at the delivery store via shipment, such as P2, the packaging location and the delivery store still appear different even at the delivery store.
The list of available commands is unaware of the store context.
The
available-commandsendpoint statically returns all registered commands. It does not filter based on the requesting store or user context.The package filter uses OR logic and is unaware of the store context.
The relevant package filter matches a package against both the source location and the delivery store using
ORlogic.In other words, a store can see both the packages it packed itself and the packages that will be delivered to it. The filter alone does not make the "delivery store only" distinction.
Conclusion & Guidance
The
RETAIL_STORE_PACKAGING_WITH_SHIPMENTandRETAIL_STORE_PACKAGING_WITHOUT_SHIPMENTconditions are the right tool for separating the packaging flow, with shipment or without shipment. They determine whether a transition should run based on the package type.However, a requirement such as "show the button only in the context of the requesting store" requires store context. Even though the identity of the requesting store,
X-Store-Remote-Id, is available, as long as this context is not explicitly incorporated into command-visibility calculation and/or package filtering, the existing conditions cannot make the "is the viewing store the delivery store?" distinction on their own at the UI level.
VIEWING_STORE_IS_DELIVERY_STORE
The limitation described in Important limitation—that command conditions are unaware of the requesting store—is addressed by this condition.
Unlike the other conditions, VIEWING_STORE_IS_DELIVERY_STORE looks not only at the package's data but also at the context of the requesting store.
Source of the Store Context
The identity of the requesting store is carried via the X-Store-Remote-Id header. This store context is supplied to the condition both during command-visibility calculation and during command execution.
If the header is absent, the store context is empty. The condition is skipped without performing a comparison, and the command is not blocked.
Comparison & Rule
The condition compares the X-Store-Remote-Id value from the header with the ERP code of the package's packaging location, stock_location.
No context, no X-Store-Remote-Id in the header
No comparison is performed
The condition is skipped and the command is not blocked. Back-office, third-party, and calls without a store context are not blocked.
Viewing store == packaging location
If equal, fails
The command is hidden/blocked and is not shown at the source store where packaging took place.
Viewing store != packaging location
If different, passes
The command is visible/runnable at the delivery store and other stores.
The comparison is made against the package's stock_location, the packaging-location ERP code, not against the order's retail_store, the delivery-store value. This way, the command is hidden at the source store where packaging took place and remains visible at the delivery store.
Typical Usage
This condition is attached to a command that is only meaningful in the context of the delivery side, such as READY_FOR_PICKUP ("Ready for Customer Pickup").
This way, the button is not shown at the source store that performed the packaging, but the command remains usable in the context of the delivery store.
Attachment is done through rules_configuration, the same way as other conditions. It takes no parameters:
Its entry in the available conditions list, available-command-conditions, is:
Quick rule: RETAIL_STORE_PACKAGING_WITH_SHIPMENT and RETAIL_STORE_PACKAGING_WITHOUT_SHIPMENT distinguish "is this package being packed with shipment or without shipment?" VIEWING_STORE_IS_DELIVERY_STORE answers the question "should this command be shown based on the requesting store, the X-Store context?" In other words, it incorporates the store context into the command condition.
Last updated
Was this helpful?

