Try our conversational search powered by Generative AI!

Loading...
ARCHIVED This content is retired and no longer maintained. See the latest version here.

Recommended reading 

Inventory requests

Introduction

This document describes the transactional requests providing secure read-modify-write interaction with the inventory system. In an environment with concurrency, inventory quantities can easily be corrupted; and inventory allocations are difficult or impossible to undo if an order is cancelled, deleted, or otherwise undone.

All requests described in this document are executed with InventoryResponse IInventory.Request(InventoryRequest request). Note that this document describes "typical" provider behavior. However, behavior may vary between providers.

How it works

The inventory system ignores any ambient transactions active in the calling system, to prevent performance issues from concurrency. The inventory system has the highest potential for performance impact from concurrency in most installations (with only promotion usage recording potentially coming close or equaling it). Each request is intended to complete immediately. It is important to note that, if some error occurs after an inventory request, the result of any successful request should be cancelled with another request. For this reason, the cancel operation should be very robust. All operations except Cancel, Complete, and Split may be cancelled.

Inventory status and low stock report [New in 8.7]

Inventory status can be either "Enabled" or "Disabled", indicating whether the inventory is being tracked or not. When the inventory status is enabled, the inventory number of physical goods will be tracked, limiting the amount that can be sold. Stock will be required to be available to process orders, and available counts will be decremented as orders are processed. When the inventory status is disabled, there will be no limitations to the amount that can be sold (for example digital goods). The requested quantities are updated, but the available counts do not change. A low stock report will be generated only when the inventory status is set to "Enabled".

Note that TrackInventory on the Pricing tab determines if the entry is tracked or not (in all warehouse inventories), whereas InventoryStatus here determines if a warehouse inventory is tracked or not.

Operation keys

All operations except Cancel and Complete are expected to return operation keys when they are successful. The operation key is an opaque string that can be used to later cancel, complete, or split the original request. Implementations may either encode the necessary data to perform the cancel, complete, or split requests directly in the operation key string; or, they may store records for each operation, and return key information for the data in the operation key. In any case, the operation key is meant to be completely opaque to callers.

The provider key is used to determine which provider created the operation key, so that old data will not be misinterpreted after a provider is changed or updated. Providers may accept operation keys from other known providers when possible (for example, if a new version of a provider uses a different operation key, but can still decode and process the old version's operation keys).

Request types

Each request may include one or more request item. Every item in a request must succeed for the request to succeed as a whole. No changes are made to inventory data by failed requests.

Purchase

Request items with request type Purchase attempt to make a purchase.

To succeed, the request date must be on or after the purchase available date, and the quantity must be less than or equal to the purchase available quantity at the fulfilling warehouse.

Preorder

Request items with request type Preorder attempt to reserve some quantity for purchase on a future date. Unlike backorders, the date when an item becomes available after preorder is a manufacturer's release date or some similar data, and is not determined by available stock levels. Also unlike backorders, preorders are promises to purchase, and are likely to require a payment or promise of future payment. 

The requested quantity must be less than or equal to the preorder available quantity at the fulfilling warehouse. Requested preorders will be decremented from both the preorder available quantity and the purchase available quantity, possibly causing the purchase available quantity to become negative.

Searching for preorder entries [New in 8.7]

When creating a pre-order, the start date (available form) must be in the future and the pre-order available date must be prior to the order date. This could lead to a problem where a product allowed for preordering might not appear in search result, because the current approach will not return products available in the future (where start date is in the future).

Therefore the CatalogEntrySearchCriteria now has an IncludePreorderEntry property.

When searching using this property (default set to true), the search results will include all variations that are valid or with "allow preorder" set.
If the property is set to false, the search results will include only variations that are valid, and no preorder variations.

Example: variations search without preorder variations

SearchFilterHelper helper = SearchFilterHelper.Current;
CatalogEntrySearchCriteria criteria = helper.CreateSearchCriteria("", sortObject);
criteria.RecordsToRetrieve = 25;
criteria.StartingRecord = _startRowIndex;
criteria.IncludePreorderEntry = false;

var searchManager = new SearchManager(AppContext.Current.ApplicationName);
var results = searchManager.Search(criteria);

Backorder

Request items with request type Backorder indicate interest in purchasing an out-of-stock item once stock is available.

Unlike preorders, backorders are not promises to purchase, do not require any payment promise, and require another interaction with the buyer to convert into a purchase.

To succeed, the request date must be on or after the preorder available date. The backorder available quantity must be greater than zero, but may be less than the requested quantity.

Purchase or preorder

The request type PurchaseOrPreorder allows a caller to request either a purchase or a preorder without knowing the preorder and purchase availability date. This can free the caller from making an extra initial request to determine availability dates.

If the request date of a PurchaseOrPreorder request is on or after the preorder available date and before the purchase availability date, then the request will proceed as if the request type is Preorder. If the request date is on or after the purchase available date, then the request will proceed as if the request type is Purchase. Otherwise, the request will fail.

Successful requests of type PurchaseOrPreorder will set the ResponseTypeInfo property of the response item to Purchase or Preorder to indicate how the request was satisfied.

Complete

Request items with a request type of Complete indicate that an existing purchase or preorder has been fulfilled, or that a backorder is no longer valid (either cancelled or converted to purchase, Complete and Cancel are synonyms when applied to backorders).

Requests with type Complete require that InventoryRequestItem.OperationKey properties are set to values that identify the operation being completed. The WarehouseCode and Quantity values in the request item are ignored. The order of ItemCode is not important.

While Complete requests will typically always be successful, calling code should still check for errors. A likely customization of the inventory provider may be to treat Purchaserequests as time-limited reservations of an item, and the Complete operation may fail if the reservation has expired and another customer has purchased the item (for example, tickets to events).

In the default implementation, Complete operations simply remove the requested quantity additions that were made by the original operation.

Cancel

Request items with a request type of Cancel indicate that an existing purchase, preorder, or backorder has been cancelled; and any allocated or reserved stock should be made available.

Requests with type Cancel require that InventoryRequestItem.OperationKey properties are set to values that identify the operation being completed. The WarehouseCode and Quantity values in the request item are ignored. The order of ItemCode is not important.

Item stock returned to availability by a cancellation request must be made available to any other request items in the same request. If a purchase of 10 items has been made and not completed, and no more of that item are available, then a request that cancels the original purchase and also requests a purchase of 9 of that item should succeed. The order of the items in a request must not affect the result of the operation.

Request cancellation cannot be used for returns, both because an order that can be returned should already have been completed, and because returned physical items typically need to go through an external inspection process before being made available for purchase. The update making the item available again will go through a stock update call, not an inventory request.

In the default implementation, Cancel operations simply undo the quantity changes that were applied by the original operation.

Split

Request items with a request type of Split indicate that an existing purchase, preorder, or backorder should be split into two separate items for further processing.

Requests with type Split require InventoryRequestItem.OperationKey properties are set to values that identify the operation being completed. The ItemCode and WarehouseCode values in the request item are ignored. The Quantity will determine the quantity of items that will be included in the 'first' part of the split, and the 'second' part will contain the difference between the original quantity and the Quantity value in the request. If the Quantity in the request is greater than or equal to the original quantity, then the request will fail.

Successful requests of type Split will include two response items with ItemIndex values matching the split request item. The ResponseTypeInfo property of the response items will be set to SplitFirst or SplitSecond to indicate which part of the split the response item describes. This value should be used to distinguish the reponse items from each other; using the quantity to distinguish the items may cause errors when a request is split exactly in half.

Split is the only operation that may return more response items than were included in the request, or result in response items with duplicate ItemIndex values.

It is not possible to perform further Cancel, Complete, or Split operations on the results of a split in the same requests that causes the split. In situations where further processing is necessary and known at the time of the original request, it may be more efficient to send a request with both a cancel and another operation in a single request, rather than split and then send a second request.

For example, if a customer has purchased 10 of an item, but the purchase quantity needs to be changed to 8, it will be more efficient and reliable to send single request resembling the following than it will be to send two separate requests.

json { "ApplicationId": "...", "RequestDateUtc": "...", "Items": 
[ { "ItemIndex": 1, "RequestType":
"Cancel", "OperationKey": "..." }, { "ItemIndex": 2, "RequestType": "Purchase", "CatalogEntryCode":
"item", "WarehouseCode": "warehouse", "Quantity": 8 } ] }

Splits themselves can be replaced by requests with a cancel item and one or more operations to replace the original item. This requires more knowledge about the original operation and will be subject to additional validation since, but can be used to accomplish splits into more than two parts. In particular, callers may need to use the original request date (instead of the current date) to split preorders if the preorder period has ended. The following example splits a purchase of 3 items into 3 individual items, without using Split.

json { "ApplicationId": "...", "RequestDateUtc": "...", "Items":
[ { "ItemIndex": 1, "RequestType": "Cancel", "OperationKey": "..." }, { "ItemIndex": 2, "RequestType":
"Purchase", "CatlaogEntryCode": "item", "WarehouseCode": "warehouse", "Quantity": 1 }, { "ItemIndex":
3, "RequestType": "Purchase", "CatalogEntryCode": "item", "WarehouseCode": "warehouse", "Quantity":
1 }, { "ItemIndex": 4, "RequestType": "Purchase", "CatalogEntryCode": "item", "WarehouseCode": "warehouse", "Quantity": 1 } ] }

In the default implementation, Split operations do not change any inventory counts, but will return two separate operation keys that can be used to cancel, complete, or split only part of the original operation.

Request data

A request to the inventory system is represented by the InventoryRequest and InventoryRequestItem classes.

InventoryRequest class

ApplicationId: An application ID for the request. Multiple applications may not be accessed by a single request.

RequestDateUtc: The date and time of the request, in UTC.

Items: A collection of InventoryRequestItems. Expected to contain at least one element.

Context: Used to pass additional data to customized providers.

InventoryRequestItem class

ItemIndex: An integer identifying the item. This number must be unique for each item within a request. This is used only to correlate the items in a request with the items in a response.

RequestType: The requested operation; one of Purchase, Preorder, Backorder, PurchaseOrPreorder, Complete, Cancel, Split, or Custom. Requests of type Custom are for extensibility and will likely require a Context value to be interpreted by the provider.

CatalogEntryCode: An identifier for the catalog entry being requested. Ignored for Complete, Cancel, and Split operations.

WarehouseCode: An optional identifier for a warehouse to request items from. If null or empty, the inventory provider is expected to assign a warehouse.

Quantity: The requested quantity. This may be an integer or decimal value, and must be greater than zero.

OperationKey: An opaque value, stored as a string, identifying a previous request to the inventory system. Only required for Complete, Cancel, and Split operations.

Context: Used to pass additional data to customized providers.

Response data

A response from the inventory system is represented by the InventoryResponse and InventoryResponseItem classes.

InventoryResponse class

IsSuccess: True if the request succeeded, false if it failed.

ApplicationId: The application ID specified in the request.

RequestDateUtc: The request date specified in the request.

Items: A collection on InventoryResponseItems describing the results of the operation. item.

Context: Used to pass additional data back to the caller from custom providers.

InventoryResponseItem class

RequestItem: Repeats the InventoryRequestItem that resulted in this request. All values will be the same as in the request.

ResponseType: The result of the individual operation, described below.

ResponseTypeInfo: A modifier tha tmay contain additional information about successful operations.

WarehouseCode: An identifier for the warehouse satisfying the operation, when applicable.

OperationKey: An identifier to reference this operation.

IsTracked, PurchaseAvailableQuantity, PreorderAvailableQuantity, BackorderAvailableQuantity, PurchaseRequestedQuantity, PreorderRequestedQuantity, BackorderRequestedQuantity, PreorderAvailableUtc, PurchaseAvailableUtc: The values of the affected inventory record, after this request has completed and before any other changes are made.

InventoryResponseType enumeration

Success: The entire request succeeded.

OtherItemFailed: This item did not cause any errors, but another item in the request failed.

InvalidRequest: The request item contained invalid data.

NotSupported: The inventory provider does not support this operation.

ItemNotFound: The inventory provider does not recognize the requested item.

WarehouseNotFound: A warehouse was specified, and the inventory provider does not recognize it.

NotEnough: There is not enough quantity to fulfill the request.

NotAvailableOnDate: The request cannot be fulfilled on the specified request date.

AmbiguousWarehouse: A warehouse was not specified, and the inventory provider does not have enough information to choose one.

Design decisions

InventoryResponseTypeInfo

Additional data is useful for the PurchaseOrPreorder operation to determine if a successful result produced a purchase or a preorder, and for the Split operation to determine if a successful result indicates the requested quantity or the remaining quantity.

The InventoryResponseTypeInfo enumeration was added to distinguish these situations. Additional values could have been added to InventoryResponseType to represent these special-case successes, but they are special cases. Life is easier when x.ResponseType == InventoryResponseType.Success does what it says, instead of checking for multiple values or knowing to use an extension method.

The extra results for PurchaseOrPreorder are frequently redundant. Following the expected logic, the response contains all the necessary information to check if a request was in the preorder or purchase period, and the caller can make the necessary assumptions. However, that moves logic out of the provider and into the caller, and things are a bit safer and simpler if no logic besides a simple equals is involved.

The extra results for Split are trickier. If a customer splits an operation with quantity 3 into quantities of 1 and 2, then the caller can tell the reponse items apart by the quantities. If a customer splits an operation exactly in half, then the quantities will not be distinct. The danger is that it is easy to write code that will fetch the first item as the item matching the requested quantity, and then fetch the first item matching (original - requested) as the second item. These will both return the same item, and seemingly benign code can both duplicate and lose an operation key. Furthermore, this may matter to some providers and not to others -- making bugs even more difficult to track down.

Rationale for single-method antipattern

All requests are executed via a single method, with the operations determined by argument data. This is typically an anti-pattern, and different behaviors should have different methods. This is compounded by having some proprties of the request object ignored depending on request type, which makes working with the system less intuitive. This pattern is used with the inventory system since it will usually be desirable to send multiple requests in a single message and have them all succeed or fail in a single operation.

If each inventory request operation was implemented as a separate method, then operations where multiple individual requests must all succeed for the overall business operation to succeed would become much more complex. Consider a system tracking hotel rooms as inventory; each room and night is either available or occupied. If a customer wishes to change an existing reservation to start and end a day later, and the operations are all individual methods, then the operation would be difficult to implement safely.

The implementation could be done as:

1. Create reservation from the 2nd until the 5th.
2. Cancel reservation from the 1st until the 4th.

If the customer had the last room available on the 2nd, then the creation of the new reservation in the first step would fail; even though someone looking through a book of reservations and changing it manually would clearly succeed. This implementation does not work as expected.

We could also try:

1. Cancel reservation from the 1nd until the 4th.
2. Create reservation from the 2nd until the 5th.
3. If the creation of the new reservation fails, re-create the initial reservation.

If a room is not available on the 5th, then the creation of the new reservation will also fail; and we cannot guarantee that the initial reservation will still be available. This implementation will fail if another customer creates a reservation for the last available room on the 1st between steps 1 and 3.

To make the second implementation work, we need to wrap the three operations in a transaction so that no other customers can create reservations until the entire operation is complete. However, the inventory system is usually the subsystem with the most concurrency in a high-volume site, and this can have severe performance implications.

The most frequently changing data for most commerce sites is order data, and the only shared resource when processing orders is typically the inventory. Catalog data does not change on a per-order bases; order, customer and payment data are all typically isolated to the individual order or customer and can be isolated from concurrency; but inventory data is largely shared and requires transactional security to be accurate. If a system external to the backing inventory system (even if that "system" is just a set of business logic processing the order, using the same servers and database), then allowing the external system to hold a transaction can cripple scalability.

If the scalability were not an issue, exposing multiple operations to the order processing logic as individual methods would means that order processing logic becomes responsible for handling the order of operations and conflict resolution. The inventory system should be responsible for these functions.

The single-method implementation was chosen so that this example sends only one request, which will either succeed or fail as a whole. This leaves it up to the inventory system to determine the interaction between the individual parts of the overall operation, increasing the extensibility of the provider and reducing the reponsibility of the caller.

Do you find this information helpful? Please log in to provide feedback.

Last updated: Dec 02, 2014

Recommended reading