Create an Order Management System - Spryker Commerce OS

Edit on GitHub

This tutorial is also available on the Spryker Training website. For more information and hands-on exercises, visit the Spryker Training website.

Challenge description

This task-based document shows how to 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.

  1. Create a new XML file in config/Zed/oms and call it Demo01.xml.
  2. 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>
  1. Activate the OMS process in config_default.php inconfig/shared by adding the name of the process Demo01 to the key [OmsConstants::ACTIVE_PROCESSES]:
$config[OmsConstants::ACTIVE_PROCESSES] = [
	'Demo01'
];
  1. Go back to the skeleton XML and add the first state by adding a state element with the name:
<states>
	<state name="new" />
</states>
  1. Check the state machine graph while building it.

    1. In the Backend Office, go to the Administration > OMS.
    2. To see the graph that represents your XML file, in the PROCESSES section, click your state machine name Demo01.

    Whenever you change the skeleton in the XML file, refresh the page to see the new changes.

  2. 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>
  1. 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>

  1. In the transition you already have, add the event to the events section and refresh the graph.
<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>
  1. 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>

The skeleton of the order process is done now. Refresh the graph and check your process.

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 into the state machine skeleton.

  1. Add a dummy command to perform the payment.

In a real scenario, this command calls a payment provider to authorize 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" />

Refresh the graph again. The command is added with the label not implemented. This means that the PHP implementation is not hooked yet.

  1. Add the command and hook it into the skeleton. The command is simply a Spryker plugin connected to the OMS module.

For the demo, you add the command plugin directly to the OMS module. In a real-life scenario, you can include the plugin in any other module, depending on the software design of your shop.

Add the command plugin to src/Pyz/Zed/Oms/Communication/Plugin/Command/Demo and call it PayCommand.

As the command is a plugin, it implements some interface. The interface for the command 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 [];
    }
}
  1. 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;
}

Refresh the graph. The not implemented label is not displayed anymore, meaning that the state machine recognizes the command.

  1. Add the condition in the same way but use 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 transitions paid->shipped and paid->unauthorized must use the same event name with a condition on one of the transitions.

The machine then examines the condition. If it returns true, then go to shipped state. Otherwise, go to unauthorized.

The skeleton looks 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 is as follows:

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 is as follows:


/**
 * @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 works now.

4. Test the state machine

After building a new order process, you need to test it:

  1. Go to the shop, chose a product, add it to the cart, and checkout using the Invoice payment method.
  2. Open the orders page in Zed UI and then open your order.

This order is now applying the process you have defined in the state machine.

  1. There is the ship button to trigger the manual event ship.
  2. Click on the last state name under the state column to see the current state for a specific item.

The current state has a yellowish background color.

  1. To move the item into the next state click ship.
  2. Click again on the last state name and check the current state.

You can keep moving the item until the order is closed.

5. Define the happy path of an order item (optional)

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, write happy = "true" on the transitions that are a part of your process happy path and refresh the graph.