Creating an Order Management System - Spryker Commerce OS
Edit on GitHubThis tutorial is also available on the Spryker Training website. For more information and hands-on exercises, visit the Spryker Training website.
Challenge description
In this task, you will create a full order management process (OMS) using the Spryker state machine and then use it in your shop.
1. Create the state machine skeleton
In this order process, you will use the following states: new, paid, shipped, returned, refunded, unauthorized, and closed.
You will build all the transitions and events between these states as well. The skeleton of Spryker state machines is simply an XML file.
- Create a new XML file in
config/Zed/oms
and call it Demo01.xml. - Add the Demo01 state machine process schema as follows:
<?xml version="1.0"?>
<statemachine
xmlns="spryker:oms-01"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="spryker:oms-01 http://static.spryker.com/oms-01.xsd">
<!-- Used as example XML for OMS implementation -->
<process name="Demo01" main="true">
<states>
</states>
<transitions>
</transitions>
<events>
</events>
</process>
</statemachine>
- Activate the OMS process in
config_default.php
inconfig/shared
by adding a name of the process Demo01 to the key[OmsConstants::ACTIVE_PROCESSES]
.
$config[OmsConstants::ACTIVE_PROCESSES] = [
'Demo01'
];
- Go back to the skeleton XML and add the first state. Simply, add a state element with the name.
<states>
<state name="new" />
</states>
- Check the state machine graph while building it.
- Go to the Administration → OMS page in the Backend Office, you will see your state machine Demo01.
- Click on it and you will see the graph that represents your XML file.
- Add the rest of the states to the state machine. Refresh the state machine graph after adding them.
<states>
<state name="new" />
<state name="paid" />
<state name="unauthorized" />
<state name="shipped" />
<state name="returned" />
<state name="refunded" />
<state name="closed" />
</states>
- Add the transitions with the statuses’ events.
Every transition has a source, a target, and an optional event. The source and target are simply state names, and the event is the name of the event defined in the events section.
Let’s start with the first transition. Refresh after adding the transition and check the updated state machine.
<transitions>
<transition happy="true" condition="Demo/IsAuthorized">
<source>new</source>
<target>paid</target>
</transition>
</transitions>
- Add the event to the events section and in the transition you already have. Refresh the graph afterwards.
<transitions>
<transition happy="true" condition="Demo/IsAuthorized">
<source>new</source>
<target>paid</target>
<event>pay</event>
</transition>
</transitions>
<events>
<event name="pay" onEnter="true" />
</events>
- Add rest of the transitions and events:
<transitions>
<transition>
<source>new</source>
<target>paid</target>
<event>pay</event>
</transition>
<transition>
<source>new</source>
<target>unauthorized</target>
<event>pay</event>
</transition>
<transition>
<source>paid</source>
<target>shipped</target>
<event>ship</event>
</transition>
<transition>
<source>shipped</source>
<target>returned</target>
<event>return</event>
</transition>
<transition>
<source>returned</source>
<target>refunded</target>
<event>refund</event>
</transition>
<transition>
<source>shipped</source>
<target>closed</target>
<event>close</event>
</transition>
<transition>
<source>refunded</source>
<target>closed</target>
<event>close after refund</event>
</transition>
</transitions>
<events>
<event name="pay" onEnter="true" />
<event name="ship" manual="true" />
<event name="return" manual="true" />
<event name="refund" onEnter="true" />
<event name="close" timeout="14 days" />
<event name="close after refund" onEnter="true" />
</events>
2. Add a command and condition to the state machine
The order process usually needs PHP implementations for certain functionalities like calling a payment provider or checking if payment is authorized or not. To do so, Spryker introduces Commands and Conditions:
- Commands are used for any implementation of any functionality used in the process.
- Conditions are used to replace an if-then statement in your process.
They are both implemented in PHP and injected in the state machine skeleton.
- Add a dummy command to perform the payment.
A command in the Spryker state machine is added to an event. So add the command Pay to the pay event.
<event name="pay" onEnter="true" command="Demo/Pay" />
- Then, add the command and hook it into the skeleton. The command is simply a Spryker plugin connected to the OMS module.
Add the command plugin to src/Pyz/Zed/Oms/Communication/Plugin/Command/Demo
and call it PayCommand
.
As the command is a plugin, it should implement some interface. The interface for the command here is CommandByOrderInterface
which has the method run();
namespace Pyz\Zed\Oms\Communication\Plugin\Command\Demo;
use Orm\Zed\Sales\Persistence\SpySalesOrder;
use Spryker\Zed\Oms\Business\Util\ReadOnlyArrayObject;
use Spryker\Zed\Oms\Communication\Plugin\Oms\Command\AbstractCommand;
use Spryker\Zed\Oms\Dependency\Plugin\Command\CommandByOrderInterface;
class PayCommand extends AbstractCommand implements CommandByOrderInterface
{
/**
* {@inheritDoc}
*
* @api
*
* @param \Orm\Zed\Sales\Persistence\SpySalesOrderItem[] $orderItems
* @param \Orm\Zed\Sales\Persistence\SpySalesOrder $orderEntity
* @param \Spryker\Zed\Oms\Business\Util\ReadOnlyArrayObject $data
*
* @return array
*/
public function run(array $orderItems, SpySalesOrder $orderEntity, ReadOnlyArrayObject $data): array
{
return [];
}
}
- Next, hook the command to your state machine using the
OmsDependencyProvider
.
In OmsDependencyProvider
, there is a method called extendCommandPlugins()
which is then called from the provideBusinessLayerDependencies()
method.
Add your new command to the command collection inside the container and use the same command name you have used in the XML skeleton like this:
/**
* @param \Spryker\Zed\Kernel\Container $container
*
* @return \Spryker\Zed\Kernel\Container
*/
protected function extendCommandPlugins(Container $container): Container
{
$container->extend(static::COMMAND_PLUGINS, function (CommandCollectionInterface $commandCollection): CommandCollectionInterface {
...
$commandCollection->add(new PayCommand(), 'Demo/Pay');
return $commandCollection;
});
return $container;
}
- Add the condition in the same way but using the
ConditionInterface
interface for the plugin instead of the command one. The state machine engine recognizes where to move next using the event name. In this case, the transitionspaid->shipped
andpaid->unauthorized
should use the same event name with a condition on one of the transitions.
The skeleton will look like this:
<transition condition="Demo/IsAuthorized">
<source>new</source>
<target>paid</target>
<event>pay</event>
</transition>
<transition>
<source>new</source>
<target>unauthorized</target>
<event>pay</event>
</transition>
The condition plugin:
namespace Pyz\Zed\Oms\Communication\Plugin\Condition\Demo;
use Orm\Zed\Sales\Persistence\SpySalesOrderItem;
use Spryker\Zed\Oms\Communication\Plugin\Oms\Condition\AbstractCondition;
use Spryker\Zed\Oms\Dependency\Plugin\Condition\ConditionInterface;
class IsAuthorizedCondition extends AbstractCondition implements ConditionInterface
{
/**
* @param \Orm\Zed\Sales\Persistence\SpySalesOrderItem $orderItem
*
* @return bool
*/
public function check(SpySalesOrderItem $orderItem): bool
{
return true;
}
}
And the OmsDependencyProvider
:
/**
* @param \Spryker\Zed\Kernel\Container $container
*
* @return \Spryker\Zed\Kernel\Container
*/
protected function extendConditionPlugins(Container $container): Container
{
$container->extend(static::CONDITION_PLUGINS, function (ConditionCollectionInterface $conditionCollection): ConditionCollectionInterface {
...
$conditionCollection->add(new IsAuthorizedCondition(), 'Demo/IsAuthorized');
return $conditionCollection;
});
return $container;
}
The order process for your shop is done. Refresh the graph and check it out.
3. Use the state machine for your orders
To use the state machine, hook it into the checkout. To do this, open the configuration file config/Shared/config_default.php
and make the invoice payment method use the Demo01 process.
$config[SalesConstants::PAYMENT_METHOD_STATEMACHINE_MAPPING] = [
DummyPaymentConfig::PAYMENT_METHOD_INVOICE => 'Demo01',
];
Your process should be working now.
4. Test the state machine
You have just built a new order process. To test it, do the following:
- Go to the shop, chose a product, add it to cart, and checkout using the Invoice payment method.
- Open the orders page in Zed UI and then open your order.
- There is the ship button to trigger the manual event ship.
- Click on the last state name under the state column to see the current state for a specific item.
- Click on ship to move the item into the next state.
- Click again on the last state name and check the current state.
Nice addition
Along with the nice representation of the state machine as a graph, Spryker provides the happy flag. It adds green arrows on the transitions to define the happy path of an order item.
To add this flag, just write happy = "true"
on the transitions that are a part of your process happy path and refresh the graph.
Thank you!
For submitting the form