Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 107 additions & 24 deletions core/api/fragments/inventory.yaml
Original file line number Diff line number Diff line change
@@ -1,46 +1,72 @@
# SPDX-License-Identifier: LicenseRef-OpenLBM-Commons-1.0
# SPDX-FileCopyrightText: 2026 FutureBuild, Inc. and OpenLBM contributors
#
# The inventory module, transcribed from internal/inventory/handler.go and model.go
# as they behave today: float quantities, the list is a nil slice (null when empty)
# and both writes answer a bare status acknowledgement.
# The inventory module. The read is on the wire contract (C3-1b, ADR 0006
# 7.2): the levels list as the ADR 0001 envelope with scale 4 decimal string
# quantities in the product's stocking unit, `available` and `uom` beside them,
# and the product summary under include=product. The two writes stay on their
# legacy shapes until C4-1 converts the module's write side.
paths:
/api/v1/inventory:
get:
operationId: inventoryList
tags:
- inventory
summary: List inventory rows of a product
description: A product_id that is not a UUID reaches the service and answers 500, not 400.
summary: List inventory levels
description: >-
The keyset envelope of ADR 0001 section 1, ordered created_at DESC,
id DESC (ADR 0006 7.2). Every quantity is a scale 4 decimal string in
the product's stocking unit, with available (quantity - allocated) and
uom beside it. The list is held to the branch wall: a context branch
(X-Branch-Id) lists its own rows; with no context branch a bound
non-admin user lists the branches granted to the user, none granted
listing none; an administrator without a header, an unbound key, the
single-branch switch and callers with no branch context at all list
every branch's. A legacy row with no location_id has no branch on its
joined location and stays visible to an administrator without a header
and to callers with no branch context only (C4-1 migrates legacy rows
onto locations). product_id and location_id are optional filters; a
request without product_id is the whole levels list, where the base
refused it with 400.
parameters:
- $ref: '#/components/parameters/PageLimit'
- $ref: '#/components/parameters/Cursor'
- $ref: '#/components/parameters/Include'
# include=total counts the filtered set; include=product embeds the
# product summary in every row.
- name: product_id
in: query
required: true
description: Required; a missing value answers 400.
required: false
description: Only the named product's rows. A value that is not a UUID is a 400 naming it.
schema:
type: string
format: uuid
- name: location_id
in: query
required: false
description: Only the rows at the named location. A value that is not a UUID is a 400 naming it.
schema:
type: string
format: uuid
- $ref: '#/components/parameters/XBranchId'
responses:
'200':
description: The product's inventory rows, one per location. The body is null when the product
has none.
description: >-
The page of inventory levels. items is never null; total is
present only under include=total; each row's product is present
only under include=product.
content:
application/json:
schema:
type:
- array
- 'null'
items:
$ref: '#/components/schemas/Inventory'
$ref: '#/components/schemas/InventoryLevelPage'
'400':
$ref: '#/components/responses/BadRequest'
$ref: '#/components/responses/BadRequestEither'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/ForbiddenEither'
'500':
$ref: '#/components/responses/InternalError'
$ref: '#/components/responses/InternalErrorEither'
/api/v1/inventory/adjust:
post:
operationId: inventoryAdjust
Expand Down Expand Up @@ -118,15 +144,22 @@ paths:
$ref: '#/components/responses/InternalError'
components:
schemas:
Inventory:
InventoryLevel:
type: object
description: inventory.Inventory. location_id is omitted when unset.
description: >-
One inventory row on the wire (ADR 0006 7.2): the quantities as scale
4 decimal strings in the product's stocking unit, available as
quantity - allocated, and the location's name beside its id. The
product summary is present only under include=product.
required:
- id
- product_id
- location
- location_id
- location_name
- quantity
- allocated
- available
- uom
- updated_at
properties:
id:
Expand All @@ -136,18 +169,68 @@ components:
type: string
format: uuid
location_id:
type: string
type:
- string
- 'null'
format: uuid
location:
description: The location the stock sits at; null on a legacy row that never had one.
location_name:
type: string
description: Deprecated text field.
description: The location's path, or the row's deprecated free text when it has none.
quantity:
type: number
type: string
description: On hand, in the product's stocking unit.
allocated:
type: number
type: string
description: Reserved, in the product's stocking unit.
available:
type: string
description: quantity - allocated, in the product's stocking unit.
uom:
$ref: '#/components/schemas/UOM'
product:
$ref: '#/components/schemas/InventoryProductSummary'
updated_at:
type: string
format: date-time
InventoryProductSummary:
type: object
description: >-
The product an inventory row stocks, embedded under include=product
(the expansion ADR 0001 section 1 names): the identity fields a levels
screen shows beside the numbers, with the stocking unit the quantities
are counted in.
required:
- id
- sku
- description
- stock_uom
properties:
id:
type: string
format: uuid
sku:
type: string
description:
type: string
stock_uom:
$ref: '#/components/schemas/UOM'
InventoryLevelPage:
type: object
required: [items, next_cursor, limit]
description: The list envelope of ADR 0001 section 1. items is never null; total is present only under include=total.
properties:
items:
type: array
items:
$ref: '#/components/schemas/InventoryLevel'
next_cursor:
type: [string, "null"]
limit:
type: integer
total:
type: integer
format: int64
StockAdjustmentRequest:
type: object
required:
Expand Down
101 changes: 81 additions & 20 deletions core/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5076,36 +5076,46 @@ paths:
operationId: inventoryList
tags:
- inventory
summary: List inventory rows of a product
description: A product_id that is not a UUID reaches the service and answers 500, not 400.
summary: List inventory levels
description: >-
The keyset envelope of ADR 0001 section 1, ordered created_at DESC, id DESC (ADR 0006 7.2). Every quantity is a scale 4 decimal string in the product's stocking unit, with available (quantity - allocated) and uom beside it. The list is held to the branch wall: a context branch (X-Branch-Id) lists its own rows; with no context branch a bound non-admin user lists the branches granted to the user, none granted listing none; an administrator without a header, an unbound key, the single-branch switch and callers with no branch context at all list every branch's. A legacy row with no location_id has no branch on its joined location and stays visible to an administrator without a header and to callers with no branch context only (C4-1 migrates legacy rows onto locations). product_id and location_id are optional filters; a request without product_id is the whole levels list, where the base refused it with 400.
parameters:
- $ref: '#/components/parameters/PageLimit'
- $ref: '#/components/parameters/Cursor'
- $ref: '#/components/parameters/Include'
# include=total counts the filtered set; include=product embeds the
# product summary in every row.
- name: product_id
in: query
required: true
description: Required; a missing value answers 400.
required: false
description: Only the named product's rows. A value that is not a UUID is a 400 naming it.
schema:
type: string
format: uuid
- name: location_id
in: query
required: false
description: Only the rows at the named location. A value that is not a UUID is a 400 naming it.
schema:
type: string
format: uuid
- $ref: '#/components/parameters/XBranchId'
responses:
'200':
description: The product's inventory rows, one per location. The body is null when the product has none.
description: >-
The page of inventory levels. items is never null; total is present only under include=total; each row's product is present only under include=product.
content:
application/json:
schema:
type:
- array
- 'null'
items:
$ref: '#/components/schemas/Inventory'
$ref: '#/components/schemas/InventoryLevelPage'
'400':
$ref: '#/components/responses/BadRequest'
$ref: '#/components/responses/BadRequestEither'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/ForbiddenEither'
'500':
$ref: '#/components/responses/InternalError'
$ref: '#/components/responses/InternalErrorEither'
/api/v1/inventory/adjust:
post:
operationId: inventoryAdjust
Expand Down Expand Up @@ -16340,15 +16350,19 @@ components:
items:
type: string
description: Never null.
Inventory:
InventoryLevel:
type: object
description: inventory.Inventory. location_id is omitted when unset.
description: >-
One inventory row on the wire (ADR 0006 7.2): the quantities as scale 4 decimal strings in the product's stocking unit, available as quantity - allocated, and the location's name beside its id. The product summary is present only under include=product.
required:
- id
- product_id
- location
- location_id
- location_name
- quantity
- allocated
- available
- uom
- updated_at
properties:
id:
Expand All @@ -16358,18 +16372,65 @@ components:
type: string
format: uuid
location_id:
type: string
type:
- string
- 'null'
format: uuid
location:
description: The location the stock sits at; null on a legacy row that never had one.
location_name:
type: string
description: Deprecated text field.
description: The location's path, or the row's deprecated free text when it has none.
quantity:
type: number
type: string
description: On hand, in the product's stocking unit.
allocated:
type: number
type: string
description: Reserved, in the product's stocking unit.
available:
type: string
description: quantity - allocated, in the product's stocking unit.
uom:
$ref: '#/components/schemas/UOM'
product:
$ref: '#/components/schemas/InventoryProductSummary'
updated_at:
type: string
format: date-time
InventoryProductSummary:
type: object
description: >-
The product an inventory row stocks, embedded under include=product (the expansion ADR 0001 section 1 names): the identity fields a levels screen shows beside the numbers, with the stocking unit the quantities are counted in.
required:
- id
- sku
- description
- stock_uom
properties:
id:
type: string
format: uuid
sku:
type: string
description:
type: string
stock_uom:
$ref: '#/components/schemas/UOM'
InventoryLevelPage:
type: object
required: [items, next_cursor, limit]
description: The list envelope of ADR 0001 section 1. items is never null; total is present only under include=total.
properties:
items:
type: array
items:
$ref: '#/components/schemas/InventoryLevel'
next_cursor:
type: [string, "null"]
limit:
type: integer
total:
type: integer
format: int64
StockAdjustmentRequest:
type: object
required:
Expand Down
3 changes: 2 additions & 1 deletion core/internal/apicontract/model_shape_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,8 @@ var modelBoundSchemas = []struct {
{"DeliveryRouteLeg", delivery.RouteLeg{}},
{"DeliveryRouteOptimizationResult", delivery.RouteOptimizationResult{}},
// inventory
{"Inventory", inventory.Inventory{}},
{"InventoryLevel", inventory.Level{}},
{"InventoryProductSummary", inventory.ProductSummary{}},
// deposits
{"Deposit", deposit.CustomerDeposit{}},
{"DepositApplication", deposit.DepositApplication{}},
Expand Down
Loading
Loading