Domain Analysis: Item Catalog¶
Date: 2026-05-10 Analyst: Claude (Technical Domain Analyst Skill) Status: Analysis Complete
1. Executive Summary¶
ItemCatalog is the single source of truth for item identity in GrinSystem — every item that appears in recipes, stock, orders, or invoices must first be registered here. The module owns two entities (Item and ItemGroup) and publishes four domain events consumed by Manufacturing, Warehouse, and CRM. Key architectural decisions made during analysis: deactivation preconditions are enforced via in-process synchronous queries to sibling modules (an explicit exception to the events-only inter-module rule), group membership is a single optional FK on Item (0 or 1 group per item), and two events were added to the catalog beyond the original spec (ItemReactivated, ItemUpdated). ItemUpdated is a unified edit event carrying all changed fields including old_ean/new_ean, replacing a previously considered separate ItemEANUpdated.
2. Bounded Context¶
Context: ItemCatalog¶
Core Responsibility: Maintain the canonical registry of item identity — code, name, type, unit of measure, and organisational grouping. Every other module references items by item_id and owns its own extension data (nutrition, stock rules, pricing) linked to that ID.
| Entity | Description | Relationships |
|---|---|---|
| Item | The core item record — identity, type, unit, status | Belongs to 0..1 ItemGroup; referenced by all other modules via item_id |
| ItemGroup | Organisational container for browsing and filtering items by business category | Has 0..N Items (via FK on Item) |
Context Boundaries: - In scope: item identity (code, name, type, unit), active/inactive lifecycle, organisational grouping, EAN/barcode, tags, shelf life days, Excel import - Out of scope: allergens, nutritional values (Manufacturing); stock levels, LOTs, reorder points (Warehouse); pricing, images, orderable flag (CRM); recipe/BOM associations (Manufacturing); supplier information (Warehouse); ingredient substitution groups — IngredientGroups (Manufacturing) - Publishes to: Manufacturing, Warehouse, CRM via events (in-process dispatcher) - Consumes from: nothing (source of truth; uses in-process queries to siblings only for deactivation checks)
3. State Machines¶
Item State Machine¶
stateDiagram-v2
[*] --> Active : CreateItem / system provisioning
Active --> Inactive : DeactivateItem [preconditions met]
Inactive --> Active : ReactivateItem
| State | Entry Actions | Exit Actions | Valid Transitions |
|---|---|---|---|
| Active | Emit ItemCreated; sibling modules create extension records | — | → Inactive (DeactivateItem) |
| Inactive | Emit ItemDeactivated; CRM sets orderable=false | — | → Active (ReactivateItem) |
Invariants:
- Active: code reserved in tenant namespace; storage_unit and item_type immutable; EAN unique within tenant if set
- Inactive: code still reserved — no other item in the tenant may claim it
ItemGroup Lifecycle¶
No state machine — a group exists or is deleted. Deletion is only permitted when the group has zero item members.
4. Actors¶
| Actor | Type | Permissions |
|---|---|---|
| Admin | Human — tenant owner or super-user | Full access: create, edit, deactivate, reactivate, import, manage groups |
| Manager (Kierownik) | Human — production manager | Create and edit items; manage groups; assign/remove group on items; cannot deactivate/reactivate |
| Technologist (Technolog) | Human — R&D / product development | Create and edit items; assign/remove group on items; cannot manage groups or deactivate |
| Worker (Pracownik) | Human — shop floor | Read-only: browse and search catalog |
| Warehouse Keeper (Magazynier) | Human — warehouse | Read-only: browse and search catalog, scan by EAN |
| Sales Rep (Handlowiec) | Human — sales | Read-only: browse orderable items |
| QA Specialist | Human — quality assurance | Read-only |
| GrinSystem (system) | System actor | Provision system items; emit ItemCreated per tenant at provisioning time |
5. Commands (Write Operations)¶
| Command | Actor | Validates | Produces |
|---|---|---|---|
| CreateItem | Admin, Manager, Technologist | Code unique (tenant + system ns); storage_unit enum; item_type enum; shelf_life_days type restriction; EAN uniqueness; tag constraints; group compatibility | ItemCreated |
| EditItem | Admin, Manager, Technologist | Not system item; immutable fields untouched; shelf_life_days type restriction; EAN uniqueness | ItemUpdated (conditional — omitted if no field changed) |
| DeactivateItem | Admin | Not system item; zero stock (Warehouse query); no open ZPs (Manufacturing query); no open ZSs (CRM query); not in Published BOM (Manufacturing query) | ItemDeactivated |
| ReactivateItem | Admin | Item is Inactive | ItemReactivated |
| AssignItemToGroup | Admin, Manager, Technologist | Not system item; group exists in same tenant; group item_type matches item | — |
| RemoveItemFromGroup | Admin, Manager, Technologist | Item currently has a group assigned | — |
| ImportItems | Admin | File ≤ 5 MB; ≤ 500 rows; valid Excel; all-or-nothing row validation | ItemCreated (per row) |
| CreateItemGroup | Admin, Manager | Name unique per (tenant, item_type); valid item_type | — |
| EditItemGroup | Admin, Manager | Name unique per (tenant, item_type) | — |
| DeleteItemGroup | Admin, Manager | Zero members | — |
CreateItem¶
- Preconditions: none
- Parameters: code (required, max 50 chars), name (required, max 200 chars), item_type (required, immutable), storage_unit (required, immutable — kg/g/l/ml/piece), description (optional, max 500 chars), tags (optional, max 20 × 50 chars), group_id (optional), shelf_life_days (optional, product/intermediate only), ean (optional, max 20 chars), active defaults to true
- Business Rules Enforced: BR-IC-01, BR-IC-02, BR-IC-07; shelf_life_days type restriction; EAN uniqueness per tenant; tag deduplication (case-insensitive); group must share item_type
- Emitted Events: ItemCreated
EditItem¶
- Preconditions: item exists; is_system = false
- Parameters: name, description, tags, shelf_life_days, ean (all optional; omitted fields unchanged)
- Business Rules Enforced: immutable fields (code, storage_unit, item_type) rejected; shelf_life_days type restriction; EAN uniqueness; tag deduplication; shelf_life_days changes affect future LOTs only; name changes do not retroactively affect historical documents
- Emitted Events: ItemUpdated if any editable field changed (name, description, tags, shelf_life_days, ean); omitted entirely on no-op edits
DeactivateItem¶
- Preconditions: item is Active; is_system = false; in-process query to Warehouse confirms stock = 0; in-process query to Manufacturing confirms no open ZPs; in-process query to CRM confirms no open ZSs; in-process query to Manufacturing confirms item not in any Published recipe BOM
- Parameters: item_id
- Business Rules Enforced: BR-IC-06; code remains reserved after deactivation
- Emitted Events: ItemDeactivated
AssignItemToGroup¶
- Preconditions: item exists; is_system = false; group exists in same tenant; group.item_type = item.item_type
- Parameters: item_id, group_id
- Business Rules Enforced: replaces any prior group assignment (0..1 constraint); no minimum group requirement for any item type
- Emitted Events: none
ImportItems¶
- Preconditions: file ≤ 5 MB; ≤ 500 data rows; valid .xlsx format
- Parameters: Excel file. Columns: code, name, item_type, storage_unit, description (opt), group_name (opt, single), tags (opt, comma-separated), ean (opt), shelf_life_days (opt)
- Business Rules Enforced: all-or-nothing — all rows validated before any save; same rules as CreateItem per row; group_name must reference existing tenant group with matching item_type (not auto-created); import never overwrites existing or system items
- Emitted Events: ItemCreated per successfully imported row (on full success only)
CreateItemGroup¶
- Preconditions: none
- Parameters: name (required, max 100 chars), item_type (required, immutable after creation)
- Business Rules Enforced: name unique per (tenant, item_type), case-insensitive
- Emitted Events: none
DeleteItemGroup¶
- Preconditions: group exists; member_count = 0
- Parameters: group_id
- Business Rules Enforced: name freed immediately — no reservation post-deletion
- Emitted Events: none
6. Queries (Read Operations)¶
| Query | Returns | Notes |
|---|---|---|
| GetItem | Full item detail | By item_id or code |
| ListItems | Paginated item list | Full-text, diacritic-insensitive search; item_type multi-select; active filter; group filter |
| GetImportTemplate | Excel .xlsx file | Template with all supported columns |
| ListItemGroups | Paginated group list with member counts | Filter by item_type |
| GetItemGroup | Group metadata + member count | — |
| ListItemsByGroup | Paginated items in a group | Primary way for frontend to browse group contents |
GetItem¶
- Parameters:
item_id(UUID) ORcode(string) - Response: item_id, code, name, item_type, storage_unit, active, is_system, description, tags[], group (nullable: {group_id, name}), shelf_life_days, ean, created_by, created_at, updated_by, updated_at
- Authorization: all authenticated roles
ListItems¶
- Parameters: q (text, optional), item_type[] (multi-select, optional), active (bool, default true), item_group_id (UUID, optional), page (default 1), page_size (default 20)
- Response: {items: [{item_id, code, name, item_type, storage_unit, active, is_system, group (nullable), source}], total, page, page_size, pages}
- Authorization: all authenticated roles; system items visible to all tenants; Workers see all item types (read-only)
- Search behaviour: case-insensitive, diacritic-insensitive (e.g. "maka" matches "mąka") across name, code, description, group.name, tags. Requires PostgreSQL
unaccentextension or equivalent.
ListItemsByGroup¶
- Parameters: group_id, page (default 1), page_size (default 20)
- Response: {items: [{item_id, code, name, item_type, storage_unit, active, is_system}], total, page, page_size, pages}
- Authorization: all authenticated roles
7. Domain Events¶
| Event | Producer | Consumers | Integration? |
|---|---|---|---|
| ItemCreated | ItemCatalog | Manufacturing, Warehouse, CRM | Yes — published via outbox, dispatched to @on_event handlers |
| ItemUpdated | ItemCatalog | Warehouse, CRM | Yes — published via outbox, dispatched to @on_event handlers |
| ItemDeactivated | ItemCatalog | Manufacturing, Warehouse, CRM | Yes — published via outbox, dispatched to @on_event handlers |
| ItemReactivated | ItemCatalog | CRM | Yes — published via outbox, dispatched to @on_event handlers |
ItemCreated¶
- Payload: item_id, tenant_id, code, name, item_type, storage_unit, shelf_life_days (nullable), ean (nullable), is_system, tags[], group_id (nullable), created_by, created_at
- Business Meaning: A new item exists in the catalog and is available for use across the system
- Side Effects: Manufacturing creates ManufacturingItemExtension + auto-creates IngredientGroup for ingredient/intermediate types; Warehouse creates WarehouseItemExtension (skipped for service type); CRM creates CRMItemExtension
- System items: Emitted per active tenant at GrinSystem provisioning time; consumers react identically to tenant-created items
ItemDeactivated¶
- Payload: item_id, tenant_id, code, item_type, deactivated_by, deactivated_at
- Business Meaning: Item is no longer available for use in new business documents
- Side Effects: Manufacturing warns if item is in a Draft recipe BOM; CRM sets orderable = false
ItemReactivated¶
- Payload: item_id, tenant_id, code, item_type, reactivated_by, reactivated_at
- Business Meaning: Item is restored to active status and available for use again
- Side Effects: CRM restores orderable based on its own CRMItemExtension rules
ItemUpdated¶
- Payload: item_id, tenant_id, name, description, tags[], shelf_life_days (nullable), old_ean (nullable), new_ean (nullable), updated_by, updated_at
- Business Meaning: One or more editable fields on the item have changed. Always carries the full current values of all editable fields, plus old_ean/new_ean to allow consumers to detect and react to EAN changes specifically.
- Side Effects: Warehouse updates cached shelf_life_days in WarehouseItemExtension; if old_ean ≠ new_ean, Warehouse also updates its EAN scan lookup index (removes old_ean entry, adds new_ean entry). CRM refreshes its display name.
- Emission rule: Only emitted when at least one field value actually changed. No-op EditItem calls (where all submitted values equal stored values) produce no event.
8. Failure Paths¶
| Failure Scenario | Detection | Recovery Strategy | User Impact |
|---|---|---|---|
| Duplicate item code on create | Repo uniqueness check (case-insensitive, covers system codes) | User corrects code | 409 — "Code '{code}' is already in use" |
| Edit immutable field (code, storage_unit, item_type) | Application-layer validation | User corrects request | 400 — field-level error |
| Deactivation: non-zero stock | In-process query to Warehouse | Admin clears stock first | 409 — "Item has stock remaining. Clear all stock before deactivating." |
| Deactivation: open ZPs | In-process query to Manufacturing | Complete or cancel open ZPs | 409 — "Item is referenced in {n} open production order(s)." |
| Deactivation: open ZSs | In-process query to CRM | Complete or cancel open ZSs | 409 — "Item is referenced in {n} open sales order(s)." |
| Deactivation: in Published BOM | In-process query to Manufacturing | Remove from Published recipes first | 409 — "Item is used in {n} published recipe(s)." |
| Edit or deactivate system item | is_system check at handler entry | — | 403 — "System items are read-only." |
| EAN duplicate on create or edit | Repo uniqueness check | User provides unique EAN or clears field | 409 — "EAN '{ean}' is already assigned to another item." |
| shelf_life_days on non-applicable type | Type restriction validation | User removes field or corrects item type | 400 — field-level error |
| Group item_type mismatch on assign | group.item_type ≠ item.item_type check | User selects correct group | 409 — "Group is for {group_type} items, not {item_type} items." |
| Delete group with members | Member count check | Reassign or remove all members first | 409 — "Group has {n} item(s) assigned." |
| Import: any row error | Full-pass validation before save | User fixes all errors and re-submits | 400 — [{row, field, message}] listing every error |
| Import: file too large or too many rows | Pre-validation check | User splits import | 400 — size / row limit message |
9. External Integrations¶
None. ItemCatalog is the source of truth; it does not consume data from external systems. It publishes events through the transactional outbox (dispatched in-process to sibling modules) and accepts in-process queries from sibling modules during deactivation checks (internal monolith only).
Architectural note on deactivation cross-module checks: ItemCatalog calls Warehouse, Manufacturing, and CRM query handlers directly in-process during DeactivateItem. This is an explicit, documented exception to the events-only inter-module communication rule, justified by the synchronous guard requirement (a user must receive an immediate response explaining why deactivation was blocked). This must not be extended beyond read-only query calls.
10. Open Questions¶
| # | Question | Impact |
|---|---|---|
| N2 | No formal latency SLA defined for ListItems search (diacritic-insensitive full-text). Requires PostgreSQL unaccent extension or equivalent. Suggest < 500 ms as a working target. |
Performance planning |
| N3 | Confirm search scope for ListItems: should system items always be included in search results, or should there be an include_system filter option (analogous to ListItemGroups)? |
Query design |
11. Glossary¶
| Term | Definition |
|---|---|
| Item | A uniquely identified product, material, or service in the GrinSystem catalog. The source of truth for item identity shared across all modules. |
| item_type | Immutable classification set at creation: ingredient, product, intermediate, packaging, consumable, service, resale. Controls which modules may reference the item and in what capacity. |
| storage_unit | Immutable unit of measure set at creation: kg, g, l, ml, piece. All BOM quantities and stock entries use this unit; no automatic conversion exists. |
| ItemGroup | Tenant-managed organisational container for catalog items of the same item_type. Used for browsing and filtering. 0 or 1 group per item. |
| IngredientGroup | Manufacturing-owned substitution group (separate concept). Defines which ingredients may substitute for each other within a specific recipe BOM. Auto-created by Manufacturing on ItemCreated for ingredient/intermediate types. Not managed by ItemCatalog. |
| System item | A pre-populated catalog item provided by GrinSystem (approx. 200 common Polish food raw materials). Shared across all tenants, read-only for tenants. is_system = true. |
| EAN | GS1-standard barcode (max 20 chars). Optional, unique per tenant. Used by Warehouse for scan-based item lookup and by CRM for customer-facing labels. |
| shelf_life_days | Optional integer indicating how many days after production a LOT expires. Applicable only to product and intermediate types. Used by Warehouse to auto-calculate expiry dates on production output LOTs. |
| is_system | Boolean flag on Item indicating GrinSystem-managed records. Tenant actors cannot create, edit, or deactivate system records. ItemGroup has no equivalent flag — all groups are tenant-created. |
| Ungrouped | A virtual view (not a real group) showing items with no group assignment (group_id = null). |
| ZP | Zlecenie Produkcyjne — Production Order. Owned by Manufacturing. |
| ZS | Zlecenie Sprzedaży — Sales Order. Owned by CRM. |
| PZ | Przyjęcie Zewnętrzne — Goods Receipt document. Owned by Warehouse. |
12. Appendix: Source Documents¶
docs/business/modules/item_catalog.md— primary business requirements and use casesdocs/architecture/bounded-contexts.md— module ownership, event catalog, key rulesdocs/architecture/hld.md— system architecture, module structure, data flow patternsdocs/architecture/conventions.md— CQS pattern, naming, testing, API conventionsdocs/business/brd.md— product vision, target market, business constraints
Next Step: This analysis feeds into the technical-planning-docs-writer skill for implementation planning.