Skip to content

Stock and Source Configuration design

Ievgen Shakhsuvarov edited this page Jul 16, 2019 · 30 revisions

Table of Contents

CQRS in Source and Stock APIs for Configuration

The main technical concept we follow in MSI architecture is to segregate Command and Query APIs. In our case, the role of Query APIs play StockItem related interfaces and services as the StockItem entity is an aggregated indexed data among the physical locations (Sources)plus additional business rules on top of it, so that StockItem data is calculated based on the raw data we have on Source level. A customer working with front-end does not update StockItem data directly, he just exposes commands which would be processed by the system later (the role of such commands play Reservation mechanism). This concept has been used for Quantity and Salability status calculation, that's why Magento front-end which works only with Stock Services (i.e. IsProductSalableInterface, IsProductSalableForRequestedQtyInterface, GetProductSalableQtyInterface) is agnostic to the fact whether Magento is Single or Multi-Source under the hood, and the number of sources from which given Stock consists of don't affect the system performance, as the reservation we place at the time of Checkout created on the level of Stock. In contrary to this, an Admin workin in Magento Admin panel updates data on the level of SourceItem which would lead to re-building the StockItem data index. You can get more details about this concept from [CQRS and EventSourcing webinar] video recording (https://www.youtube.com/watch?v=K1jLOiIXL2M).

The similar principle should be applied for StockItem configuration as well so that Front-end would work with StockItem configuration only which is built based on SourceItem configuration. But the things here are a bit more complicated than in comparison to stock availability and salable quantity calculation because there are different configuration options and some of them could be specified on the level of Source, others on the level of Stock. Thus, the process of Indexation and Aggregation data for StockItemConfiguration indexes would be more sophisticated as it should apply own corresponding business rules to build and store each particular Config Option.

Here is a list of all Stock/Source configuration options.

Inventory Options

You can modify global and product specific inventory options through the following locations in the Magento Admin:

  • Inventory page in the Stores > Configuration settings
  • Sources section > Assigned Sources grid when editing a product in Catalog > Products
  • Advanced Inventory page when editing a product

Refer to the Inventory Options table for information on all product options and configurations details.

Programming Interface Query API

It's proposed to remove all the command methods which alter Stock Configuration data out of StockItemConfigurationInterface and make it pure immutable, representing the indexed and aggregated data.

namespace Magento\InventoryConfigurationApi\Api\Data;

/**
 * @api
 */
interface StockItemConfigurationInterface
{
    const BACKORDERS_NO = 0;
    const BACKORDERS_YES_NONOTIFY = 1;
    const BACKORDERS_YES_NOTIFY = 2;

    const IS_QTY_DECIMAL = 'is_qty_decimal';
    const SHOW_DEFAULT_NOTIFICATION_MESSAGE = 'show_default_notification_message';

    /*
     * Safety stock threshold
     */
    const MIN_QTY = 'min_qty';

    /*
     * Threshold intended to show the "Only X left"
     */
    const STOCK_THRESHOLD_QTY = 'stock_threshold_qty';

    /*
     * Used to prevent to buy less than a certain qty of a product, not to confuse with the safety stock threshold
     */
    const MIN_SALE_QTY = 'min_sale_qty';

    const MAX_SALE_QTY = 'max_sale_qty';
    const BACKORDERS = 'backorders';
    const NOTIFY_STOCK_QTY = 'notify_stock_qty';
    const QTY_INCREMENTS = 'qty_increments';
    const ENABLE_QTY_INCREMENTS = 'enable_qty_increments';
    const MANAGE_STOCK = 'manage_stock';
    const LOW_STOCK_DATE = 'low_stock_date';
    const IS_DECIMAL_DIVIDED = 'is_decimal_divided';
    const STOCK_STATUS_CHANGED_AUTO = 'stock_status_changed_auto';

    /**
     * @return bool
     */
    public function isQtyDecimal(): bool;

    /**
     * @return bool
     */
    public function isShowDefaultNotificationMessage(): bool;

    /**
     * @return float
     */
    public function getMinQty(): float;

    /**
     * @return float
     */
    public function getMinSaleQty(): float;

    /**
     * @return float
     */
    public function getMaxSaleQty(): float;

    /**
     * @return bool
     */
    public function isUseConfigBackorders(): bool;

    /**
     * Retrieve backorders status
     *
     * @return int
     */
    public function getBackorders(): int;

    /**
     * @return float
     */
    public function getNotifyStockQty(): float;

    /**
     * Retrieve Quantity Increments data wrapper
     *
     * @return float
     */
    public function getQtyIncrements(): float;

    /**
     * @return bool
     */
    public function isEnableQtyIncrements(): bool;

    /**
     * @return bool
     */
    public function isManageStock(): bool;

    /**
     * @return string
     */
    public function getLowStockDate(): string;

    /**
     * @return bool
     */
    public function isDecimalDivided(): bool;

    /**
     * @return int
     */
    public function getStockStatusChangedAuto(): bool;

    /**
     * @return float
     */
    public function getStockThresholdQty(): float;
}

Programming Interface Command API

Command APIs should be represented as a set of Interfaces corresponding each particular command modifying or retrieving data for given the particular Configuration value.

Command Interfaces for Configuration option Specified on the level of SourceItem Option 1 - Granular Commands

Backorders are a good example of configuration option which supposed to be specified on the Source level (i.e. warehouse), and based on the Sources assigned to given Stock, the configuration value of StockItemConfiguration should be calculated. For example, there are next Sources:

  • Source A (backorders = 0)
  • Source B (backorders = 0)
  • Source C (backorders = 1, Allow Qty Below 0) All of these sources are assigned to Stock A Backorder Stock configuration should be calculated based on SourceItemConfiguration values. In this case calculated value supposed to be backorders = 1.
//Command API for Backorder to set Backorder and retrieve backorder on the level of SourceItem

/**
 * Specify Backorder configuration option on the SourceItem level
 * @api
 */
interface SetBackorderStatusInterface
{
    /**
     * @param string $sku
     * @param string $sourceCode Backorder is specified on the level of SourceItem and calculated on the level of Stock
     * @param int $backorderStatus if NULL is set that means fallback to Source configuration would be used
     * @return void
     */
    public function execute(string $sku, string $sourceCode, ?int $backorderStatus): void;
}

/**
 * Retrieve Backorder configuration option on the SourceItem level
 * @api
 */
interface GetBackorderStatusInterface
{
    /**
     * @param string $sku
     * @param string $sourceCode Backorder is specified on the level of SourceItem and calculated on the level of Stock
     * @return int  If backorderStatus is NULL fallback to Backorder Status for Source is used
     */
    public function execute(string $sku, string $sourceCode): int;
}

/**
 * Backorder is specified on the level of Source and applied for all SourceItems assigned to this Source
 * @api
 */
interface SetBackorderStatusForSourceInterface
{
    /**
     * @param string $sourceCode Backorder is specified on the level of Source and applied for all SourceItems assigned to this Source
     * @param int $backorderStatus if NULL is set that means fallback to Global configuration would be used
     * @return void
     */
    public function execute(string $sourceCode, ?int $backorderStatus): void;
}

/**
 * Get Backorder status for Source
 * @api
 */
interface GetBackorderStatusForSourceInterface
{
    /**
     * @param string $sourceCode Backorder is specified on the level of Source and applied for all SourceItems assigned to this Source
     * @return int If backorderStatus is NULL Fallback to Global level is used 
     */
    public function execute(string $sourceCode): int;
}


/**
 * Set Backorder configuration globally
 * @api
 */
interface SetBackorderStatusGlobalValueInterface
{
    /**
     * @param int $backorderStatus Backorder configuration applied globally
     * @return void
     */
    public function execute(int $backorderStatus): void;
}

/**
 * Get Backorder configuration globally
 * @api
 */
interface GetBackorderStatusGlobalValueInterface
{
    /**
     * @return int
     */
    public function execute(): int;
}

Pay attention to the interface of setter commands, if admin wants to reset value for particular option he set NULL value, that's why setter commands accept ?int. But getters return pure value int, as getters apply fallback to retrieve value from Source and Global scopes correspondingly.

Command Interfaces for Configuration option Specified on the level of SourceItem Option 2 - Commands Facade

/**
 * All APIs to retrieve Backorder Config Value on the level of SourceItem/Source/Globally 
 * @api
 */
interface GetBackorderStatusConfigurationValue
{
    /**
     * @param string $sku
     * @param string $sourceCode
     * @return ?int
     */
    public function forSourceItem(string $sku, string $sourceCode): ?int;

    /**
     * @param string $sourceCode
     * @return ?int
     */
    public function forSource(string $sourceCode): ?int;

    /**
     * @param string $sourceCode
     * @return int
     */
    public function forGlobal(): int;
}


/**
 * All APIs to specify Backorder Config Value on the level of SourceItem/Source/Globally
 * @api
 */
interface SetBackorderStatusConfigurationValue
{
    /**
     * @param string $sku
     * @param string $sourceCode Backorder
     * @param int $backorderStatus if NULL is set that means fallback to Source configuration would be used
     * @return void
     */
    public function forSourceItem(string $sku, string $sourceCode, ?int $backorderStatus): void;

    /**
     * @param string $sourceCode
     * @param int $backorderStatus if NULL is set that means fallback to Global configuration would be used
     * @return void
     */
    public function forSource(string $sourceCode, ?int $backorderStatus): void;

    /**
     * @param int $backorderStatus Backorder configuration applied globally
     * @return void
     */
    public function forGlobal(int $backorderStatus): void;
}

Command Interfaces for Configuration option Specified on the level of StockItem Option 1 - Granular Commands

Command to specify Safety Stock threshold which affects salability quantity.

/**
 * Set the safety stock value on the level of Product in Stock (StockItem)
 * @api
 */
interface SetSafetyQuantityInterface
{
    /**
     * @param string $sku
     * @param int $stockId
     * @param float $safetyQty if NULL is set, fallback to Stock configuration would be used
     * @return void
     */
    public function execute(string $sku, int $stockId, ?float $safetyQty): void;
}

/**
 * Get the safety stock value on the level of Product in Stock (StockItem)
 * @api
 */
interface GetSafetyQuantityInterface
{
    /**
     * @param string $sku
     * @param int $stockId
     * @return float
     */
    public function execute(string $sku, int $stockId): float;
}

/**
 * Set the safety stock value globally for all Products assigned to Stock
 * @api
 */
interface SetSafetyQuantityGlobalValueInterface
{
    /**
     * @param int $stockId
     * @param float $safetyQty
     * @return void
     */
    public function execute(int $stockId, float $safetyQty): void;
}

/**
 * Get the safety stock global value for all Products assigned to Stock by Default
 * @api
 */
interface GetSafetyQuantityGlobalValueInterface
{
    /**
     * @param int $stockId
     * @return float
     */
    public function execute(int $stockId): float;
}

Command Interfaces for Configuration option Specified on the level of StockItem Option 2 - Commands Facade

/**
 * Get the safety stock value on the level of StockItem/Stock
 * @api
 */
interface GetSafetyStockQuantityConfigurationValue
{
    /**
     * @param string $sku
     * @param int $stockId
     * @return ?float
     */
    public function forStockItem(string $sku, int $stockId): ?float;

    /**
     * @param int $stockId
     * @return float
     */
    public function forStock(int $stockId): float;
}


/**
 * Set the safety stock value on the level of StockItem/Stock
 * @api
 */
interface SetSafetyStockQuantityConfigurationValue
{
    /**
     * @param string $sku
     * @param int $stockId
     * @param float $safetyQty if NULL is set, fallback to Stock configuration would be used
     * @return void
     */
    public function forStockItem(string $sku, int $stockId, ?float $safetyQty): void;

    /**
     * @param int $stockId
     * @param float $safetyQty
     * @return void
     */
    public function forStock(int $stockId, float $safetyQty): void;
}

MSI Documentation:

  1. Technical Vision. Catalog Inventory
  2. Installation Guide
  3. List of Inventory APIs and their legacy analogs
  4. MSI Roadmap
  5. Known Issues in Order Lifecycle
  6. MSI User Guide
  7. DevDocs Documentation
  8. User Stories
  9. User Scenarios:
  10. Technical Designs:
  11. Admin UI
  12. MFTF Extension Tests
  13. Weekly MSI Demos
  14. Tutorials
Clone this wiki locally