Skip to content

API Contract: Item Catalog

Module: item_catalog Last Updated: 2026-05-10 Generated From: analysis.md


Endpoints Overview

Method Path Use Case Auth Roles
POST /api/v1/items UC-001: CreateItem JWT Admin, Manager, Technologist
GET /api/v1/items UC-Q02: ListItems JWT All
GET /api/v1/items/import/template UC-Q03: GetImportTemplate JWT Admin
POST /api/v1/items/import UC-007: ImportItems JWT Admin
GET /api/v1/items/{item_id} UC-Q01: GetItem JWT All
PATCH /api/v1/items/{item_id} UC-002: EditItem JWT Admin, Manager, Technologist
POST /api/v1/items/{item_id}/deactivate UC-003: DeactivateItem JWT Admin
POST /api/v1/items/{item_id}/reactivate UC-004: ReactivateItem JWT Admin
PUT /api/v1/items/{item_id}/group UC-005: AssignItemToGroup JWT Admin, Manager, Technologist
DELETE /api/v1/items/{item_id}/group UC-006: RemoveItemFromGroup JWT Admin, Manager, Technologist
POST /api/v1/item-groups UC-008: CreateItemGroup JWT Admin, Manager
GET /api/v1/item-groups UC-Q04: ListItemGroups JWT All
GET /api/v1/item-groups/{group_id} UC-Q05: GetItemGroup JWT All
PATCH /api/v1/item-groups/{group_id} UC-009: EditItemGroup JWT Admin, Manager
DELETE /api/v1/item-groups/{group_id} UC-010: DeleteItemGroup JWT Admin, Manager
GET /api/v1/item-groups/{group_id}/items UC-Q06: ListItemsByGroup JWT All

Router registration order note: /api/v1/items/import/template and /api/v1/items/import must be registered before /api/v1/items/{item_id} in the FastAPI router so the literal import segment is not consumed as a UUID path parameter.


Shared Schemas

Error Response (all error codes)

{
  "error": {
    "code": "ITEM_NOT_FOUND",
    "message": "Item with id '...' was not found.",
    "details": []
  }
}

For validation errors the details array contains field-level entries:

{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Validation failed.",
    "details": [
      { "field": "code", "message": "Field is required." }
    ]
  }
}

Pagination Response Envelope

All list endpoints return:

{
  "items": [...],
  "total": 100,
  "page": 1,
  "page_size": 20,
  "pages": 5
}

Endpoint Details


POST /api/v1/items

Use Case: UC-001: CreateItem Roles: Admin, Manager, Technologist

Request Body

{
  "code": "MAK-001",
  "name": "Mąka pszenna typ 500",
  "item_type": "ingredient",
  "storage_unit": "kg",
  "description": "Biała mąka pszenna do pieczywa.",
  "tags": ["mąka", "pszenna"],
  "group_id": "11111111-1111-1111-1111-111111111111",
  "shelf_life_days": null,
  "ean": "5901234123457"
}
Field Type Required Constraints
code string Yes max 50 chars
name string Yes max 200 chars
item_type string (enum) Yes ingredient | product | intermediate | packaging | consumable | service | resale
storage_unit string (enum) Yes kg | g | l | ml | piece
description string | null No max 500 chars
tags string[] No max 20 items, each max 50 chars; duplicates removed
group_id UUID | null No must exist for tenant; item_type must match group
shelf_life_days integer | null No product and intermediate only; positive integer
ean string | null No max 20 chars; must be unique per tenant

Response — 201 Created

{
  "item_id": "22222222-2222-2222-2222-222222222222"
}

Error Responses

Status Error Code Domain Exception
400 SHELF_LIFE_DAYS_NOT_APPLICABLE ShelfLifeDaysNotApplicable
409 ITEM_CODE_ALREADY_IN_USE ItemCodeAlreadyInUse
409 EAN_ALREADY_IN_USE EANAlreadyInUse
404 ITEM_GROUP_NOT_FOUND ItemGroupNotFound
409 GROUP_ITEM_TYPE_MISMATCH GroupItemTypeMismatch
422 VALIDATION_ERROR Pydantic validation failure

GET /api/v1/items

Use Case: UC-Q02: ListItems Roles: All authenticated

Query Parameters

Parameter Type Default Description
q string Full-text, diacritic-insensitive search across name, code, description, group name, tags
item_type string[] [] Multi-value filter; repeat parameter e.g. ?item_type=ingredient&item_type=product
status "active" \| "inactive" \| "all" "active" Tri-state working-set filter (see conventions.md § Status filter)
group_id UUID Filter to items in a specific group
page integer 1 Page number
page_size integer 20 Items per page (max 100)
as_of datetime (ISO-8601, UTC) Snapshot timestamp used for stable pagination — see conventions.md § Pagination + snapshot
sort enum string -created_at One of name, -name, code, -code, created_at, -created_at, updated_at, -updated_at, item_type, -item_type, status, -status, storage_unit, -storage_unit, shelf_life_days, -shelf_life_days. A - prefix sorts descending.

Response — 200 OK

{
  "items": [
    {
      "item_id": "22222222-2222-2222-2222-222222222222",
      "code": "MAK-001",
      "name": "Mąka pszenna typ 500",
      "item_type": "ingredient",
      "storage_unit": "kg",
      "status": "active",
      "is_system": false,
      "group": {
        "group_id": "11111111-1111-1111-1111-111111111111",
        "name": "Mąki i skrobie"
      }
    }
  ],
  "total": 142,
  "page": 1,
  "page_size": 20,
  "pages": 8
}

GET /api/v1/items/import/template

Use Case: UC-Q03: GetImportTemplate Roles: Admin

Response — 200 OK

Binary .xlsx file download.

Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Content-Disposition: attachment; filename="item_catalog_import_template.xlsx"

POST /api/v1/items/import

Use Case: UC-007: ImportItems Roles: Admin

Request

Content-Type: multipart/form-data

Field Type Description
file file .xlsx file, max 5 MB, max 500 data rows

Response — 201 Created

{
  "created_count": 47
}

Error Responses

Status Error Code Description
400 IMPORT_FILE_TOO_LARGE File exceeds 5 MB
400 IMPORT_TOO_MANY_ROWS More than 500 data rows
400 IMPORT_VALIDATION_FAILED One or more rows failed validation

For IMPORT_VALIDATION_FAILED, the details array lists every failing row:

{
  "error": {
    "code": "IMPORT_VALIDATION_FAILED",
    "message": "3 row(s) failed validation. No items were imported.",
    "details": [
      { "row": 2, "field": "code", "message": "Code 'MAK-001' is already in use." },
      { "row": 5, "field": "item_type", "message": "Invalid item type 'raw'." },
      { "row": 9, "field": "shelf_life_days", "message": "shelf_life_days is not applicable to item type 'service'." }
    ]
  }
}

GET /api/v1/items/{item_id}

Use Case: UC-Q01: GetItem (by ID) Roles: All authenticated

Path Parameters

Parameter Type Description
item_id UUID Item identifier

Response — 200 OK

{
  "item_id": "22222222-2222-2222-2222-222222222222",
  "code": "MAK-001",
  "name": "Mąka pszenna typ 500",
  "item_type": "ingredient",
  "storage_unit": "kg",
  "status": "active",
  "is_system": false,
  "description": "Biała mąka pszenna do pieczywa.",
  "tags": ["mąka", "pszenna"],
  "group": {
    "group_id": "11111111-1111-1111-1111-111111111111",
    "name": "Mąki i skrobie"
  },
  "shelf_life_days": null,
  "ean": "5901234123457",
  "created_by": "33333333-3333-3333-3333-333333333333",
  "created_at": "2026-01-15T09:00:00Z",
  "updated_by": "44444444-4444-4444-4444-444444444444",
  "updated_at": "2026-03-20T14:30:00Z"
}

To look up by code instead of UUID, use query parameter: GET /api/v1/items?q=MAK-001&item_type=... — or expose a separate lookup endpoint if needed (GET /api/v1/items/by-code/{code}). Decision deferred to implementation.

Error Responses

Status Error Code Domain Exception
404 ITEM_NOT_FOUND ItemNotFound

PATCH /api/v1/items/{item_id}

Use Case: UC-002: EditItem Roles: Admin, Manager, Technologist

Request Body

Only include fields to change. All fields are optional. To explicitly clear an optional field (EAN, shelf_life_days), send it as null.

{
  "name": "Mąka pszenna typ 650",
  "description": "Zaktualizowany opis.",
  "tags": ["mąka", "pszenna", "chlebowa"],
  "shelf_life_days": null,
  "ean": null
}
Field Type Constraints Clears field when null?
name string | absent max 200 chars No (required if present)
description string | null | absent max 500 chars Yes — null clears description
tags string[] | absent max 20 × 50 chars No (send [] to clear all tags)
shelf_life_days integer | null | absent positive integer Yes — null clears the value
ean string | null | absent max 20 chars Yes — null clears EAN

Immutable fields (code, item_type, storage_unit) must not be sent. If sent, return 400.

Response — 200 OK

{
  "item_id": "22222222-2222-2222-2222-222222222222",
  "changed": true
}

changed: false when all submitted values were identical to stored values (no-op edit).

Error Responses

Status Error Code Domain Exception
404 ITEM_NOT_FOUND ItemNotFound
403 SYSTEM_ITEM_IS_READ_ONLY SystemItemIsReadOnly
400 IMMUTABLE_FIELD_CANNOT_BE_CHANGED ImmutableFieldCannotBeChanged
400 SHELF_LIFE_DAYS_NOT_APPLICABLE ShelfLifeDaysNotApplicable
409 EAN_ALREADY_IN_USE EANAlreadyInUse

POST /api/v1/items/{item_id}/deactivate

Use Case: UC-003: DeactivateItem Roles: Admin

Request Body

Empty body or {}.

Response — 200 OK

{
  "item_id": "22222222-2222-2222-2222-222222222222"
}

Error Responses

Status Error Code Domain Exception User-Facing Message
404 ITEM_NOT_FOUND ItemNotFound
403 SYSTEM_ITEM_IS_READ_ONLY SystemItemIsReadOnly "System items are read-only."
400 INVALID_ITEM_STATE_TRANSITION InvalidItemStateTransition "Item is already inactive."
409 ITEM_HAS_STOCK_REMAINING ItemHasStockRemaining "Item has stock remaining. Clear all stock before deactivating."
409 ITEM_HAS_OPEN_PRODUCTION_ORDERS ItemHasOpenProductionOrders "Item is referenced in {n} open production order(s)."
409 ITEM_IS_IN_PUBLISHED_BOM ItemIsInPublishedBOM "Item is used in {n} published recipe(s)."
409 ITEM_HAS_OPEN_SALES_ORDERS ItemHasOpenSalesOrders "Item is referenced in {n} open sales order(s)."

POST /api/v1/items/{item_id}/reactivate

Use Case: UC-004: ReactivateItem Roles: Admin

Request Body

Empty body or {}.

Response — 200 OK

{
  "item_id": "22222222-2222-2222-2222-222222222222"
}

Error Responses

Status Error Code Domain Exception
404 ITEM_NOT_FOUND ItemNotFound
400 INVALID_ITEM_STATE_TRANSITION InvalidItemStateTransition

PUT /api/v1/items/{item_id}/group

Use Case: UC-005: AssignItemToGroup Roles: Admin, Manager, Technologist

Request Body

{
  "group_id": "11111111-1111-1111-1111-111111111111"
}

Response — 200 OK

{
  "item_id": "22222222-2222-2222-2222-222222222222",
  "group_id": "11111111-1111-1111-1111-111111111111"
}

Error Responses

Status Error Code Domain Exception
404 ITEM_NOT_FOUND ItemNotFound
403 SYSTEM_ITEM_IS_READ_ONLY SystemItemIsReadOnly
404 ITEM_GROUP_NOT_FOUND ItemGroupNotFound
409 GROUP_ITEM_TYPE_MISMATCH GroupItemTypeMismatch

DELETE /api/v1/items/{item_id}/group

Use Case: UC-006: RemoveItemFromGroup Roles: Admin, Manager, Technologist

Response — 204 No Content

Error Responses

Status Error Code Domain Exception
404 ITEM_NOT_FOUND ItemNotFound
403 SYSTEM_ITEM_IS_READ_ONLY SystemItemIsReadOnly
400 ITEM_GROUP_NOT_ASSIGNED ItemGroupNotAssigned

POST /api/v1/item-groups

Use Case: UC-008: CreateItemGroup Roles: Admin, Manager

Request Body

{
  "name": "Mąki i skrobie",
  "item_type": "ingredient"
}
Field Type Required Constraints
name string Yes max 100 chars; unique per (tenant, item_type) case-insensitively
item_type string (enum) Yes ingredient | product | intermediate | packaging | consumable | service | resale; immutable after creation

Response — 201 Created

{
  "group_id": "11111111-1111-1111-1111-111111111111"
}

Error Responses

Status Error Code Domain Exception
409 ITEM_GROUP_NAME_ALREADY_IN_USE ItemGroupNameAlreadyInUse
422 VALIDATION_ERROR Pydantic

GET /api/v1/item-groups

Use Case: UC-Q04: ListItemGroups Roles: All authenticated

Query Parameters

Parameter Type Default Description
item_type string Filter groups by item type
page integer 1
page_size integer 20

Response — 200 OK

{
  "groups": [
    {
      "group_id": "11111111-1111-1111-1111-111111111111",
      "name": "Mąki i skrobie",
      "item_type": "ingredient",
      "member_count": 14
    }
  ],
  "total": 8,
  "page": 1,
  "page_size": 20,
  "pages": 1
}

GET /api/v1/item-groups/{group_id}

Use Case: UC-Q05: GetItemGroup Roles: All authenticated

Response — 200 OK

{
  "group_id": "11111111-1111-1111-1111-111111111111",
  "name": "Mąki i skrobie",
  "item_type": "ingredient",
  "member_count": 14,
  "created_by": "33333333-3333-3333-3333-333333333333",
  "created_at": "2026-01-10T08:00:00Z",
  "updated_by": null,
  "updated_at": null
}

Error Responses

Status Error Code Domain Exception
404 ITEM_GROUP_NOT_FOUND ItemGroupNotFound

PATCH /api/v1/item-groups/{group_id}

Use Case: UC-009: EditItemGroup Roles: Admin, Manager

Request Body

{
  "name": "Mąki, skrobie i grysiki"
}
Field Type Required Constraints
name string Yes max 100 chars

Response — 200 OK

{
  "group_id": "11111111-1111-1111-1111-111111111111",
  "changed": true,
  "version": 3
}
Field Type Description
group_id uuid Echoes the path parameter
changed bool false when the request is a no-op (empty body, or name equals current value); no version bump, no audit-field update
version int Post-save aggregate version (from OptimisticLockMixin). Clients pass this back as their last-seen version on subsequent PATCHes

Error Responses

Status Error Code Domain Exception
404 ITEM_GROUP_NOT_FOUND ItemGroupNotFound
409 ITEM_GROUP_NAME_ALREADY_IN_USE ItemGroupNameAlreadyInUse

DELETE /api/v1/item-groups/{group_id}

Use Case: UC-010: DeleteItemGroup Roles: Admin, Manager

Response — 204 No Content

Error Responses

Status Error Code Domain Exception
404 ITEM_GROUP_NOT_FOUND ItemGroupNotFound
403 SYSTEM_GROUP_IS_READ_ONLY SystemGroupIsReadOnly
409 ITEM_GROUP_HAS_MEMBERS ItemGroupHasMembers

GET /api/v1/item-groups/{group_id}/items

Use Case: UC-Q06: ListItemsByGroup Roles: All authenticated

Query Parameters

Parameter Type Default Description
page integer 1
page_size integer 20

Response — 200 OK

Same envelope as /api/v1/items but scoped to the group. Each item in the items array matches the ItemSummary shape (without group field — it is redundant here).

Error Responses

Status Error Code Domain Exception
404 ITEM_GROUP_NOT_FOUND ItemGroupNotFound

Domain Exception → HTTP Status Mapping (consolidated)

Domain Exception HTTP Status Error Code
ItemNotFound 404 ITEM_NOT_FOUND
ItemGroupNotFound 404 ITEM_GROUP_NOT_FOUND
ItemCodeAlreadyInUse 409 ITEM_CODE_ALREADY_IN_USE
EANAlreadyInUse 409 EAN_ALREADY_IN_USE
SystemItemIsReadOnly 403 SYSTEM_ITEM_IS_READ_ONLY
SystemGroupIsReadOnly 403 SYSTEM_GROUP_IS_READ_ONLY
ImmutableFieldCannotBeChanged 400 IMMUTABLE_FIELD_CANNOT_BE_CHANGED
ShelfLifeDaysNotApplicable 400 SHELF_LIFE_DAYS_NOT_APPLICABLE
GroupItemTypeMismatch 409 GROUP_ITEM_TYPE_MISMATCH
ItemHasStockRemaining 409 ITEM_HAS_STOCK_REMAINING
ItemHasOpenProductionOrders 409 ITEM_HAS_OPEN_PRODUCTION_ORDERS
ItemIsInPublishedBOM 409 ITEM_IS_IN_PUBLISHED_BOM
ItemHasOpenSalesOrders 409 ITEM_HAS_OPEN_SALES_ORDERS
InvalidItemStateTransition 400 INVALID_ITEM_STATE_TRANSITION
ItemGroupNameAlreadyInUse 409 ITEM_GROUP_NAME_ALREADY_IN_USE
ItemGroupHasMembers 409 ITEM_GROUP_HAS_MEMBERS
ItemGroupNotAssigned 400 ITEM_GROUP_NOT_ASSIGNED
ImportValidationFailed 400 IMPORT_VALIDATION_FAILED
ImportFileTooLarge 400 IMPORT_FILE_TOO_LARGE
ImportTooManyRows 400 IMPORT_TOO_MANY_ROWS