Install the Recurring Orders feature EARLY ACCESS

Edit on GitHub
Early Access

This feature is in Early Access. We’d love for you to try it out and share feedback as we work toward general availability.

This document describes how to install the Recurring Orders feature.

Install feature core

Follow the steps below to install the Recurring Orders feature core.

Prerequisites

To start feature integration, review and install the necessary features:

NAME VERSION INSTALLATION GUIDE
Spryker Core 202606.0 Install the Spryker Core feature
Company Account 202606.0 Install the Company Account feature
Checkout 202606.0 Install the Checkout feature

1) Install the required modules

Required modules
composer require spryker-feature/order-experience-management:"^0.1.4" --update-with-dependencies
composer update \
  spryker/availability:"^9.32.0" \
  spryker/merchant:"^3.20.0" \
  spryker/merchant-product-option:"^1.4.0" \
  spryker/merchant-switcher:"^0.6.8" \
  spryker/product-approval:"^1.5.0" \
  spryker/product-bundle:"^7.28.0" \
  spryker/product-cart-connector:"^4.15.0" \
  spryker/product-configuration-cart:"^1.1.0" \
  spryker/product-discontinued:"^1.15.0" \
  spryker/product-offer:"^1.18.0" \
  spryker/product-packaging-unit:"^4.14.0" \
  spryker/product-quantity:"^3.8.0" \
  spryker-feature/purchasing-control:"^1.1.1" \
  spryker-shop/checkout-page:"^3.42.0" \
  spryker-shop/customer-page:"^2.80.0" \
  --with-dependencies

2) Set up database schema and transfer objects

Apply database changes and generate entity and transfer changes:

console propel:install
console transfer:generate
Verification

Make sure the following changes have been applied in the database:

DATABASE ENTITY TYPE EVENT
spy_recurring_schedule table created
spy_recurring_schedule_item table created
spy_recurring_schedule_history table created

Make sure the following changes have been applied in transfer objects:

TRANSFER TYPE EVENT PATH
RecurringSchedule class created src/Generated/Shared/Transfer/RecurringScheduleTransfer.php
RecurringScheduleCollection class created src/Generated/Shared/Transfer/RecurringScheduleCollectionTransfer.php
RecurringScheduleCriteria class created src/Generated/Shared/Transfer/RecurringScheduleCriteriaTransfer.php
RecurringScheduleConditions class created src/Generated/Shared/Transfer/RecurringScheduleConditionsTransfer.php
RecurringScheduleCollectionRequest class created src/Generated/Shared/Transfer/RecurringScheduleCollectionRequestTransfer.php
RecurringScheduleCollectionResponse class created src/Generated/Shared/Transfer/RecurringScheduleCollectionResponseTransfer.php
RecurringScheduleItem class created src/Generated/Shared/Transfer/RecurringScheduleItemTransfer.php
RecurringScheduleHistory class created src/Generated/Shared/Transfer/RecurringScheduleHistoryTransfer.php
RecurringScheduleValidationResult class created src/Generated/Shared/Transfer/RecurringScheduleValidationResultTransfer.php
RecurringScheduleItemReview class created src/Generated/Shared/Transfer/RecurringScheduleItemReviewTransfer.php
RecurringScheduleReviewResponse class created src/Generated/Shared/Transfer/RecurringScheduleReviewResponseTransfer.php
RecurringScheduleEventRequest class created src/Generated/Shared/Transfer/RecurringScheduleEventRequestTransfer.php
RecurringScheduleEventResponse class created src/Generated/Shared/Transfer/RecurringScheduleEventResponseTransfer.php
RecurringScheduleStatusCountCollection class created src/Generated/Shared/Transfer/RecurringScheduleStatusCountCollectionTransfer.php
RecurringOrderSettings class created src/Generated/Shared/Transfer/RecurringOrderSettingsTransfer.php
RecurringOrderQuoteUpdateRequest class created src/Generated/Shared/Transfer/RecurringOrderQuoteUpdateRequestTransfer.php
RecurringOrderQuoteUpdateResponse class created src/Generated/Shared/Transfer/RecurringOrderQuoteUpdateResponseTransfer.php
Quote.recurringOrderSettings property created src/Generated/Shared/Transfer/QuoteTransfer.php

3) Set up data import

Import the CMS blocks that provide the HTML and text templates for recurring order notification emails.

The CMS block definitions are provided in the module at src/SprykerFeature/OrderExperienceManagement/data/import/cms_block.csv. Copy the contents of that file and add them to data/import/common/common/cms_block.csv.

For each store you want to enable the email notifications in, add the corresponding block keys to data/import/common/{store}/cms_block_store.csv.

Import the data:

console data:import:cms-block
console data:import:cms-block-store
Verification

In the Back Office, under Content > Blocks, make sure the CMS blocks from the module file are present and active.

4) Set up behavior

Enable the following behaviors by registering the plugins.

Set up Checkout plugins

PLUGIN SPECIFICATION PREREQUISITES NAMESPACE
RecurringOrderCheckoutPreConditionPlugin Validates the quote is eligible for a recurring order before checkout proceeds. Checks that the quote is not locked, not from an RFQ, not a guest session, the payment method is invoice-based, and the cadence type is registered and valid. None SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Checkout
RecurringOrdersCheckoutPostSavePlugin Creates a recurring schedule and registers it with the state machine after the order is successfully saved. Does nothing when recurringOrderSettings is not set on the quote. None SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Checkout

src/Pyz/Zed/Checkout/CheckoutDependencyProvider.php

<?php

namespace Pyz\Zed\Checkout;

use Spryker\Zed\Checkout\CheckoutDependencyProvider as SprykerCheckoutDependencyProvider;
use Spryker\Zed\Kernel\Container;
use SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Checkout\RecurringOrderCheckoutPreConditionPlugin;
use SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Checkout\RecurringOrdersCheckoutPostSavePlugin;

class CheckoutDependencyProvider extends SprykerCheckoutDependencyProvider
{
    /**
     * @param \Spryker\Zed\Kernel\Container $container
     *
     * @return list<\Spryker\Zed\CheckoutExtension\Dependency\Plugin\CheckoutPreConditionPluginInterface>
     */
    protected function getCheckoutPreConditions(Container $container): array
    {
        return [
            // ...
            new RecurringOrderCheckoutPreConditionPlugin(), #RecurringOrdersFeature
        ];
    }

    /**
     * @param \Spryker\Zed\Kernel\Container $container
     *
     * @return list<\Spryker\Zed\CheckoutExtension\Dependency\Plugin\CheckoutPostSaveInterface>
     */
    protected function getCheckoutPostHooks(Container $container): array
    {
        return [
            // ...
            new RecurringOrdersCheckoutPostSavePlugin(), #RecurringOrdersFeature
        ];
    }
}
Verification
  1. Add a product to the cart, set a recurring order cadence on the quote, and complete checkout. Make sure a recurring schedule is created in spy_recurring_schedule.
  2. Attempt to place a recurring order with a non-invoice payment method. Make sure checkout is blocked.
  3. Attempt to place a recurring order with an invalid cadence type. Make sure checkout is blocked.

Set up the Subscription dependency provider

Register the built-in cadence type and schedule validator plugins:

PLUGIN SPECIFICATION PREREQUISITES NAMESPACE
WeeklyCadenceTypePlugin Calculates the next trigger date 7 days after the current trigger date. None SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Cadence
BiWeeklyCadenceTypePlugin Calculates the next trigger date 14 days after the current trigger date. None SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Cadence
MonthlyCadenceTypePlugin Calculates the next trigger date on the same day of the following month. None SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Cadence
EveryNWeeksCadenceTypePlugin Calculates the next trigger date every N weeks. Requires cadenceValue to be set on the schedule. None SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Cadence
PriceScheduleValidatorPlugin Detects price increases on recurring schedule items compared to their stored reference prices before order placement. None SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\ScheduleValidator
CheckoutPlaceabilityScheduleValidatorPlugin Simulates a checkout to detect availability or product approval issues on recurring schedule items before order placement. None SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\ScheduleValidator

src/Pyz/Zed/OrderExperienceManagement/OrderExperienceManagementDependencyProvider.php

<?php

namespace Pyz\Zed\OrderExperienceManagement;

use SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Cadence\BiWeeklyCadenceTypePlugin;
use SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Cadence\EveryNWeeksCadenceTypePlugin;
use SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Cadence\MonthlyCadenceTypePlugin;
use SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Cadence\WeeklyCadenceTypePlugin;
use SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\ScheduleValidator\CheckoutPlaceabilityScheduleValidatorPlugin;
use SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\ScheduleValidator\PriceScheduleValidatorPlugin;
use SprykerFeature\Zed\OrderExperienceManagement\OrderExperienceManagementDependencyProvider as SprykerOrderExperienceManagementDependencyProvider;

class OrderExperienceManagementDependencyProvider extends SprykerOrderExperienceManagementDependencyProvider
{
    /**
     * @return array<\SprykerFeature\Zed\OrderExperienceManagement\Dependency\Plugin\CadenceTypePluginInterface>
     */
    protected function getCadenceTypePlugins(): array
    {
        return [
            new WeeklyCadenceTypePlugin(), #RecurringOrdersFeature
            new BiWeeklyCadenceTypePlugin(), #RecurringOrdersFeature
            new MonthlyCadenceTypePlugin(), #RecurringOrdersFeature
            new EveryNWeeksCadenceTypePlugin(), #RecurringOrdersFeature
        ];
    }

    /**
     * @return array<\SprykerFeature\Zed\OrderExperienceManagement\Dependency\Plugin\ScheduleValidatorPluginInterface>
     */
    protected function getScheduleValidatorPlugins(): array
    {
        return [
            new PriceScheduleValidatorPlugin(), #RecurringOrdersFeature
            new CheckoutPlaceabilityScheduleValidatorPlugin(), #RecurringOrdersFeature
        ];
    }
}
Verification

Make sure all four cadence types (weekly, bi-weekly, monthly, every N weeks) are available when setting up a recurring order on the storefront.

Set up the state machine handler

Register the recurring orders state machine handler:

PLUGIN SPECIFICATION PREREQUISITES NAMESPACE
RecurringOrdersStateMachineHandlerPlugin Registers the RecurringOrder state machine process, maps commands and conditions to plugins, updates the state machine item state on each transition, and returns schedule items by state IDs. None SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\StateMachine

src/Pyz/Zed/StateMachine/StateMachineDependencyProvider.php

<?php

namespace Pyz\Zed\StateMachine;

use Spryker\Zed\StateMachine\StateMachineDependencyProvider as SprykerStateMachineDependencyProvider;
use SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\StateMachine\RecurringOrdersStateMachineHandlerPlugin;

class StateMachineDependencyProvider extends SprykerStateMachineDependencyProvider
{
    /**
     * @return array<\Spryker\Zed\StateMachine\Dependency\Plugin\StateMachineHandlerInterface>
     */
    protected function getStateMachineHandlers(): array
    {
        return [
            // ...
            new RecurringOrdersStateMachineHandlerPlugin(), #RecurringOrdersFeature
        ];
    }
}

Copy the state machine process XML from the module into your project. The example file is located at src/SprykerFeature/OrderExperienceManagement/config/Zed/StateMachine/RecurringOrder/RecurringOrderStateMachine.xml in the module. Add it to your project at the following path:

config/Zed/StateMachine/RecurringOrder/RecurringOrderStateMachine.xml

Verification

In the Back Office, under Maintenance > State Machine, make sure the RecurringOrderStateMachine process is listed and the diagram renders correctly.

Set up Mail plugins

Register the following mail type builder plugins:

PLUGIN SPECIFICATION PREREQUISITES NAMESPACE
RecurringOrderUpcomingNotificationMailTypeBuilderPlugin Builds the pre-trigger notification email sent to the buyer a configurable number of hours before the scheduled order is placed. None SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Mail
RecurringOrderValidationFailedMailTypeBuilderPlugin Builds the review-required notification email sent to the buyer when a price increase or product availability issue is detected. None SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Mail
RecurringOrderFailureMailTypeBuilderPlugin Builds the order placement failure notification email sent to the buyer when order placement fails. None SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Mail

src/Pyz/Zed/Mail/MailDependencyProvider.php

<?php

namespace Pyz\Zed\Mail;

use Spryker\Zed\Mail\MailDependencyProvider as SprykerMailDependencyProvider;
use SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Mail\RecurringOrderFailureMailTypeBuilderPlugin;
use SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Mail\RecurringOrderUpcomingNotificationMailTypeBuilderPlugin;
use SprykerFeature\Zed\OrderExperienceManagement\Communication\Plugin\Mail\RecurringOrderValidationFailedMailTypeBuilderPlugin;

class MailDependencyProvider extends SprykerMailDependencyProvider
{
    /**
     * @return array<\Spryker\Zed\MailExtension\Dependency\Plugin\MailTypeBuilderPluginInterface>
     */
    protected function getMailTypeBuilderPlugins(): array
    {
        return [
            // ...
            new RecurringOrderUpcomingNotificationMailTypeBuilderPlugin(), #RecurringOrdersFeature
            new RecurringOrderValidationFailedMailTypeBuilderPlugin(), #RecurringOrdersFeature
            new RecurringOrderFailureMailTypeBuilderPlugin(), #RecurringOrdersFeature
        ];
    }
}
Verification

Trigger a recurring order cycle and verify the following:

  • A pre-trigger notification email is sent to the buyer within the configured notification window hours.
  • When a price increase or product availability issue is detected, a review-required email is sent.
  • When order placement fails, a failure notification email is sent.

Set up permissions

By default, a company user can only see their own recurring orders. To allow users to view recurring orders across their company or business unit, register the following permission plugins:

PLUGIN SPECIFICATION PREREQUISITES NAMESPACE
SeeCompanyOrdersPermissionPlugin Grants permission to view all recurring orders within the company. Assign to company roles that should have company-wide visibility. None Spryker\Zed\CompanySalesConnector\Communication\Plugin\Permission
SeeBusinessUnitOrdersPermissionPlugin Grants permission to view all recurring orders within the company business unit. Assign to company roles that should have business-unit-wide visibility. None Spryker\Zed\CompanyBusinessUnitSalesConnector\Communication\Plugin\Permission

src/Pyz/Zed/Permission/PermissionDependencyProvider.php

<?php

namespace Pyz\Zed\Permission;

use Spryker\Zed\CompanyBusinessUnitSalesConnector\Communication\Plugin\Permission\SeeBusinessUnitOrdersPermissionPlugin;
use Spryker\Zed\CompanySalesConnector\Communication\Plugin\Permission\SeeCompanyOrdersPermissionPlugin;
use Spryker\Zed\Permission\PermissionDependencyProvider as SprykerPermissionDependencyProvider;

class PermissionDependencyProvider extends SprykerPermissionDependencyProvider
{
    /**
     * @return array<\Spryker\Shared\PermissionExtension\Dependency\Plugin\PermissionPluginInterface>
     */
    protected function getPermissionPlugins(): array
    {
        return [
            // ...
            new SeeCompanyOrdersPermissionPlugin(), #RecurringOrdersFeature
            new SeeBusinessUnitOrdersPermissionPlugin(), #RecurringOrdersFeature
        ];
    }
}

src/Pyz/Client/Permission/PermissionDependencyProvider.php

<?php

namespace Pyz\Client\Permission;

use Spryker\Client\CompanyBusinessUnitSalesConnector\Plugin\Permission\SeeBusinessUnitOrdersPermissionPlugin;
use Spryker\Client\CompanySalesConnector\Plugin\Permission\SeeCompanyOrdersPermissionPlugin;
use Spryker\Client\Permission\PermissionDependencyProvider as SprykerPermissionDependencyProvider;

class PermissionDependencyProvider extends SprykerPermissionDependencyProvider
{
    /**
     * @return array<\Spryker\Shared\PermissionExtension\Dependency\Plugin\PermissionPluginInterface>
     */
    protected function getPermissionPlugins(): array
    {
        return [
            // ...
            new SeeCompanyOrdersPermissionPlugin(), #RecurringOrdersFeature
            new SeeBusinessUnitOrdersPermissionPlugin(), #RecurringOrdersFeature
        ];
    }
}

Sync the permission plugins to the database:

console sync:data permission
Verification

In the Back Office, under Customers > Company Roles, assign SeeCompanyOrdersPermissionPlugin to a company role. Make sure company users with that role can see all recurring orders within their company on the storefront.

Assign SeeBusinessUnitOrdersPermissionPlugin to a role. Make sure users with that role can see all recurring orders within their business unit.

Set up cron jobs

The recurring orders state machine relies on a cron job to evaluate condition transitions. Register the job in your Jenkins configuration:

config/Zed/cronjobs/jenkins.php

/* RecurringOrder StateMachine */
$jobs[] = [
    'name' => 'recurring-order-check-conditions',
    'command' => '$PHP_BIN vendor/bin/console state-machine:check-condition RecurringOrder',
    'schedule' => '* * * * *',
    'enable' => true,
];
Scheduling recommendation

To have recurring orders placed before the business day starts, set trigger dates to an early morning time (for example, 01:00) and configure DEFAULT_NOTIFICATION_WINDOW_HOURS to 18 (for example) or more. With an 18-hour window, the pre-trigger notification is sent the previous afternoon (after 12:00), allowing the buyer to review or skip before the order is placed overnight.

If your project uses Symfony Scheduler instead of Jenkins, register the equivalent job in your scheduler config:

src/Pyz/Zed/SymfonyScheduler/SymfonySchedulerConfig.php

'recurring-orders-check-condition' => [
    'command' => '$PHP_BIN vendor/bin/console state-machine:check-condition RecurringOrder',
    'schedule' => '* * * * *',
],
'recurring-orders-clear-locks' => [
    'command' => '$PHP_BIN vendor/bin/console state-machine:clear-locks',
    'schedule' => '0 6 * * *',
],
Verification

Activate a recurring schedule. Make sure the state machine condition check job runs and the schedule transitions from draft to active within one minute.

Optional: Register the trigger console command

RecurringOrderTriggerConsole lets you manually trigger order placement for a specific recurring schedule from the CLI. It runs the same placement logic as the state machine PlaceOrderCommand, which makes it useful for development, debugging, and one-off operational tasks.

To enable it, register the command in your console dependency provider:

src/Pyz/Zed/Console/ConsoleDependencyProvider.php

<?php

namespace Pyz\Zed\Console;

use Spryker\Zed\Console\ConsoleDependencyProvider as SprykerConsoleDependencyProvider;
use SprykerFeature\Zed\OrderExperienceManagement\Communication\Console\RecurringOrderTriggerConsole;

class ConsoleDependencyProvider extends SprykerConsoleDependencyProvider
{
    /**
     * @param \Spryker\Zed\Kernel\Container $container
     *
     * @return list<\Symfony\Component\Console\Command\Command>
     */
    protected function getConsoleCommands(Container $container): array
    {
        return [
            // ...
            new RecurringOrderTriggerConsole(), #RecurringOrdersFeature
        ];
    }
}

Usage:

# Trigger placement by numeric ID
console recurring-orders:trigger-placement 42

# Trigger placement by UUID
console recurring-orders:trigger-placement 550e8400-e29b-41d4-a716-446655440000

# Run pre-placement validation first; aborts if validation fails
console recurring-orders:trigger-placement 42 --validate

The --validate flag runs all registered ScheduleValidatorPlugin implementations (for example, PriceScheduleValidatorPlugin and CheckoutPlaceabilityScheduleValidatorPlugin) before attempting placement. If any validator reports a failure, the command exits with an error and does not place the order.

Development use only

This command is intended for development and debugging. Do not use it in production automated pipelines — the state machine cron job is the intended trigger for production order placement.

Configure product bundle field copying

Product Bundles feature

This step is only required if your project uses the Product Bundles feature.

Override the allowed fields to copy so that shipment information is preserved when recurring orders re-create bundle items:

PLUGIN SPECIFICATION PREREQUISITES NAMESPACE
getAllowedBundleItemFieldsToCopy() Returns the list of ItemTransfer fields copied from bundle items when they are duplicated during order re-creation. Adding ItemTransfer::SHIPMENT ensures the shipment is preserved on each recurring order placement. None Pyz\Zed\ProductBundle

src/Pyz/Zed/ProductBundle/ProductBundleConfig.php

<?php

namespace Pyz\Zed\ProductBundle;

use Generated\Shared\Transfer\ItemTransfer;
use Spryker\Zed\ProductBundle\ProductBundleConfig as SprykerProductBundleConfig;

class ProductBundleConfig extends SprykerProductBundleConfig
{
    /**
     * @return list<string>
     */
    public function getAllowedBundleItemFieldsToCopy(): array
    {
        return [
            ItemTransfer::SHIPMENT,
        ];
    }
}
Verification

Add a product bundle to the cart, set up a recurring order, and complete checkout. On the next scheduled order placement, make sure the bundle items are re-created with the correct shipment assignment.

5) Configure module behavior

Override the following configuration methods in your project (if needed) to adjust the default behavior:

src/Pyz/Zed/OrderExperienceManagement/OrderExperienceManagementConfig.php

<?php

namespace Pyz\Zed\OrderExperienceManagement;

use SprykerFeature\Shared\OrderExperienceManagement\OrderExperienceManagementConfig as SharedOrderExperienceManagementConfig;
use SprykerFeature\Zed\OrderExperienceManagement\OrderExperienceManagementConfig as SprykerOrderExperienceManagementConfig;

class OrderExperienceManagementConfig extends SprykerOrderExperienceManagementConfig
{
    /**
     * Specification:
     * - Returns the number of hours before the trigger date when the pre-trigger notification is sent.
     * - Default: 48 hours.
     * - Overriding this value affects all schedules that do not have a per-schedule override.
     *
     * @api
     */
    public function getDefaultNotificationWindowHours(): int
    {
        return 18;
    }

    /**
     * Specification:
     * - Returns a map of review reason groups to the checkout error types that resolve to them.
     * - Override to add custom checkout error types to existing groups or to introduce new groups.
     * - The key is a SharedOrderExperienceManagementConfig::REVIEW_REASON_GROUP_* constant.
     * - The value is a list of raw checkout error type strings reported by the checkout facade.
     *
     * @api
     *
     * @return array<string, array<string>>
     */
    public function getReviewReasonGroupMap(): array
    {
        return array_merge_recursive(parent::getReviewReasonGroupMap(), [
            SharedOrderExperienceManagementConfig::REVIEW_REASON_GROUP_UNAVAILABLE => [
                // Add project-specific checkout error types here.
            ],
        ]);
    }

    /**
     * Specification:
     * - Returns the review reason groups whose items are treated as non-purchasable.
     * - Items in these groups block order placement and must be removed before the order can proceed.
     * - Default: [REVIEW_REASON_GROUP_UNAVAILABLE].
     * - Override to add REVIEW_REASON_GROUP_DISCONTINUED if discontinued items should also block placement.
     *
     * @api
     *
     * @return array<string>
     */
    public function getNonPurchasableReviewReasonGroups(): array
    {
        return [
            SharedOrderExperienceManagementConfig::REVIEW_REASON_GROUP_UNAVAILABLE,
            SharedOrderExperienceManagementConfig::REVIEW_REASON_GROUP_DISCONTINUED,
        ];
    }
}
CONFIGURATION METHOD DEFAULT DESCRIPTION
getDefaultNotificationWindowHours() 48 Number of hours before the trigger date when the pre-trigger notification is sent. Per-schedule overrides stored in spy_recurring_schedule.notification_window_hours take precedence.
getReviewReasonGroupMap() See OrderExperienceManagementConfig Maps review reason groups to checkout error types. Extend to map project-specific error types to the appropriate review group.
getNonPurchasableReviewReasonGroups() [REVIEW_REASON_GROUP_UNAVAILABLE] Review reason groups whose items block order placement and must be removed before the order can proceed. Override to also block on REVIEW_REASON_GROUP_DISCONTINUED.

Install feature frontend

Follow the steps below to install the Recurring Orders feature frontend.

1) Set up routes

Register the following route provider plugin:

PLUGIN SPECIFICATION PREREQUISITES NAMESPACE
RecurringOrderRouteProviderPlugin Adds storefront routes for the recurring order list, detail, create, clear, pause, resume, skip, cancel, confirm, review, and approve-review actions. None SprykerFeature\Yves\OrderExperienceManagement\Plugin\Router

src/Pyz/Yves/Router/RouterDependencyProvider.php

<?php

namespace Pyz\Yves\Router;

use Spryker\Yves\Router\RouterDependencyProvider as SprykerRouterDependencyProvider;
use SprykerFeature\Yves\OrderExperienceManagement\Plugin\Router\RecurringOrderRouteProviderPlugin;

class RouterDependencyProvider extends SprykerRouterDependencyProvider
{
    /**
     * @return array<\Spryker\Yves\RouterExtension\Dependency\Plugin\RouteProviderPluginInterface>
     */
    protected function getRouteProvider(): array
    {
        return [
            // ...
            new RecurringOrderRouteProviderPlugin(), #RecurringOrdersFeature
        ];
    }
}

After registering the plugin, warm up the router caches:

vendor/bin/yves router:cache:warm-up
vendor/bin/console router:cache:warm-up
vendor/bin/console router:cache:warm-up:backend-gateway
Verification

Make sure the following storefront routes are accessible:

  • /recurring-orders — recurring order list page.
  • /recurring-orders/{uuid} — recurring order detail page.
  • /recurring-orders/{uuid}/review-required — review required page.
  • POST /recurring-order/save — saves recurring order settings on the quote.
  • POST /recurring-order/clear — removes recurring order settings from the quote.

2) Set up widgets

Register the following global widgets:

WIDGET DESCRIPTION NAMESPACE
RecurringOrderSelectorWidget Renders the recurring order setup form at checkout. Visible only when the quote is eligible for a recurring order (invoice payment, not locked, not from RFQ, not guest). SprykerFeature\Yves\OrderExperienceManagement\Widget
RecurringOrderMenuItemWidget Renders the Recurring Orders navigation menu item in the storefront company menu. SprykerFeature\Yves\OrderExperienceManagement\Widget
CostCenterDetailWidget Displays the selected cost center and budget on the cart page. Takes a QuoteTransfer as input. Requires the Purchasing Control feature. SprykerFeature\Yves\PurchasingControl\Widget

src/Pyz/Yves/ShopApplication/ShopApplicationDependencyProvider.php

<?php

namespace Pyz\Yves\ShopApplication;

use SprykerFeature\Yves\PurchasingControl\Widget\CostCenterDetailWidget;
use SprykerFeature\Yves\OrderExperienceManagement\Widget\RecurringOrderMenuItemWidget;
use SprykerFeature\Yves\OrderExperienceManagement\Widget\RecurringOrderSelectorWidget;
use SprykerShop\Yves\ShopApplication\ShopApplicationDependencyProvider as SprykerShopApplicationDependencyProvider;

class ShopApplicationDependencyProvider extends SprykerShopApplicationDependencyProvider
{
    /**
     * @return array<string>
     */
    protected function getGlobalWidgets(): array
    {
        return [
            // ...
            RecurringOrderSelectorWidget::class, #RecurringOrdersFeature
            RecurringOrderMenuItemWidget::class, #RecurringOrdersFeature
            CostCenterDetailWidget::class, #RecurringOrdersFeature
        ];
    }
}

3) Add the recurring order selector to the checkout summary page

The RecurringOrderSelectorWidget is not rendered automatically — it must be explicitly called from the checkout summary template. Add it after any cost center or voucher sections, before the order form.

src/Pyz/Yves/CheckoutPage/Theme/default/views/summary/summary.twig

{% widget 'RecurringOrderSelectorWidget' args [data.cart] only %}{% endwidget %}
Verification

On the checkout summary page with an invoice-based payment method, make sure the Set up as recurring order checkbox and description are displayed.

Verification
  • On the cart page, make sure the selected cost center and budget names are displayed.

4) Add the menu item to the customer navigation sidebar

To make the Recurring Orders menu item appear in the customer account sidebar, add a plain data entry to the data.items array in your project’s customer navigation sidebar template. Place it after the Order History item.

src/Pyz/Yves/CustomerPage/Theme/default/components/molecules/navigation-sidebar/navigation-sidebar.twig


{% define data = {
    items: [
        {# ... existing items ... #}
        {
            name: 'order',
            url: path('customer/order'),
            label: 'customer.account.order_history' | trans,
            icon: 'history',
        },
        {
            name: 'recurring-orders',
            url: path('recurring-orders'),
            label: 'recurring_orders.menu_item' | trans,
            icon: 'calendar',
        },
        {# ... remaining items ... #}
    ]
} %}

Verification

In the storefront customer account, make sure the Recurring Orders menu item appears directly below Order History in the left sidebar navigation and links to /recurring-orders.

Alternative: widget-based menu item

If your project’s navigation sidebar template doesn’t use a plain data.items array — for example, it’s built from a custom navigation plugin or uses a different template structure — you can render the menu item via RecurringOrderMenuItemWidget instead. Make sure the widget is registered in ShopApplicationDependencyProvider (see Set up widgets), then call it from the postContent block of your sidebar template:


{% block postContent %}
    {# ... existing widget calls ... #}
    {% widget 'RecurringOrderMenuItemWidget' args [data.activePage] only %}{% endwidget %}
{% endblock %}

The widget renders an <li> element and must be placed inside a <ul> context.

5) Build the frontend

After making changes to Twig templates and registering new widgets, rebuild the Yves frontend assets:

npm run yves
Verification

Reload the checkout summary page and make sure the recurring order selector renders without console errors.

7) Import glossary data

The full list of glossary keys is provided in the module at src/SprykerFeature/OrderExperienceManagement/data/import/glossary.csv. Copy the contents of that file and add them to data/import/common/common/glossary.csv.

Import data:

console data:import:glossary
Verification

Make sure that, in the database, the configured data has been added to the spy_glossary_key and spy_glossary_translation tables.

8) Configure the Back Office settings

Sync the recurring orders configuration settings to the database to make them editable in the Back Office:

console configuration:sync
Verification

In the Back Office, go to Configuration > Recurring Orders > General > Schedule. Make sure the Schedule Grace Period field is displayed with a default value of 48.

Configurable settings
SETTING DEFAULT DESCRIPTION
Schedule Grace Period 48 Number of hours before the trigger date when the pre-trigger notification email is sent to the buyer. Per-schedule overrides stored in spy_recurring_schedule.notification_window_hours take precedence over this global value.