Events: Item Catalog¶
Module: item_catalog Last Updated: 2026-05-10 Generated From: analysis.md
Event Routing¶
Events are published through the transactional outbox and dispatched in-process by the OutboxPoller / EventDispatcher to the @on_event handlers registered for each event type — there are no Kafka topics or partitions. The event_type field inside the envelope is the routing key: a subscribing module registers @on_event(ItemCreated) (etc.) in its application/event_handlers.py. Kafka is deferred until a module is extracted to its own service, at which point these events move onto a grinsystem.item_catalog.events topic with no change to handler signatures. See conventions.md § Event Handlers and the outbox dispatcher runbook.
Produced Events¶
ItemCatalog is a source-of-truth module — it produces events and does not consume any.
| Event | Producing Use Case |
|---|---|
ItemCreated |
UC-001, UC-007 (one per row) |
ItemUpdated |
UC-002 (only when fields changed) |
ItemDeactivated |
UC-003 |
ItemReactivated |
UC-004 |
Event Envelope¶
All events share the same outer envelope structure:
{
"event_id": "<UUID>",
"event_type": "<string>",
"aggregate_id": "<UUID>",
"aggregate_type": "Item",
"tenant_id": "<UUID>",
"timestamp": "<ISO-8601>",
"version": 1,
"payload": { ... },
"metadata": {
"correlation_id": "<UUID>",
"causation_id": "<UUID>"
}
}
Event Schemas¶
ItemCreated¶
Produced by: Item.create() — triggered by UC-001 (CreateItem) and UC-007 (ImportItems, one event per row).
Consumers:
- Manufacturing: creates ManufacturingItemExtension; for ingredient and intermediate types, also auto-creates an IngredientGroup for substitution grouping.
- Warehouse: creates WarehouseItemExtension; skipped for service type.
- CRM: creates CRMItemExtension.
{
"event_id": "550e8400-e29b-41d4-a716-446655440001",
"event_type": "ItemCreated",
"aggregate_id": "22222222-2222-2222-2222-222222222222",
"aggregate_type": "Item",
"tenant_id": "11111111-1111-1111-1111-111111111111",
"timestamp": "2026-05-10T09:00:00Z",
"version": 1,
"payload": {
"item_id": "22222222-2222-2222-2222-222222222222",
"code": "MAK-001",
"name": "Mąka pszenna typ 500",
"item_type": "ingredient",
"storage_unit": "kg",
"shelf_life_days": null,
"ean": "5901234123457",
"is_system": false,
"tags": ["mąka", "pszenna"],
"group_id": "11111111-1111-1111-1111-111111111111",
"created_by": "33333333-3333-3333-3333-333333333333",
"created_at": "2026-05-10T09:00:00Z"
},
"metadata": {
"correlation_id": "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa",
"causation_id": "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
}
}
Payload field reference:
| Field | Type | Nullable | Description |
|---|---|---|---|
item_id |
UUID | No | Item identifier |
code |
string | No | Unique item code |
name |
string | No | Display name |
item_type |
string (enum) | No | ingredient | product | intermediate | packaging | consumable | service | resale |
storage_unit |
string (enum) | No | kg | g | l | ml | piece |
shelf_life_days |
integer | Yes | Product/intermediate only |
ean |
string | Yes | GS1 barcode |
is_system |
boolean | No | GrinSystem-provided item |
tags |
string[] | No | Tag list (empty array if none) |
group_id |
UUID | Yes | ItemGroup assignment at creation time |
created_by |
UUID | No | Creating user |
created_at |
ISO-8601 string | No | Creation timestamp |
System items: also emitted at provisioning time (one event per active tenant per system item). Consumers treat these identically to tenant-created items.
ItemUpdated¶
Produced by: Item.edit() — triggered by UC-002 (EditItem), only when at least one editable field value changed. No-op edits produce no event.
Consumers:
- Warehouse: updates cached shelf_life_days in WarehouseItemExtension; if old_ean ≠ new_ean, also updates the EAN scan lookup index (removes old_ean entry, adds new_ean entry).
- CRM: refreshes display name.
{
"event_id": "550e8400-e29b-41d4-a716-446655440002",
"event_type": "ItemUpdated",
"aggregate_id": "22222222-2222-2222-2222-222222222222",
"aggregate_type": "Item",
"tenant_id": "11111111-1111-1111-1111-111111111111",
"timestamp": "2026-05-10T14:30:00Z",
"version": 1,
"payload": {
"item_id": "22222222-2222-2222-2222-222222222222",
"name": "Mąka pszenna typ 650",
"description": "Zaktualizowany opis.",
"tags": ["mąka", "pszenna", "chlebowa"],
"shelf_life_days": null,
"old_ean": "5901234123457",
"new_ean": "5901234123464",
"updated_by": "44444444-4444-4444-4444-444444444444",
"updated_at": "2026-05-10T14:30:00Z"
},
"metadata": {
"correlation_id": "cccccccc-cccc-cccc-cccc-cccccccccccc",
"causation_id": "dddddddd-dddd-dddd-dddd-dddddddddddd"
}
}
Payload field reference:
| Field | Type | Nullable | Description |
|---|---|---|---|
item_id |
UUID | No | Item identifier |
name |
string | No | Full current value (not just the changed portion) |
description |
string | Yes | Full current value |
tags |
string[] | No | Full current tag list |
shelf_life_days |
integer | Yes | Full current value |
old_ean |
string | Yes | EAN before the edit; null if no EAN was set |
new_ean |
string | Yes | EAN after the edit; null if EAN was cleared |
updated_by |
UUID | No | Editing user |
updated_at |
ISO-8601 string | No | Edit timestamp |
EAN change detection: consumers check old_ean ≠ new_ean to determine whether the EAN scan index needs updating. Both are always present in the payload regardless of whether EAN changed.
ItemDeactivated¶
Produced by: Item.deactivate() — triggered by UC-003 (DeactivateItem).
Consumers:
- Manufacturing: warns if item appears in a Draft recipe BOM (informational only — deactivation is already complete at this point).
- Warehouse: marks item as unavailable for new incoming stock operations.
- CRM: sets orderable = false on CRMItemExtension.
{
"event_id": "550e8400-e29b-41d4-a716-446655440003",
"event_type": "ItemDeactivated",
"aggregate_id": "22222222-2222-2222-2222-222222222222",
"aggregate_type": "Item",
"tenant_id": "11111111-1111-1111-1111-111111111111",
"timestamp": "2026-05-10T15:00:00Z",
"version": 1,
"payload": {
"item_id": "22222222-2222-2222-2222-222222222222",
"code": "MAK-001",
"item_type": "ingredient",
"deactivated_by": "33333333-3333-3333-3333-333333333333",
"deactivated_at": "2026-05-10T15:00:00Z"
},
"metadata": {
"correlation_id": "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee",
"causation_id": "ffffffff-ffff-ffff-ffff-ffffffffffff"
}
}
Payload field reference:
| Field | Type | Nullable | Description |
|---|---|---|---|
item_id |
UUID | No | Item identifier |
code |
string | No | Item code (included so consumers can log/display without a separate lookup) |
item_type |
string (enum) | No | Allows consumers to apply type-specific deactivation logic |
deactivated_by |
UUID | No | Admin who performed the deactivation |
deactivated_at |
ISO-8601 string | No | Deactivation timestamp |
ItemReactivated¶
Produced by: Item.reactivate() — triggered by UC-004 (ReactivateItem).
Consumers:
- CRM: restores orderable flag on CRMItemExtension based on CRM's own rules.
{
"event_id": "550e8400-e29b-41d4-a716-446655440004",
"event_type": "ItemReactivated",
"aggregate_id": "22222222-2222-2222-2222-222222222222",
"aggregate_type": "Item",
"tenant_id": "11111111-1111-1111-1111-111111111111",
"timestamp": "2026-05-10T16:00:00Z",
"version": 1,
"payload": {
"item_id": "22222222-2222-2222-2222-222222222222",
"code": "MAK-001",
"item_type": "ingredient",
"reactivated_by": "33333333-3333-3333-3333-333333333333",
"reactivated_at": "2026-05-10T16:00:00Z"
},
"metadata": {
"correlation_id": "gggggggg-gggg-gggg-gggg-gggggggggggg",
"causation_id": "hhhhhhhh-hhhh-hhhh-hhhh-hhhhhhhhhhhh"
}
}
Payload field reference:
| Field | Type | Nullable | Description |
|---|---|---|---|
item_id |
UUID | No | Item identifier |
code |
string | No | Item code |
item_type |
string (enum) | No | Allows type-specific reactivation logic |
reactivated_by |
UUID | No | Admin who performed the reactivation |
reactivated_at |
ISO-8601 string | No | Reactivation timestamp |
Consumed Events¶
ItemCatalog registers no @on_event handlers. It is the source of truth for item identity. The only inbound data flows are HTTP requests from human actors and in-process query calls from sibling modules during deactivation checks (see domain-model.md — Cross-Module Query Ports).
Subscribers (downstream modules)¶
These handlers are owned by the subscribing modules, documented here for cross-reference. Each registers @on_event handlers in its application/event_handlers.py.
| Module | Events Subscribed | Handler Purpose |
|---|---|---|
| Manufacturing | ItemCreated, ItemDeactivated |
Create/update ManufacturingItemExtension; auto-create IngredientGroup for ingredient/intermediate |
| Warehouse | ItemCreated, ItemUpdated, ItemDeactivated |
Create/update WarehouseItemExtension; update EAN scan index |
| CRM | ItemCreated, ItemUpdated, ItemDeactivated, ItemReactivated |
Create/update CRMItemExtension; manage orderable flag |
Idempotency¶
Dispatch is at-least-once, so each handler must be idempotent using event_id:
- Before processing, check if
event_idexists in a localprocessed_eventstable. - If found: skip (no duplicate processing).
- If not found: process and record
event_idwithin the same database transaction as the side effect.
This guards against duplicate delivery (a row re-dispatched after a partial failure). See conventions.md § Handler-side idempotency.
Schema Versioning¶
All events carry "version": 1. When the payload schema must change in a breaking way:
- Increment
versionto2. - Maintain
version: 1support in all consumers during the transition window. - The
event_typeremains unchanged — handlers dispatch onevent_typethen branch onversionfor deserialization.