The StockLevelService is responsible for managing the stock levels of ProductVariants.
Whenever you need to adjust the stockOnHand or for a ProductVariant,
you should use this service.
stockAllocated(ctx: RequestContext, productVariantId: ID, stockLocationId: ID) => Promise<StockLevel>Returns the StockLevel for the given ProductVariant and StockLocation.
(ctx: RequestContext, productVariantId: ID) => Promise<StockLevel[]>(ctx: RequestContext, productVariantId: ID, options?: StockLevelReadOptions) => Promise<AvailableStock>v3.7.4 - Added the `options` parameterReturns the available stock (on hand and allocated) for the given ProductVariant. This is determined by the configured StockLocationStrategy.
Pass { lockStockLevels: true } when the caller is about to allocate stock based on the
returned figures. This takes a write lock on the underlying StockLevel rows which is held
until the surrounding transaction commits, so that the check and the allocation cannot be
interleaved with another checkout for the same ProductVariant.
(ctx: RequestContext, productVariantId: ID, stockLocationId: ID, change: number) => Updates the stockOnHand for the given ProductVariant and StockLocation.
When creating a new StockLevel the initial value is the adjustment delta itself, which may be
negative (e.g. a backorder against a variant with a negative outOfStockThreshold), so the
row stays consistent with the StockAdjustment ledger.
(ctx: RequestContext, productVariantId: ID, stockLocationId: ID, change: number) => Updates the stockAllocated for the given ProductVariant and StockLocation.
stockAllocated is clamped at 0 so a release can never produce a negative value. A clamp
which fires is logged, since it means more was released than was ever allocated.
(ctx: RequestContext, productVariantIds: ID[], options?: StockLevelLockOptions) => Promise<void>v3.7.4Takes the write lock of StockLevelService.getLockedStockLevelsForVariant for each of
the given ProductVariants, in a fixed order: duplicates are dropped, the variants are visited
in ascending id order and each variant's rows in id order. A transaction which changes the
stock of more than one variant should call this once, before its first stock read or write.
Two such transactions then take their locks in the same order, so they cannot deadlock, and
later stock writes in the transaction meet rows it already holds.
Variants which do not track inventory are skipped by default: nothing is checked or counted
for them, so locking them would only serialize their checkouts. Pass
{ includeUntrackedVariants: true } when the transaction derives what it writes from the
current stockOnHand rather than from a saleable stock check.
Degrades like getLockedStockLevelsForVariant without a transaction or row lock support.
Returns every StockLevel of the given ProductVariant, whatever its
StockLocation or Channel, with a write lock (SELECT ... FOR UPDATE) held until
the surrounding transaction commits. Use it when the returned figures decide how much
stock to allocate and the allocation happens in the same transaction: a second
transaction for the same variant waits here until the first commits, then reads the
updated stockAllocated.
The rows are read by the locking statement itself. On MySQL and MariaDB a plain SELECT under REPEATABLE READ keeps returning the transaction's opening snapshot, while a locking read sees the latest committed row.
Rows are locked in id order. When locking several variants in one transaction, visit them in a fixed order too, so two transactions cannot take the same locks in opposite order.
Without an open transaction, or on a driver without row locks (SQLite, SQL.js), this falls back to an unlocked read and logs a warning once per process.
Architecture reviews, custom plugin work, migrations, ongoing support. Get a hand from the team that builds Vendure.
Talk to the team