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/templateand/api/v1/items/importmust be registered before/api/v1/items/{item_id}in the FastAPI router so the literalimportsegment 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:
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¶
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¶
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¶
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¶
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¶
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¶
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¶
| 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¶
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¶
| Field | Type | Required | Constraints |
|---|---|---|---|
| name | string | Yes | max 100 chars |
Response — 200 OK¶
| 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 |