Stock Item Service
Overview
Stock Items represent the current stock state of a product in a specific warehouse.
Each Stock Item defines:
- quantity physically available (
QuantityOnHand) - quantity reserved/allocated (
QuantityAllocated) - replenishment threshold (
ReorderLevel)
Stock items are unique per (ProductId, WarehouseId) pair and are used by Inventory
to power availability checks, allocation logic and replenishment planning.
All stock item operations are tenant-aware and fully audited.
Endpoints
GET
/api/v1/StockItems/product/{productId}
Returns all stock items for a given product across warehouses.
Behavior:
- Filters by
ProductId - Returns a list of
StockItemModel - Returns
200with an empty list if the product has no stock items
(service returns empty list; controller currently returns200as long as the list is not null)
Response:
List<StockItemModel>
Authorization:
- Requires Bearer Token
GET
/api/v1/StockItems/warehouse/{warehouseId}
Returns all stock items stored in a specific warehouse.
Behavior:
- Filters by
WarehouseId - Supports large datasets via internal batching:
- if total count exceeds
LARGE_DATASET_THRESHOLD, the service reads in batches ofBATCH_SIZE
- if total count exceeds
- Returns a list of
StockItemModel - Returns
200with an empty list if no stock items exist in the warehouse
(service returns empty list; controller currently returns200as long as the list is not null)
Response:
List<StockItemModel>
Authorization:
- Requires Bearer Token
POST
/api/v1/StockItems
Creates a new stock item (product stock in a warehouse).
Request body (StockItemModel):
WarehouseId(long?, required, must be > 0)ProductId(long, required, must be > 0)QuantityOnHand(decimal?, required, must be >= 0)QuantityAllocated(decimal?, required, must be >= 0)ReorderLevel(decimal?, required, must be >= 0)
Behavior:
- Validation is enforced in the service layer (
IsReadyToAdd()) - Prevents duplicates by enforcing uniqueness of
(ProductId, WarehouseId) - Successful creation writes an audit log entry
Errors:
- Returns
400if:- request body is invalid (
IsReadyToAdd()is false) - stock item already exists for
(ProductId, WarehouseId) - database insert - or other internal error occurs
- request body is invalid (
Authorization:
- Requires Bearer Token
PUT
/api/v1/StockItems/product/{productId}
Updates an existing stock item for a given product and warehouse.
Route parameters:
productId(long, required)
Request body (StockItemModel):
WarehouseId(long?, required, must be > 0)QuantityOnHand(decimal?, optional)QuantityAllocated(decimal?, optional)ReorderLevel(decimal?, optional)
Behavior:
- The controller sets
ProductIdfrom the route - Validation is enforced (
IsReadyToUpdate()requiresProductId > 0andWarehouseId > 0) - Looks up stock item by
(ProductId, WarehouseId) - Updates only fields provided in body:
QuantityOnHandupdated if not nullQuantityAllocatedupdated if not nullReorderLevelupdated if not null
- Successful update writes an audit log entry
Errors:
- Returns
400if request body is invalid (IsReadyToUpdate()is false) - Returns
404if no stock item exists for(ProductId, WarehouseId)
Authorization:
- Requires Bearer Token
POST
/api/v1/StockItems/variant/{productVariantId}/transfer
Transfers a quantity of a product variant's stock from one warehouse to another.
Route parameters:
productVariantId(long, required)
Request body (StockTransferRequestModel):
FromWarehouseId(long, required, must be > 0)ToWarehouseId(long, required, must be > 0, must differ fromFromWarehouseId)Quantity(decimal, required, must be > 0)ReasonCode(string?, optional — defaults to"Transfer")
Behavior:
- Locks the source and destination
(ProductVariantId, WarehouseId)stock items (in a fixed order to avoid deadlocks) so concurrent transfers can't race - Validates the source warehouse has enough
QuantityOnHandto cover the transfer - Creates the destination
StockItemautomatically if one doesn't already exist for this warehouse - Writes two linked
StockTransactionrows (a negativeQtyChangeat the source, a positive one at the destination) sharing the sameRefIdandRefType = "Transfer", saved atomically in a single database write - Successful transfer writes an audit log entry
Response:
StockTransferResultModel—{ from: StockItemModel, to: StockItemModel }, the updated stock items at both warehouses
Errors:
- Returns
422if the request body is invalid (FromWarehouseId/ToWarehouseIdmissing orQuantity <= 0) - Returns
400ifFromWarehouseIdequalsToWarehouseId - Returns
404if no stock item exists for the source(ProductVariantId, FromWarehouseId) - Returns
409if the source warehouse doesn't have enoughQuantityOnHand, or if a concurrent update conflict couldn't be resolved after retrying
Authorization:
- Requires Bearer Token
DELETE
/api/v1/StockItems/product/{productId}/warehouse/{warehouseId}
Deletes a stock item by (ProductId, WarehouseId).
Route parameters:
productId(long, required)warehouseId(long, required)
Behavior:
- Performs a hard delete
- Successful deletion writes an audit log entry
Errors:
- Returns
404if the stock item does not exist
Authorization:
- Requires Bearer Token
Notes
- Validation is enforced in the service layer, not via model attributes
(ProductId, WarehouseId)is treated as a unique pair (duplicates are blocked)- Large warehouse reads are batched when dataset exceeds
LARGE_DATASET_THRESHOLD - All state-changing operations are audited
- Internal errors are logged and not exposed to API consumers