Developer Resourcesinventory
Inventory Module API & Integration Guide
Inventory API contracts for admin operations and order-driven stock lifecycle integration.
Audience: Admin frontend developers, backend integrators, and QA engineers
Scope: Admin inventory routes, DTO contracts, response envelopes, and queue/outbox integration points
Inventory Module - API & Integration Guide
1. How to Read / Quick Metadata
- Module:
Inventory - Auth model:
JwtAuthGuard + RoleGuard + @Permissions(...) - Primary base URL:
/api/admin/inventory - Response envelope: successful endpoints return
ResponseDto<T> - Swagger tag used by controller:
Inventory (Admin) - Route owner:
InventoryAdminController
2. High-Level Overview
Inventory API is split into:
- synchronous admin reads (
list,low-stock,product inventory detail) - synchronous admin mutations (
adjust,bulk-sync) - asynchronous worker-side lifecycle handlers (reserve/finalize/release) triggered by order outbox events
Inventory ownership model:
- Inventory owns: stock numbers (
total,reserved,available) and stock mutation rules - Order owns: business event timing (
create,payment success,cancel/failure/expire) and outbox emission - BullMQ owns: async execution and retry delivery
3. Core Concepts and Terminology
- inventory row: one
product_inventoryrow per product (UNIQUE(product_id)) - reserve: move quantity from available to reserved
- finalize: permanently deduct sold quantity from total + reserved
- release: return reserved quantity back to available
- adjust: manual admin correction (positive/negative delta)
- sync: reconciliation operation that re-derives
available = total - reserved - dedupeKey: deterministic idempotency fingerprint carried by outbox payloads
- low stock:
availableQuantity <= lowStockThreshold
4. Route Summary
| Method | Path | Permission |
|---|---|---|
GET | /api/admin/inventory | Inventory_READ |
GET | /api/admin/inventory/low-stock | Inventory_READ |
GET | /api/admin/inventory/:productId | Inventory_READ |
POST | /api/admin/inventory/:productId/adjust | Inventory_UPDATE |
POST | /api/admin/inventory/bulk-sync | Inventory_UPDATE |
5. Route Details
5.1 List inventory
| Aspect | Details |
|---|---|
| Endpoint | GET /api/admin/inventory |
| Auth | JwtAuthGuard + RoleGuard |
| Permission | Inventory_READ |
| Query DTO | InventoryListQueryDto |
| Response | ResponseDto<InventoryResponseDto[]> |
| Pagination | QueryDto driven (pagination, page, size) |
Behavior notes:
- ordered by
updatedAt DESC - supports optional
lowStockOnly=true+ optionalthreshold - returns count/page/size metadata only when
pagination=true
5.2 List low-stock inventory
| Aspect | Details |
|---|---|
| Endpoint | GET /api/admin/inventory/low-stock |
| Auth | JwtAuthGuard + RoleGuard |
| Permission | Inventory_READ |
| Query | threshold?: number |
| Response | ResponseDto<InventoryResponseDto[]> |
Behavior notes:
- low-stock predicate delegates to shared service:
trackInventory=true AND available <= threshold - default threshold inside service is
5when not provided
5.3 Get inventory by product ID
| Aspect | Details |
|---|---|
| Endpoint | GET /api/admin/inventory/:productId |
| Auth | JwtAuthGuard + RoleGuard |
| Permission | Inventory_READ |
| Param | productId (int) |
| Response | ResponseDto<InventoryResponseDto | null> |
Behavior notes:
- returns
"Inventory not found"withdata: nullenvelope when row is missing
5.4 Adjust stock
| Aspect | Details |
|---|---|
| Endpoint | POST /api/admin/inventory/:productId/adjust |
| Auth | JwtAuthGuard + RoleGuard |
| Permission | Inventory_UPDATE |
| Param | productId (int) |
| Body DTO | StockAdjustDto |
| Response | ResponseDto<InventoryResponseDto> |
Behavior notes:
- positive
adjustmentincreases stock - negative
adjustmentdecreases stock - if product has no inventory row, service auto-creates row with normalized values
- rejects when adjustment would produce negative quantities
5.5 Bulk sync inventory
| Aspect | Details |
|---|---|
| Endpoint | POST /api/admin/inventory/bulk-sync |
| Auth | JwtAuthGuard + RoleGuard |
| Permission | Inventory_UPDATE |
| Body DTO | BulkSyncDto |
| Response | ResponseDto<InventoryResponseDto[]> |
Behavior notes:
- current controller accepts
trigger: stringbut runtime logic ignores trigger semantics - reconciles all inventory rows where
available != (total - reserved)
6. Query Parameters
6.1 InventoryListQueryDto (extends QueryDto)
| Param | Type | Default | Notes |
|---|---|---|---|
pagination | boolean | true | if false, no pagination metadata |
page | number | 1 | minimum 1 |
size | number | 20 | minimum 1 |
sort | string | updatedAt | currently normalized upstream, list query uses updatedAt DESC |
order | asc/desc | desc | same as above |
search | string | optional | currently not applied in inventory query |
lowStockOnly | boolean | optional | transforms from string ("true") |
threshold | number | optional | min 1 |
7. Request DTO Contracts
7.1 StockAdjustDto
| Field | Type | Required | Validation |
|---|---|---|---|
adjustment | number | yes | @IsInt() |
reason | string | yes | @IsString(), @MinLength(3) |
actorId | string | no | defaults to "admin" in admin service |
7.2 BulkSyncDto
| Field | Type | Required | Validation |
|---|---|---|---|
trigger | string | yes | @IsString() |
8. Response DTO Contracts
8.1 InventoryResponseDto
| Field | Type | Notes |
|---|---|---|
id | number | inventory row ID |
productId | number | linked product |
totalQuantity | number | tracked total units |
reservedQuantity | number | units reserved by pending lifecycle |
availableQuantity | number | units currently sellable |
lowStockThreshold | number | low-stock alert threshold |
trackInventory | boolean | inventory tracking toggle |
allowOversell | boolean | oversell policy flag |
isLowStock | boolean | computed: available <= threshold |
createdAt | date | creation timestamp |
updatedAt | date | last mutation timestamp |
9. Payload Examples
9.1 Adjust stock request
{
"adjustment": 5,
"reason": "restock",
"actorId": "admin-user-1"
}9.2 Single inventory response item
{
"id": 11,
"productId": 101,
"totalQuantity": 25,
"reservedQuantity": 3,
"availableQuantity": 22,
"lowStockThreshold": 5,
"trackInventory": true,
"allowOversell": false,
"isLowStock": false,
"createdAt": "2026-04-16T02:05:44.000Z",
"updatedAt": "2026-04-16T02:15:44.000Z"
}9.3 Paginated list response
{
"message": "Inventory fetched",
"data": [
{
"id": 11,
"productId": 101,
"totalQuantity": 25,
"reservedQuantity": 3,
"availableQuantity": 22,
"lowStockThreshold": 5,
"trackInventory": true,
"allowOversell": false,
"isLowStock": false,
"createdAt": "2026-04-16T02:05:44.000Z",
"updatedAt": "2026-04-16T02:15:44.000Z"
}
],
"count": 1,
"currentPage": 1,
"totalPage": 1,
"nextCursor": null
}9.4 Bulk sync response
{
"message": "2 inventory records synced",
"data": [
{
"id": 11,
"productId": 101,
"totalQuantity": 25,
"reservedQuantity": 3,
"availableQuantity": 22,
"lowStockThreshold": 5,
"trackInventory": true,
"allowOversell": false,
"isLowStock": false,
"createdAt": "2026-04-16T02:05:44.000Z",
"updatedAt": "2026-04-16T02:15:44.000Z"
},
{
"id": 12,
"productId": 102,
"totalQuantity": 10,
"reservedQuantity": 0,
"availableQuantity": 10,
"lowStockThreshold": 5,
"trackInventory": true,
"allowOversell": false,
"isLowStock": false,
"createdAt": "2026-04-16T02:07:44.000Z",
"updatedAt": "2026-04-16T02:15:44.000Z"
}
]
}10. Queue/Outbox Integration Contracts
Inventory jobs are consumed by inventory processors using @nomor/jobs contracts.
10.1 Job name + queue mapping
| Job | Queue | Payload |
|---|---|---|
stock.reserve | orders_reservations | StockReservePayload |
stock.finalize | orders_stock_finalize | StockFinalizePayload |
stock.release | orders_stock_release | StockReleasePayload |
stock.adjust | inventory | StockAdjustPayload |
stock.sync | inventory | StockSyncPayload |
10.2 Payload contract summary
| Payload | Required fields | Optional fields |
|---|---|---|
StockReservePayload | orderId, productId, quantity, dedupeKey | requestId, correlationId |
StockFinalizePayload | orderId, productId, quantity, dedupeKey | paymentReference, requestId, correlationId |
StockReleasePayload | orderId, productId, quantity, reason, dedupeKey | requestId, correlationId |
StockAdjustPayload | productId, adjustment, reason, actorId, dedupeKey | requestId, correlationId |
StockSyncPayload | none required (productId optional) | productId, requestId, correlationId |
10.3 Outbox event emission points (order-owned)
| Order lifecycle point | Outbox event | Queue target |
|---|---|---|
order created (createFromCart, createBuyNow) | stock.reserve | orders_reservations |
| payment success | stock.finalize | orders_stock_finalize |
| cancel/payment_failed/expired | stock.release | orders_stock_release |
11. Error Handling
Representative API/domain errors for inventory flows:
| HTTP | errorCode | Message |
|---|---|---|
| 400 | INVENTORY_INSUFFICIENT_STOCK | Insufficient stock. |
| 400 | INVENTORY_INVALID_OPERATION | Invalid stock operation. |
| 400 | INVENTORY_INVALID_ADJUSTMENT | Adjustment would result in negative stock. |
| 404 | INVENTORY_NOT_FOUND | Inventory not found. |
12. Integration Sequence Diagrams
12.1 Admin adjustment flow
12.2 Order reserve flow
12.3 Payment success finalize flow
12.4 Cancel/failure release flow
13. Endpoint Reference Cheatsheet
| Endpoint | Purpose | Notes |
|---|---|---|
GET /api/admin/inventory | full inventory list | supports pagination and optional low-stock filtering |
GET /api/admin/inventory/low-stock | low-stock list | threshold override via query |
GET /api/admin/inventory/:productId | product-level inventory detail | returns data: null when missing |
POST /api/admin/inventory/:productId/adjust | manual stock mutation | validates non-negative outcomes |
POST /api/admin/inventory/bulk-sync | reconciliation | current body field trigger is informational |
14. Current Known Gaps (API-Level)
orders_stockqueue is registered/owned by dispatcher but currently has no processor consumer.- Inventory cache TTL env keys are defined but not yet applied by inventory service.
- Mongo
AuditLogmutation writes for inventory are planned in docs/plans but not implemented in current inventory service. INVENTORY_ALREADY_FINALIZEDandINVENTORY_ALREADY_RELEASEDconstants exist but are not currently emitted by service logic.
15. QA / Release Checklist
- Admin routes enforce
Inventory_READ/Inventory_UPDATEcorrectly. - List endpoint pagination metadata matches
ResponseDtocontract. - Reserve/finalize/release jobs reach expected queues and processors.
- Stock adjustment rejects negative resulting quantities.
- Bulk sync reconciles inconsistent rows (
available != total - reserved). - Order cancellation and payment-failure flows emit
stock.releaseoutbox events. - Payment-success flow emits
stock.finalizeoutbox events. - No stale queue name mismatch between docs and
QueueNameenum.
16. File Map
apps/api/src/modules/inventory/admin/inventory-admin.controller.tsapps/api/src/modules/inventory/admin/inventory-admin.service.tsapps/api/src/modules/inventory/admin/dto/*apps/api/src/modules/inventory/shared/inventory.service.tsapps/api/src/modules/inventory/processors/inventory.processor.tsapps/api/src/modules/order/order.service.ts(reserve/release outbox emitters)apps/api/src/modules/order/processing/order.processor.ts(finalize/release emitters)apps/api/src/modules/order/processing/outbox-dispatcher.service.tspackages/jobs/src/index.ts
See Also
17. Endpoint Payload Cheatsheet
17.1 GET /api/admin/inventory
Request
GET /api/admin/inventory?page=1&size=20&pagination=true&lowStockOnly=false HTTP/1.1
Authorization: Bearer <jwt>Success response (200)
{
"message": "Inventory fetched",
"data": [
{
"id": 15,
"productId": 205,
"totalQuantity": 40,
"reservedQuantity": 4,
"availableQuantity": 36,
"lowStockThreshold": 5,
"trackInventory": true,
"allowOversell": false,
"isLowStock": false,
"createdAt": "2026-04-16T05:00:00.000Z",
"updatedAt": "2026-04-16T05:05:00.000Z"
}
],
"count": 1,
"currentPage": 1,
"totalPage": 1,
"nextCursor": null
}17.2 GET /api/admin/inventory/low-stock
Request
GET /api/admin/inventory/low-stock?threshold=3 HTTP/1.1
Authorization: Bearer <jwt>Success response (200)
{
"message": "Low stock inventory",
"data": [
{
"id": 21,
"productId": 310,
"totalQuantity": 8,
"reservedQuantity": 1,
"availableQuantity": 2,
"lowStockThreshold": 5,
"trackInventory": true,
"allowOversell": false,
"isLowStock": true,
"createdAt": "2026-04-16T05:00:00.000Z",
"updatedAt": "2026-04-16T06:00:00.000Z"
}
]
}17.3 GET /api/admin/inventory/:productId
Request
GET /api/admin/inventory/205 HTTP/1.1
Authorization: Bearer <jwt>Success response (200, found)
{
"message": "Inventory fetched",
"data": {
"id": 15,
"productId": 205,
"totalQuantity": 40,
"reservedQuantity": 4,
"availableQuantity": 36,
"lowStockThreshold": 5,
"trackInventory": true,
"allowOversell": false,
"isLowStock": false,
"createdAt": "2026-04-16T05:00:00.000Z",
"updatedAt": "2026-04-16T05:05:00.000Z"
}
}Success response (200, missing)
{
"message": "Inventory not found",
"data": null
}17.4 POST /api/admin/inventory/:productId/adjust
Request
POST /api/admin/inventory/205/adjust HTTP/1.1
Authorization: Bearer <jwt>
Content-Type: application/json
{
"adjustment": -2,
"reason": "manual_count_correction",
"actorId": "admin-user-1"
}Success response (200)
{
"message": "Stock adjusted",
"data": {
"id": 15,
"productId": 205,
"totalQuantity": 38,
"reservedQuantity": 4,
"availableQuantity": 34,
"lowStockThreshold": 5,
"trackInventory": true,
"allowOversell": false,
"isLowStock": false,
"createdAt": "2026-04-16T05:00:00.000Z",
"updatedAt": "2026-04-16T06:30:00.000Z"
}
}17.5 POST /api/admin/inventory/bulk-sync
Request
POST /api/admin/inventory/bulk-sync HTTP/1.1
Authorization: Bearer <jwt>
Content-Type: application/json
{
"trigger": "admin_reconcile"
}Success response (200)
{
"message": "1 inventory records synced",
"data": [
{
"id": 22,
"productId": 415,
"totalQuantity": 20,
"reservedQuantity": 5,
"availableQuantity": 15,
"lowStockThreshold": 5,
"trackInventory": true,
"allowOversell": false,
"isLowStock": false,
"createdAt": "2026-04-16T05:00:00.000Z",
"updatedAt": "2026-04-16T06:35:00.000Z"
}
]
}18. Outbox Payload Examples
18.1 Reserve payload (stock.reserve)
{
"orderId": 9001,
"productId": 205,
"quantity": 2,
"dedupeKey": "stock.reserve:9001:205",
"requestId": "checkout_6de8b7",
"correlationId": "order_checkout_user_42_6de8b7"
}18.2 Finalize payload (stock.finalize)
{
"orderId": 9001,
"productId": 205,
"quantity": 2,
"dedupeKey": "stock.finalize:9001:205",
"paymentReference": "esewa_txn_39f",
"requestId": "payment_success_9001",
"correlationId": "order_payment_success_9001"
}18.3 Release payload (stock.release)
{
"orderId": 9001,
"productId": 205,
"quantity": 2,
"reason": "cancel",
"dedupeKey": "stock.release:9001:205:cancel",
"requestId": "cancel_1ab4",
"correlationId": "order_cancel_9001"
}18.4 Adjust payload (stock.adjust)
{
"productId": 205,
"adjustment": 7,
"reason": "warehouse_received",
"actorId": "admin-user-1",
"dedupeKey": "stock.adjust:205:warehouse_received",
"requestId": "inventory_adjust_205",
"correlationId": "inventory_admin_adjust_205"
}18.5 Sync payload (stock.sync)
{
"productId": 205,
"requestId": "inventory_sync_205",
"correlationId": "inventory_admin_sync_205"
}19. HTTP Error Reference
| Endpoint | Condition | HTTP | errorCode |
|---|---|---|---|
POST /:productId/adjust | negative resulting quantity | 400 | INVENTORY_INVALID_ADJUSTMENT |
| lifecycle worker path | insufficient stock during reserve | 400 | INVENTORY_INSUFFICIENT_STOCK |
| lifecycle worker path | invalid reserve/finalize math | 400 | INVENTORY_INVALID_OPERATION |
| lifecycle worker path | finalize on missing row | 400/404 surface-dependent | INVENTORY_NOT_FOUND |
| guarded admin routes | missing/invalid auth | 401/403 | auth/permission errors |
20. Troubleshooting Guide
20.1 Stock seems deducted twice
Check in order:
- outbox dedupe key uniqueness in
outbox_events - job id dedupe behavior in outbox dispatcher
- whether replay happened after state already finalized
- quantity values in
order_item.quantity
20.2 Reserve keeps failing with insufficient stock
Check:
trackInventoryandallowOversellflags for that product row- current
availableQuantityvs requested quantity - concurrent reservations causing latest row lock result to fail
20.3 Low-stock endpoint returns no data
Check:
trackInventory=truefor candidate products- threshold value (query or default)
- whether available quantity is already above threshold after sync/adjust
20.4 Sync endpoint returns empty list
This means all rows already satisfy:
availableQuantity === totalQuantity - reservedQuantity
21. cURL Examples
21.1 List inventory
curl -X GET "http://localhost:5002/api/admin/inventory?page=1&size=20" \
-H "Authorization: Bearer <token>"21.2 Adjust stock
curl -X POST "http://localhost:5002/api/admin/inventory/205/adjust" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"adjustment":3,"reason":"restock","actorId":"admin-1"}'21.3 Bulk sync
curl -X POST "http://localhost:5002/api/admin/inventory/bulk-sync" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"trigger":"scheduled_reconcile"}'22. Environment Variables (API Surface Context)
| Variable | Default | Used by |
|---|---|---|
INVENTORY_STOCK_CACHE_TTL_SECONDS | unset | not yet wired |
INVENTORY_LIST_CACHE_TTL_SECONDS | unset | not yet wired |
INVENTORY_CONFIG_CACHE_TTL_SECONDS | unset | not yet wired |
INVENTORY_HISTORY_CACHE_TTL_SECONDS | unset | not yet wired |
INVENTORY_RESERVATIONS_CACHE_TTL_SECONDS | unset | not yet wired |
Implementation note:
- these keys are present in env validation but currently not consumed by inventory service/controller logic.