Item Module
Items describe the types of materials, work-in-progress, and finished goods that an organization uses in its operations. The module supports inventory management, purchasing, sales, and production.
Purpose
Section titled “Purpose”The Item module provides a Data Authority API for managing item reference data with full bitemporal support. Beyond the standard CRUDQ operations, it adds supply management, lookup services for typeahead fields, image upload, and printing of labels and breadcrumbs.
Integrations
Section titled “Integrations”- Business Affiliates Module (
reference.businessAffiliate): Resolves supplier references in item supply records to the VENDORBusinessRole, creates affiliates/roles on the fly (PROPAGATE), and emits the role notifications the Item module reacts to (vendor rename, supplier removal) - PDF Render Module (
shopAccess.pdfRender): Renders label and breadcrumb PDFs via Documint. See PDF Render Module - S3 Asset Service (
common-module): Manages CSV upload and image upload storage - Kanban Cards Module (
resources.kanban): Observes item changes viaDataAuthorityNotificationto keep card data in sync
Block Diagram
Section titled “Block Diagram”Entity Model
Section titled “Entity Model”Supplier Reference
Section titled “Supplier Reference”An ItemSupply (and the primarySupply / secondarySupply snapshots embedded on Item via ItemSupplyReference) links to its supplier through a single supplier: SupplierReference.Value descriptor. This canonical supplier descriptor targets the VENDOR BusinessRole (per the Business Affiliate module), replacing the legacy supplierEId: EntityId? + denormalized supplier: String pair, which has been removed.
SupplierReference.Value is a supplier descriptor rather than a strict entity reference:
| Field | Nullability | Meaning |
|---|---|---|
name | required | Single source of truth for the vendor name |
eId | nullable | The VENDOR BusinessRole entity id — null means unlinked (name-only / legacy) |
affiliateEId | nullable | The parent BusinessAffiliate entity id — required to resolve the child VENDOR role across the Universe boundary; null when unlinked |
rId | nullable | Pinned record id — null while floating, set to the supplier tombstone on retirement |
retired | Boolean | Denormalized copy of the referenced role’s retired state (the stale marker) |
provenance | nullable | Denormalized who/when of the most recent reaction stamp |
The smart constructor enforces the invariant that a non-null eId requires a non-null affiliateEId. A null eId/affiliateEId is an unlinked, name-only descriptor — the migration-window state that carries legacy supplies until they are lazily re-linked on edit. There is no SQL foreign key to the Business Affiliate Universe; the link is a soft, cross-Universe reference resolved through BusinessAffiliateService.
API Endpoints
Section titled “API Endpoints”The module base path is /v1/item. All routes except lookups require authentication.
Data Authority (CRUDQ)
Section titled “Data Authority (CRUDQ)”Inherited from EditableDataAuthorityEndpoint:
| Method | Path | Description |
|---|---|---|
POST | /item/add | Create item (or draft) |
PUT | /item/update | Update item |
GET | /item/{id} | Read item by record ID |
DELETE | /item/{id} | Logical delete (retire) |
POST | /item/query | Query with filters, sort, pagination |
GET | /item/query/{pageToken} | Navigate paginated results |
GET | /item/{id}/history | Entity version history |
Query parameter mutation-mode is retained for backward compatibility but accepted-and-ignored since 6.0.0 — the former STRICT and LAX values were removed, leaving PROPAGATE (canonical create-on-the-fly) as the sole reconciliation mode (PDEV-930, supersedes DQ-011). Every supply write — full-item create/update and the dedicated supply route alike — resolves its SupplierReference the same canonical way: find or create the BusinessAffiliate + VENDOR role by name, then link.
A new supply may never point at a retired supplier — PROPAGATE rejects a retired role rather than resurrecting it (DQ-012). An existing link to a now-retired role is preserved (marked stale), never reactivated. Embedded primarySupply / secondarySupply snapshots always resolve canonically so later supplier rename/retire reactions can find them.
Supply Management
Section titled “Supply Management”Supplies are exposed on their own canonical reference-data endpoint (item-supply/supply) keyed by the parent {item-eId}. These routes share the same ItemService handlers as the retained legacy aliases (DQ-009), so the two paths are behaviorally identical. Both columns below show full paths from the version segment — the canonical routes live on the reference-data domain base path, not under this module’s /v1/item base path; the legacy aliases live under /v1/item/item.
| Method | Canonical path | Legacy alias | Description |
|---|---|---|---|
POST | /v1/reference-data/item/item-supply/supply/{item-eId}/add | /v1/item/item/{parent-item-id}/supply | Create item supply |
GET | /v1/reference-data/item/item-supply/supply/{item-eId}/list | /v1/item/item/{parent-item-id}/supply | List item supplies |
PUT | /v1/reference-data/item/item-supply/supply/{item-eId}/{supply-id}/update | /v1/item/item/{parent-item-id}/supply/{item-supply-id} | Update item supply |
DELETE | /v1/reference-data/item/item-supply/supply/{item-eId}/{supply-id}/delete | /v1/item/item/{parent-item-id}/supply/{item-supply-id} | Remove item supply (clears any referencing slot first) |
The mutation-mode query parameter is accepted-and-ignored (see above). Removing a supply still referenced as primarySupply / secondarySupply no longer fails: the Item service clears the referencing slot (and a now-dangling defaultSupply) as part of the delete, so removal never strands a stale link (6.0.0, PDEV-930; was previously a conflict). See the API Endpoint Catalog for the consolidated route listing.
Printing
Section titled “Printing”| Method | Path | Request | Response | Description |
|---|---|---|---|---|
POST | /item/item-print-label | EntityIdsInput (list of item UUIDs) | CompositeRenderResult | Print labels for items |
POST | /item/item-print-breadcrumb | EntityIdsInput (list of item UUIDs) | CompositeRenderResult | Print breadcrumbs for items |
Both accept optional query parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
live-print | Boolean | false | Production rendering (true) vs test/preview with watermark (false) |
debug | Boolean | false | Include the Documint payload in the response (debugPayload field) |
dry-run | Boolean | false | Construct payload but skip Documint call. No side effects. Overrides live-print and debug. |
Image Upload
Section titled “Image Upload”| Method | Path | Request | Response | Description |
|---|---|---|---|---|
POST | /item/{eId}/image-upload-url | ImageUploadRequest | ImageUploadResponse | Generate presigned upload credentials |
Lookups (Unauthenticated)
Section titled “Lookups (Unauthenticated)”All lookups accept name (search term) and limit (max results) query parameters and return List<String> (or List<ItemLookupResult> for items).
| Method | Path | Description |
|---|---|---|
GET | /item/lookup-suppliers | Supplier names |
GET | /item/lookup-units | Quantity units |
GET | /item/lookup-items | Item names + EIDs |
GET | /item/lookup-types | Classification types |
GET | /item/lookup-subtypes | Classification subtypes |
GET | /item/lookup-usecases | Use cases |
GET | /item/lookup-facilities | Facilities |
GET | /item/lookup-departments | Departments |
GET | /item/lookup-locations | Locations |
GET | /item/lookup-sublocations | Sublocations |
The OpenAPI specification is available via Redocly at https://stage.alpha002.io.arda.cards/v1/item/docs/redoc/index.html#tag/v1.
Query Locator Fields
Section titled “Query Locator Fields”The following fields (case-insensitive) are accepted in query filters and sort parameters:
| Locator | Type | Description |
|---|---|---|
id | uuid | Record ID (bitemporal) |
effective_as_of | TIMESTAMP | Bitemporal effective timestamp |
recorded_as_of | TIMESTAMP | Bitemporal recorded timestamp |
eid | uuid | Entity UUID |
retired | BOOLEAN | Whether logically deleted |
tenant_id | uuid | Owning tenant |
item_name | String | Item name |
image_url | String (URL) | Product image URL |
classification_type | String | Item type |
classification_sub_type | String | Item sub-type |
use_case | String | Use case label |
physical_locator_facility | String | Storage facility |
physical_locator_department | String | Storage department |
physical_locator_location | String | Storage location |
internal_sku | String | Internal SKU |
notes | String | Free-form notes |
card_notes_default | String | Default card notes |
taxable | BOOLEAN | Taxable flag |
primary_supply_supplier_ref_name | String | Primary supplier name (from the supplier reference) |
primary_supply_sku | String | Primary supplier SKU |
primary_supply_order_method | String | Preferred order method |
primary_supply_url | String (URL) | Supplier product URL |
primary_supply_order_quantity_amount | Decimal | Order quantity amount |
primary_supply_order_quantity_unit | String | Order quantity unit |
primary_supply_unit_cost_value | Decimal | Unit cost |
primary_supply_unit_cost_currency | String | Currency |
primary_supply_average_lead_time_length | Integer | Lead time length |
primary_supply_average_lead_time_time_unit | String | Time unit |
secondary_supply_* | (same as primary) | Secondary supplier fields |
default_supply | String | Default supplier name |
default_supply_eid | uuid | Default supply UUID |
card_size | String | Card print size |
label_size | String | Label print size |
breadcrumb_size | String | Breadcrumb print size |
item_color | String | Display color |
Service Layer
Section titled “Service Layer”ItemService
Section titled “ItemService”ItemService implements three interfaces:
ItemService no longer owns an inner SupplyService; supply operations are delegated to the dedicated ItemSupplyService (see below). The dependency is now one-directional — Item → ItemSupply only: as of 6.0.0 the child service holds no reference back to ItemService (the former setParentService cycle was eliminated). The Item service subscribes to VENDOR BusinessRole events through ItemSupplyRoleListener and re-derives affected parents’ embedded projection through ItemVendorCascade; the child service simply returns the updated supplies and never calls back up. The supply methods listed below remain on the ItemService surface as thin pass-throughs so existing callers and the endpoint handlers are unaffected.
Item-specific methods beyond the Data Authority pattern:
| Method | Description |
|---|---|
supplies(parentEId, time) | List all supplies for an item (delegates to ItemSupplyService) |
createItemSupply(parentEId, supply, time, qualifier) | Add a supply source (delegates to ItemSupplyService) |
updateItemSupply(parentEId, supply, time, qualifier) | Update a supply source (delegates to ItemSupplyService) |
removeItemSupply(parentEId, supplyEId, time) | Remove a supply source (delegates to ItemSupplyService) |
lookupSupplierByName(name, n, asOf) | Typeahead for supplier names |
lookupUnit(name, n, asOf) | Typeahead for quantity units |
lookupItemByName(name, n, asOf) | Typeahead for item names (returns name + EID) |
lookupTypeByName(name, n, asOf) | Typeahead for classification types |
lookupSubTypeByName(name, n, asOf) | Typeahead for subtypes |
lookupUseCaseByName(name, n, asOf) | Typeahead for use cases |
lookupFacilityByName(name, n, asOf) | Typeahead for facilities |
lookupDepartmentByName(name, n, asOf) | Typeahead for departments |
lookupLocationByName(name, n, asOf) | Typeahead for locations |
lookupSublocationByName(name, n, asOf) | Typeahead for sublocations |
generateImageUploadCredentials(contentType, contentLength, author) | Presigned S3 POST for image upload |
The list path (list and the underlying listEntities query) accepts an includeRetired option so a consumer resolving references to possibly-deleted items can retrieve soft-deleted rows in a single batch instead of erroring on them — used by the kanban card list path when resolving believed-live entries (see Kanban Cards Module).
Business rules enforced by the service:
- Item name uniqueness within tenant scope
- Canonical supply reconciliation on every write: the supplier is resolved to its VENDOR role and the embedded
primarySupply/secondarySupplyprojection is re-derived from the authoritativeItemSupply(never persisted from the request); an irregular stored pre-state is healed toward canonical rather than propagated — see Canonical reconciliation & heal-on-write - Distinct slots (INV-A8): primary and secondary must reference distinct supplies; placing the same supply in both is rejected
- Stale-write rejection (Q12): a write whose effective basis a later version has superseded (e.g. a vendor-rename cascade) is rejected as a client conflict, never branched from the outdated view
- Image URL validation (CDN pattern, tenant isolation, HEAD verification)
ItemSupplyService
Section titled “ItemSupplyService”ItemSupplyService is the dedicated module element (under the service layer) that owns the ItemSupply child universe and all supplier resolution. As of 6.0.0 it is a pure child of ItemService — Item → ItemSupply is the only direction of dependency, and the child holds no reference back to its parent (the former two-phase setParentService(...) cycle was removed). Parent existence is enforced where the write happens (the supply universe’s parent lookup), and the request tenant comes from the authenticated scope rather than a value read off the parent Item. The reaction methods return the updated supplies; the Item service re-derives each affected parent’s embedded projection (via ItemVendorCascade) and owns the VENDOR-BusinessRole observer subscription (via ItemSupplyRoleListener, registered fail-fast at construction).
ItemSupplyService composes two delegate implementations and a cross-parent query universe:
| Collaborator | Responsibility |
|---|---|
ItemSupplyCrudService | Per-parent create / update / remove / list of supplies, with canonical supplier resolution. Writes only supply rows — re-deriving the parent Item’s embedded projection is the Item service’s job (no call back up). |
ItemSupplyReactionService | Vendor-event reactions: update the linking ItemSupply rows on a VENDOR rename, mark them stale on a removal. Returns the updated supplies for the Item service to project; never writes the parent. |
CrossItemSupplyUniverse | Cross-parent supply queries (e.g. “all supplies referencing a given VENDOR role”), tenant-scoped at the query level |
CrossItemSupplyUniverse is needed because item_supply is a ChildTable with no tenant_id column — its tenant scope is inherited from the parent Item. Rather than reading parents N+1, the cross universe composes the parent Item universe’s tenant condition into the supply query as a correlated sub-query, so tenant filtering happens in the database. It also resolves a supply’s parent Item eId when only the supply eId is known.
Vendor resolution (canonical, PROPAGATE-only since 6.0.0): every supply resolves create-on-the-fly — find or create the BusinessAffiliate + VENDOR role by name, then link. The former STRICT and LAX modes were removed; mutation-mode is accepted-and-ignored. The same vendor name reuses the same role (find-or-create dedup — no duplicate affiliates/roles); a retired supplier is rejected, never resurrected (DQ-012). Embedded primary/secondary supplies resolve the same way.
Supplier-removal reaction (stale, don’t delete): when a VENDOR BusinessRole is deleted, ItemSupplyService (via ItemSupplyReactionService) observes the notification, finds every referencing supply through CrossItemSupplyUniverse, and marks each one stale rather than deleting it — retired = true, the reference pinned (rId) to the supplier tombstone, and a provenance stamp recorded. It returns those updated supplies, and the Item service (via ItemVendorCascade) re-derives each affected parent’s embedded primarySupply / secondarySupply projection — the child never writes the parent. This mirrors the Kanban-card-reacts-to-Item-deletion pattern (PDEV-808): the supply data is preserved so pending workflows already committed to those values still resolve, and a retired+pinned reference reads back from the supplier tombstone. The vendor-rename and retirement reaction paths deliberately bypass resolveVendorRef (using a direct updateFixed) to avoid cascading another BusinessAffiliate update and looping.
| Method | Description |
|---|---|
supplies(parentEId, time) | List all supplies for an item |
createItemSupply(parentEId, supply, time, qualifier) | Add a supply source; resolves the supplier canonically (find-or-create). The parent existence is enforced by the supply universe before vendor resolution, so a missing parent never creates an orphan vendor (PDEV-930). |
updateItemSupply(parentEId, supply, time, qualifier) | Update a supply source; the Item service then syncs the parent projection. Both steps run in one transaction, writing a single Item version (PDEV-944). |
removeItemSupply(parentEId, supplyEId, time) | Remove a supply. The Item service clears any referencing slot first, so removal never strands a stale link. |
propagateVendorNameChange(...) | React to a VENDOR rename across referencing supplies (returns updated supplies) |
markSupplierRetired(...) | React to a VENDOR removal by marking referencing supplies stale (returns updated supplies) |
Canonical reconciliation & heal-on-write
Section titled “Canonical reconciliation & heal-on-write”Since 6.0.0 (PDEV-930) every Item write — full-item create/update and the dedicated supply route — drives the data toward a single canonical shape rather than persisting the request verbatim. Two ideas underpin it.
Re-derive, don’t persist, the projection. The embedded primarySupply / secondarySupply (and defaultSupplyEId) are a projection of the authoritative ItemSupply rows. On every write the supplies are resolved/linked first, then the projection is re-derived from them — so the snapshot can never drift from the rows it mirrors. The full-item path and the supply route share the same resolution.
Heal-on-write. A write that meets an irregular stored pre-state heals it toward canonical form instead of propagating it:
- a stripped or dangling supply link is re-linked, or reconciled by name when the link is absent;
- a stale embedded projection is re-derived from the authoritative supply;
- an empty-named supplier adopts its business affiliate’s name (or the supply’s own name);
- an empty or stale
defaultSupplyclears to null; - a supply whose vendor role has been retired stays linked without reactivating the role.
Pre-existing duplicate supplies are tolerated. Invariant: data is never persisted less canonical than it was read; each healing action records a system note on the affected bitemporal row (appended to any caller-supplied mutation note).
Guards that still reject (canonical-or-abort, no degradation):
- Distinct slots (INV-A8): primary and secondary must reference distinct supplies — the same
supplyEIdor the same effective supply name in both slots is rejected. - Stale write (Q12): when the latest-effective head is newer than the write’s effective basis (e.g. a vendor-rename cascade wrote a later, future-dated successor), the write is rejected as a client conflict (HTTP 409) before any mutation — the head is never reverted.
Printed SUPPLIER (PDEV-928): an empty defaultSupply is treated as unset and an empty-named supplier is never chosen for printing, so the preferred supplier resolves to a named supply — fixing the previously blank SUPPLIER field.
ItemPrintingService
Section titled “ItemPrintingService”The printing sub-interface manages template resolution and print job orchestration. It is implemented within ItemService.Impl.
Template resolution methods resolve a size enum to a PrintingTemplateConfiguration (template ID, columns, description). Each falls back to the default size if the requested size is not configured.
Print methods (printLabels, printBreadcrumbs) orchestrate the full printing flow:
- Deduplicate and validate the input ID list
- Fetch items from
ItemUniverseby record IDs - For each item, resolve the template via the size field and render the item to a
ItemPrintInfoJSON payload viaItemPrinter - Verify all items resolve to the same template (current constraint)
- Construct a
RenderJobwith the template configuration and aGridof item payloads - Delegate to
PdfRenderService.render()and return theRenderResult
Deleted-item marking (PDEV-808). ItemPrintInfo carries is_retired, last_updated_by, and last_updated_at, populated by ItemPrinter.render from the resolved item record (itemRd.retired / author / asOf), so a deleted item’s Label and Breadcrumb render with a deleted marker rather than failing. No resolution change was needed: ItemPrintingService fetches print targets by record id (step 2), which returns a retired row regardless of its deletion state. The last_updated_at value is formatted as an ISO-8601 UTC date by a shared helper also used by the kanban card print (KanbanCardPrintInfo — see Kanban Cards Module).
Printing Sequence Diagrams
Section titled “Printing Sequence Diagrams”Print Labels
Section titled “Print Labels”Print Breadcrumbs
Section titled “Print Breadcrumbs”The breadcrumb flow is identical to labels, substituting breadcrumbSize for labelSize in template resolution and POST /item/item-print-breadcrumb as the endpoint path.
Print Labels — Mixed Sizes (Multi-Template Grouping)
Section titled “Print Labels — Mixed Sizes (Multi-Template Grouping)”When items have different label sizes, the system groups them by template and renders one PDF per group:
Persistence Layer
Section titled “Persistence Layer”ItemUniverse
Section titled “ItemUniverse”ItemUniverse extends AbstractScopedUniverse<Item, ItemMetadata, ITEM_TABLE, ItemRecord> and provides:
- Standard bitemporal CRUDQ operations (inherited)
- Tenant-scoped queries via
ItemUniversalCondition - Business rule validation via
ItemValidator - Fuzzy-match lookup methods for typeahead fields
ItemSupplyUniverse and CrossItemSupplyUniverse
Section titled “ItemSupplyUniverse and CrossItemSupplyUniverse”ItemSupplyUniverse is the per-parent child universe over ITEM_SUPPLY_TABLE, constructed for a given parent Item eId via a factory and owned by ItemSupplyService. CrossItemSupplyUniverse complements it with cross-parent queries that a parent-scoped universe cannot serve: finding every supply that references a given VENDOR role, and resolving a supply’s parent eId from the supply eId alone. Because item_supply has no tenant_id column, CrossItemSupplyUniverse enforces tenant scope at the query level — it composes the parent Item universe’s ItemUniversalCondition into the supply query as a correlated sub-query, returning a composable DBIO action the service runs in its own transaction.
Database Tables
Section titled “Database Tables”| Table | Type | Description |
|---|---|---|
ITEM_TABLE (item_bitemporal) | ScopedTable | Main item entity with bitemporal versioning |
ITEM_SUPPLY_TABLE (item_supply_bitemporal) | ChildTable (parent: ITEM) | Item supply sources |
Composite Persistence Components
Section titled “Composite Persistence Components”The item table uses composite components to map structured value objects to flat database columns:
| Component | Maps To | Columns |
|---|---|---|
ItemClassificationComponent | ItemClassification | type, sub_type, use_case, gl_code |
PhysicalLocatorComponent | PhysicalLocator | facility, department, location, sub_location |
QuantityComponent | Quantity (minQuantity) | amount, unit |
ItemSupplyReferenceComponent | ItemSupplyReference (primary, secondary) | supply_eid, name, sku, order_method, url, order_quantity_, unit_cost_, lead_time_*, plus the embedded SupplierReferenceComponent columns |
SupplierReferenceComponent | SupplierReference.Value (within each supply and supply reference) | supplier_ref_name, supplier_ref_entity_id, supplier_ref_affiliate_eid, supplier_ref_record_id, supplier_ref_retired, supplier_ref_provenance_updated_by, supplier_ref_provenance_updated_at |
Bitemporal features: effective time, recorded time, entity history, time-travel queries, logical deletion (retired flag).
Configuration
Section titled “Configuration”The module is configured at path system.reference.item and reads:
Module Extras
Section titled “Module Extras”| Key | Type | Description |
|---|---|---|
colors.optionsPath | String | Path to color-options.json resource |
colors.basePath | String | Path to colors.json resource |
frontend.baseUrl | String | Base URL for QR code generation |
printTemplatesPath | String | Path to pdf-templates.json resource |
Dependencies Injected at Initialization
Section titled “Dependencies Injected at Initialization”| Dependency | Source | Description |
|---|---|---|
PdfRenderService | shopAccess.pdfRender module | PDF rendering via Documint |
BusinessAffiliateService | reference.businessAffiliate module | Supplier reference resolution |
Authentication | Component configuration | Request authentication |
AwsConfiguration | Component configuration | AWS region, S3 bucket ARNs, presign role ARNs, CDN domain |
Resources
Section titled “Resources”| Resource | Purpose |
|---|---|
reference/item/printing/pdf-templates.json | Template ID, columns, description, active flag per size per material type |
reference/item/printing/color-options.json | Available item color values |
reference/item/printing/colors.json | Color asset URLs (rectangles, icons) per color value |
Item Lookup URL (QR Code)
Section titled “Item Lookup URL (QR Code)”Printed labels and breadcrumbs embed a QR code that encodes a URL for looking up the item in the UI:
https://{base-url}/item/{eId}/{type}{type} | Material |
|---|---|
0 | Label |
1 | Breadcrumb |
Base URLs by Environment
Section titled “Base URLs by Environment”| Environment | Base URL |
|---|---|
| Production | live.app.arda.cards |
| Staging | stage.alpha002.app.arda.cards |
| Development | dev.alpha002.app.arda.cards |
The base URL is configured via frontend.baseUrl in the module extras. The eId is the item’s entity ID (available from the Item API at results[*].payload.eId).
When the browser navigates to an item lookup URL, the UI displays the item details for the given eId. If the item is not found, the UI displays an “Item Not Found” message (item never existed, user lacks access, or item was deleted).
Copyright: © Arda Systems 2025-2026, All rights reserved