Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/acp 4802/master over underpaid #3053

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Asynchronous API for payment service providers
description: Overview of PSP Asynchronous API
last_updated: Now 08, 2024
last_updated: Feb 19, 2025
template: concept-topic-template
related:
- title: Configure and disconnect flows for payment service providers
Expand Down Expand Up @@ -32,6 +32,8 @@ Sent from the app:
* `PaymentRefundFailed`: Payment refund fails.
* `PaymentCanceled`: A payment is canceled.
* `PaymentCancellationFailed`: Payment cancellation fails.
* `PaymentOverpaid`: A Payment is overpaid.
* `PaymentUnderpaid`: A Payment is underpaid.
* `PaymentCreated`: A payment is created.
* `PaymentUpdated`: A payment is updated.
* `ReadyForMerchantAppOnboarding`: App is ready to onboard merchants.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,21 @@ Before you begin, make sure the following prerequisites are met:
- You have installed the Spryker Order Management System. For the installation instructions, see [Install the Order Management feature](/docs/pbc/all/order-management-system/{{site.version}}/base-shop/install-and-upgrade/install-features/install-the-order-management-feature.html).
- You are familiar with the basics of OMS provided in [Order Management feature overview](/docs/pbc/all/order-management-system/{{site.version}}/base-shop/order-management-feature-overview/order-management-feature-overview.html), [State machine cookbook](/docs/pbc/all/order-management-system/{{site.version}}/base-shop/state-machine-cookbook/state-machine-cookbook.html) and their sub-pages.

## Default ACP payment OMS
## Default ACP Payment App OMS

The default ACP payment OMS configuration is located at `vendor/spryker/sales-payment/config/Zed/Oms/ForeignPaymentStateMachine01.xml`.
This configuration is assigned to each order paid with the ACP payment method.
The default ACP Payment App OMS configuration is located at `vendor/spryker/sales-payment/config/Zed/Oms/ForeignPaymentStateMachine01.xml`.
This configuration is assigned to each order paid with an ACP Payment App payment method. Use this one as a starting point to build your own.

### XML file structure

The main OMS file includes `<subprocesses>` – similar to libraries or building blocks with their own `<states>`, `<transitions>`, and `<events>`:

#### Subprocesses

A typical state machine consists of a couple of sub processes which are representing your complete order process.

Here is an example on how to add sub processes.

```xml
<subprocesses>
<process name="PaymentAuthorization" file="Subprocess/PaymentAuthorization01.xml"/>
Expand All @@ -33,31 +39,91 @@ The main OMS file includes `<subprocesses>` – similar to libraries or building

Each process in the `<subprocesses>` section has a start state, one or more final states, and the states are linked to each other in the main State Machine .xml file.

![default-oms](https://spryker.s3.eu-central-1.amazonaws.com/docs/dg/dev/acp/integrate-acp-payment-apps-with-spryker-oms-configuration/default-oms.png)
Other sub processes provided by Spryker are:
- `ItemClose01`
- `ItemReturn01`
- `ItemSupply01`
- `ItemSupplyNonMarketplace01`
- `MerchantOrder01` - Only needed for Marketplaces
- `MerchantPayout01` - Only needed for Marketplaces
- `MerchantPayoutReverse01` - Only needed for Marketplaces
- `PaymentCancel01`
- `PaymentCapture01`
- `PaymentRefund01`
- `PaymentRefund01NonMarketplace`

The default setup assumes a two-step process for collecting money, beginning with the `PaymentAuthorization` subprocess.
You can use those as examples and or building blocks for your own state machine definition.

### Automatic transitions of states

Transitions between states occur automatically via asynchronous ACP messages handled by the `spryker/message-broker` module.
### Transition of orders and order items

Spryker provides two ways of dealing with transitions between states based on the Payment Apps behaviour and states. Both options require a slightly different set up that will be explained in the following sections.

- Triggering events bases on the Payment App messages.
- Using conditional transitions based on the Payment App messages and used condition plugins.

The first one is set up by adding the `\Spryker\Zed\Payment\Communication\Plugin\MessageBroker\PaymentOperationsMessageHandlerPlugin` to your MessageBrokerDependencyProvider.
The second one is set up by adding the `\Spryker\Zed\PaymentApp\Communication\Plugin\MessageBroker\PaymentAppOperationsMessageHandlerPlugin` to your MessageBrokerDependencyProvider and condition plugins.

#### Transition by using event triggers

The `\Spryker\Zed\Payment\Communication\Plugin\MessageBroker\PaymentOperationsMessageHandlerPlugin` has message handlers attached that will trigger events based on the received messages from the Payment App.

Transitions between states occur automatically via asynchronous ACP messages handled by the `spryker/message-broker` module.
The sub-processes with auto-transitions are: `PaymentAuthorization`, `PaymentCapture`, `PaymentRefund`, and `PaymentCancel`.

The MessageBroker worker checks for new messages in the background (cron job) and triggers OMS events based on
the configuration in `\Spryker\Zed\Payment\PaymentConfig::getSupportedOrderPaymentEventTransfersList()`.You can modify this configuration for your project.
the configuration in `\Spryker\Zed\Payment\PaymentConfig::getSupportedOrderPaymentEventTransfersList()`. You can modify this configuration for your project.

The list of payment event messages is predefined, and they are common for all payment methods from the ACP App Catalog:
- PaymentAuthorized
- PaymentAuthorizationFailed
- PaymentCanceled
- PaymentCancellationFailed
- PaymentCaptured
- PaymentCaptureFailed
- PaymentRefunded
- PaymentRefundFailed
{% info_block infobox %}

Use this approach when you follow closely the default `ForeignPaymentStateMachine01` and most importantly, when you know your OMS does not have any slow running commands which could lead to loss of transitions.

The status of a payment on the App side can change very fast and could lead to the issue that your OMS is not in a state where the transition can be made.

F.e. when you have to have a command somewhere between `PaymentAuthorized` and `PaymentCapturePending` that takes longer to process and a message is received that want to trigger the transition from `PaymentCapturePending` to `PaymentCaptured` the OMS will not be able to do this transition as the transitions is currently not possible.

{% endinfo_block %}

#### Conditional transitions

The `\Spryker\Zed\PaymentApp\Communication\Plugin\MessageBroker\PaymentAppOperationsMessageHandlerPlugin` works differently then the above. When a message is received from the Payment App, the message handler will persist a status of the payment that is given on the App side. Through conditions (list down below) the OMS will check if the payment is in a state that allows the transition to the next state.

##### Conditions
- `IsPaymentAppPaymentStatusAuthorizationFailedConditionPlugin`
- `IsPaymentAppPaymentStatusAuthorizedConditionPlugin`
- `IsPaymentAppPaymentStatusCanceledConditionPlugin`
- `IsPaymentAppPaymentStatusCancellationFailedConditionPlugin`
- `IsPaymentAppPaymentStatusCapturedConditionPlugin`
- `IsPaymentAppPaymentStatusCaptureFailedConditionPlugin`
- `IsPaymentAppPaymentStatusCaptureRequestedConditionPlugin`
- `IsPaymentAppPaymentStatusOverpaidConditionPlugin`
- `IsPaymentAppPaymentStatusUnderpaidConditionPlugin`

Each of the conditions checks if a payment is in an expected state and if so transitions to the next one. This completely decouples your OMS from the Payment App and allows you to have slow running commands in your OMS and a much more fine-grained control.

To be able to use this approach you also need to add the OOTB provided condition plugins to your OMS configuration in the `OmsDependencyProvider::extendConditionPlugins`.

For example, a submitted order from the state `"new"` moves to the `PaymentAuthorization` sub-process where OMS will wait for a payment event from ACP apps. Depending on the received message, the order is then moved to the next state: either `"payment authorized"` or `"payment authorization failed"`.
If you see some transitions are not happening as expected you can change the configuration via the `\Spryker\Zed\PaymentApp\PaymentAppConfig::STATUS_MAP` constant.

A similar approach is implemented also in the `PaymentCapture`, `PaymentRefund` and `PaymentCancel` sub-processes.
This configuration enables the Payment App states to move faster than your OMS but your OMS conditions can still execute. F.e. the Payment App status is already moved to `PaymentCaptured` but your OMS is still in `new` state, when you now ask via conditions if the payment is authorized it will return `true` as before a payment is set to `PaymentCaptured` as it was already authorized.


### Automatic transitions of states

The list of payment event messages is predefined, and they are common for all payment methods from the ACP App Catalog:
- `PaymentAuthorized`
- `PaymentAuthorizationFailed`
- `PaymentCanceled`
- `PaymentCancellationFailed`
- `PaymentCaptured`
- `PaymentCaptureFailed`
- `PaymentRefunded`
- `PaymentRefundFailed`
- `PaymentOverpaid`
- `PaymentUnderpaid`

Make sure that these messages are configured in your `config_default.php`.

{% info_block infoBox "Manual transition between states" %}

Expand All @@ -69,6 +135,8 @@ For example, if an order got stuck in the `"payment capture pending"` state, a B

### Payment operation commands

The following commands have to be triggered from your OMS to tell the Payment App where you are and what to do next.

The default OMS setup has three commands:
- `Payment/Capture`
- `Payment/Cancel`
Expand All @@ -77,9 +145,9 @@ The default OMS setup has three commands:
The commands send asynchronous ACP messages to a payment app, allowing it to schedule the requested operation for a specific amount for the selected order and its items. The payment app responds with a payment event message indicating either success or failure.

The list of payment command messages is predefined:
- CapturePayment
- CancelPayment
- RefundPayment
- `CapturePayment`
- `CancelPayment`
- `RefundPayment`

In the project OMS configuration, you can put these commands into the needed transition.

Expand Down Expand Up @@ -107,6 +175,69 @@ $config[SalesConstants::PAYMENT_METHOD_STATEMACHINE_MAPPING] = [
];
```

### Adding Commands and Conditions

#### Adding Commands

You can add new commands to the OMS configuration by extending the `OmsDependencyProvider::extendCommandPlugins` method.

```php
...
protected function extendCommandPlugins(Container $container): Container
{
$container->extend(self::COMMAND_PLUGINS, function (CommandCollectionInterface $commandCollection) {
...
// ----- External PSP
$commandCollection->add(new SendCapturePaymentMessageCommandPlugin(), 'Payment/Capture');
$commandCollection->add(new SendRefundPaymentMessageCommandPlugin(), 'Payment/Refund');
$commandCollection->add(new RefundCommandPlugin(), 'Payment/Refund/Confirm');
$commandCollection->add(new SendCancelPaymentMessageCommandPlugin(), 'Payment/Cancel');
$commandCollection->add(new MerchantPayoutCommandByOrderPlugin(), 'SalesPaymentMerchant/Payout');
$commandCollection->add(new MerchantPayoutReverseCommandByOrderPlugin(), 'SalesPaymentMerchant/ReversePayout');

return $commandCollection;
});

return $container;
}

```

Update the list of commands to your needs.

#### Adding Conditions

You can add new conditions to the OMS configuration by extending the `OmsDependencyProvider::extendConditionPlugins` method.

```php
protected function extendConditionPlugins(Container $container): Container // phpcs:ignore SlevomatCodingStandard.Functions.UnusedParameter
{
$container->extend(self::CONDITION_PLUGINS, function (ConditionCollectionInterface $conditionCollection) {
// ----- External PSP
$conditionCollection->add(new IsMerchantPaidOutConditionPlugin(), 'SalesPaymentMerchant/IsMerchantPaidOut');
$conditionCollection->add(new IsMerchantPayoutReversedConditionPlugin(), 'SalesPaymentMerchant/IsMerchantPayoutReversed');

// ----- External PSP V2 Condition to ask for payment state instead of getting it told by the PSP
$conditionCollection->add(new IsPaymentAppPaymentStatusAuthorizationFailedConditionPlugin(), 'Payment/IsAuthorizationFailed');
$conditionCollection->add(new IsPaymentAppPaymentStatusAuthorizedConditionPlugin(), 'Payment/IsAuthorized');
$conditionCollection->add(new IsPaymentAppPaymentStatusCanceledConditionPlugin(), 'Payment/IsCanceled');
$conditionCollection->add(new IsPaymentAppPaymentStatusCancellationFailedConditionPlugin(), 'Payment/IsCancellationFailed');
$conditionCollection->add(new IsPaymentAppPaymentStatusCapturedConditionPlugin(), 'Payment/IsCaptured');
$conditionCollection->add(new IsPaymentAppPaymentStatusCaptureFailedConditionPlugin(), 'Payment/IsCaptureFailed');
$conditionCollection->add(new IsPaymentAppPaymentStatusCaptureRequestedConditionPlugin(), 'Payment/IsCaptureRequested');
$conditionCollection->add(new IsPaymentAppPaymentStatusOverpaidConditionPlugin(), 'Payment/IsOverpaid');
$conditionCollection->add(new IsPaymentAppPaymentStatusUnderpaidConditionPlugin(), 'Payment/IsUnderpaid');

return $conditionCollection;
});

return $container;
}
```

Update the list of conditions to your needs.


## Customizing for your business flow

You have several options to customize your business flow:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Create an Order Management System - Spryker Commerce OS
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..
last_updated: Oct 21, 2021
last_updated: Feb 18, 2025
template: howto-guide-template
originalLink: https://documentation.spryker.com/2021080/docs/t-oms-and-state-machines-spryker-commerce-os
originalArticleId: dc0c3c0d-c1af-4949-9645-762c67f03c8a
Expand Down Expand Up @@ -413,7 +413,119 @@ You can keep moving the item until the order is closed.

{% endinfo_block %}

### 5. Define the happy path of an order item (optional)
### 5. Automated tests for your State Machine

Besides the explained manual tests you can and should also implement automated tests for your state machine. Spryker provides some test helpers that you can use to build your tests.

- `\SprykerTest\Zed\Oms\Helper\OmsHelper` - This helper provides hooks to add your commands and conditions to the tests.
- `\SprykerTest\Shared\Sales\Helper\SalesOmsHelper` - This helper provides some handy methods to test the state machine.

#### 5.1 Add the test helper to cour `codeception.yml`

```yaml
namespace: PyzTest\Zed\YourModuleName

suites:
Integration:
path: Integration
actor: YourModuleNameIntegrationTester
modules:
enabled:
- \SprykerTest\Shared\Sales\Helper\SalesHelper
- \SprykerTest\Shared\Sales\Helper\SalesOmsHelper
- \SprykerTest\Shared\Testify\Helper\DataCleanupHelper
- \SprykerTest\Shared\Sales\Helper\SalesDataHelper
- \SprykerTest\Shared\Shipment\Helper\ShipmentMethodDataHelper
- \SprykerTest\Zed\Oms\Helper\OmsHelper:
conditions:
name-of/your-condition: \Fully\Qualified\Class\Name
...
commands:
name-of/your-command: \Fully\Qualified\Class\Name
...

```

This is a basic example which shows how to add commands and conditions to the OmsHelper. You can add as many commands and conditions as you need.

There are also some default commands and conditions for testing purposes you can use:
- `\SprykerTest\Zed\Oms\Helper\Mock\AlwaysTrueConditionPluginMock` - This condition always returns true.
- `\SprykerTest\Zed\Oms\Helper\Mock\AlwaysFalseConditionPluginMock` - This condition always returns false.
- `\SprykerTest\Zed\Oms\Helper\Mock\CommandByItemPluginMock` - This command is a mock for the CommandByItemInterface and always return an empty array.
- `\SprykerTest\Zed\Oms\Helper\Mock\CommandByOrderPluginMock` - This command is a mock for the CommandByOrderInterface and always return an empty array.

You can use those when you need placeholders for commands and conditions. The key is the name that you are using in the OmsDependencyProvider as well to set up the state machine.

{% info_block infoBox %}

Currently, it is not possible to use commands or condition at run time of the test. You have to define them in the `codeception.yml` file.

Another important thing is that it is currently only possible to use a single item in the test scenarios. And timeouts are also not testable yet.

{% endinfo_block %}

#### 5.2 Create a test for your state machine

An example test could look like this:

```php
<?php

declare(strict_types = 1);

namespace PyzTest\Zed\YourModuleName\Integration\Oms;

use Codeception\Test\Unit;
use PyzTest\Zed\YourModuleName\YourModuleNameIntegrationTester;

class OmsIntegrationTest extends Unit
{
/**
* @var string
*/
protected const STATE_MACHINE_NAME = 'ForeignPaymentStateMachine01';

/**
* @param \Codeception\TestInterface $test
*
* @return void
*/
public function _before(TestInterface $test): void
{
parent::_before($test);

$xmlFileDirectory = APPLICATION_VENDOR_DIR . 'spryker/spryker/Bundles/SalesPayment/config/Zed/Oms/';
$this->getSalesOmsHelper()->setupStateMachine(static::STATE_MACHINE_NAME, $xmlFileDirectory);
}

public function testMoveAnItemFromStateAToStateB(): void
{
// Set up one single item with its current expected state
$this->tester->haveOrderItemInState('a');

// do something where you expect that the item should move to the next state

// Trigger the state machine
$this->tester->tryToTransitionOrderItems();
// or
$this->tester->tryToTransitionOrderItems('event name');

// Assert that the item is moved to the expected state.
$this->tester->assertOrderItemIsInState('b');
}
}
```

In the `_before` method the state machine setup is done. You need to provide the state machine name and the path to the XML files of your definitions.

The first method `haveOrderItemInState` sets up an item in the state `a`. This method also accepts a second argument where you can pass individual fields to be used for order item creation.
After that you need to add your code where you expect that the item should move to the next state. F.e. receiving an Async API message or calling a command.
The second method (use only one of both examples) `tryToTransitionOrderItems` triggers the state machine to move the item to the next state if possible. When used without an event name only conditions are checked. If you want to trigger a specific event you can pass the event name as a parameter.
The last method asserts that the item is in the expected state `b`.

These tests can be easy but also become very complex depending on your needs for testing. When you need more items or processing of complete orders you currently need to create your own helper.

### 6. 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.

Expand Down
Loading
Loading