Hide menu Last updated: Oct 12 2015

Inventory requests

This topic describes the transactional requests providing secure read-modify-write interaction with the inventory system. In an environment with concurrency, inventory quantities can 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 topic are executed with InventoryResponse IInventory.Request(InventoryRequest request). Note that this topic describes typical provider behavior but behavior may vary between providers.

How it works

The inventory system ignores any ambient transactions active in the calling system, to prevent concurrency performance issues. 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.

Note: 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. You may cancel all operations except Cancel, Complete, and Split.

Inventory status and low stock report

Inventory status is Enabled or Disabled, indicating whether the inventory is being tracked or not.

  • Enabled. When the inventory status is enabled, the inventory number of physical goods is tracked, limiting the amount that can be sold. Stock is required to be available to process orders, and available counts are decremented as orders are processed.
  • Disabled. When the inventory status is disabled, there are 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 is generated only when the inventory status is set to Enabled.

Note: 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 later cancels, completes, or splits 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 determines which provider created the operation key, so that old data is not 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 includes one or more request items. Every item in a request must succeed for the request to succeed as a whole. Failed requests do not change inventory data.

Purchase

Request items with Purchase request type 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 Preorder request type 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 are 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

When you create 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 include 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.

PurchaseOrPreorder

The PurchaseOrPreorder request type lets a caller request a purchase or a preorder without knowing the preorder and purchase availability date, which 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 proceeds as if the request type is Preorder. If the request date is on or after the purchase available date, then the request proceeds as if the request type is Purchase. Otherwise, the request will fail.

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

Complete

Request items with a Complete request type indicate that an existing purchase or preorder was 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 Complete type require that InventoryRequestItem.OperationKey properties are set to values that identify the operation being completed. The ItemIndexWarehouseCode, and Quantity values in the request item are ignored.

While Complete requests typically successful, calling code should still check for errors. A likely customization of the inventory provider may be to treat Purchase requests as time-limited reservations of an item, and the Complete operation may fail if the reservation expires and another customer purchases 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 Cancel request type indicate that an existing purchase, preorder, or backorder was cancelled; and any allocated or reserved stock should be made available.

Requests with Cancel type require that InventoryRequestItem.OperationKey properties are set to values that identify the operation being completed. The ItemIndex, WarehouseCode, and Quantity values in the request item are ignored.

A cancellation returns an item stock to availabliltiy and can be made available to any other request items in the same request. For example, a customer tries to purchase 10 items but only 9 items are available, you should be able to cancel the original purchase of 10 items and request the purchase of 9 items that are available. The order of the items in a request must not affect the result of the operation.

You cannot use a request cancellation for returns, because an order that can be returned was already 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 goes 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 Split request type indicate that an existing purchase, preorder, or backorder should be split into two separate items for further processing.

Requests with Split type require InventoryRequestItem.OperationKey properties are set to values that identify the operation being completed. The ItemIndex and WarehouseCode values in the request item are ignored. The Quantity determines the quantity of items that are included in the first part of the split, and the second part contains 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 include two response items with ItemIndex values matching the split request item. The ResponseTypeInfo property of the response items are set to SplitFirst or SplitSecond to indicate which part of the split the response item describes. Use this value 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.

You cannot 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, you should 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 is more efficient and reliable to send single request resembling the following than to send two separate requests.

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

You can replace splits 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 is subject to additional validation since, but you can 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 return two separate operation keys that you can use 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. You cannot access multiple applications 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 and the InventoryResponseType Enumeration.

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 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 properties 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 because it usually is 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 wants 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 2nd 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 2nd 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 you cannot guarantee that the initial reservation will be still available. This implementation fails if another customer creates a reservation for the last available room on the 2nd between steps 1 and 3.

To make the second implementation work, 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 succeeds or fails 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.

Comments