Skip to content

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.

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.

  • Business Affiliates Module (reference.businessAffiliate): Resolves supplier references in item supply records to the VENDOR BusinessRole, 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 via DataAuthorityNotification to keep card data in sync

PlantUML diagram

PlantUML diagram

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:

FieldNullabilityMeaning
namerequiredSingle source of truth for the vendor name
eIdnullableThe VENDOR BusinessRole entity id — null means unlinked (name-only / legacy)
affiliateEIdnullableThe parent BusinessAffiliate entity id — required to resolve the child VENDOR role across the Universe boundary; null when unlinked
rIdnullablePinned record id — null while floating, set to the supplier tombstone on retirement
retiredBooleanDenormalized copy of the referenced role’s retired state (the stale marker)
provenancenullableDenormalized 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.

The module base path is /v1/item. All routes except lookups require authentication.

Inherited from EditableDataAuthorityEndpoint:

MethodPathDescription
POST/item/addCreate item (or draft)
PUT/item/updateUpdate item
GET/item/{id}Read item by record ID
DELETE/item/{id}Logical delete (retire)
POST/item/queryQuery with filters, sort, pagination
GET/item/query/{pageToken}Navigate paginated results
GET/item/{id}/historyEntity 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.

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.

MethodCanonical pathLegacy aliasDescription
POST/v1/reference-data/item/item-supply/supply/{item-eId}/add/v1/item/item/{parent-item-id}/supplyCreate item supply
GET/v1/reference-data/item/item-supply/supply/{item-eId}/list/v1/item/item/{parent-item-id}/supplyList 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.

MethodPathRequestResponseDescription
POST/item/item-print-labelEntityIdsInput (list of item UUIDs)CompositeRenderResultPrint labels for items
POST/item/item-print-breadcrumbEntityIdsInput (list of item UUIDs)CompositeRenderResultPrint breadcrumbs for items

Both accept optional query parameters:

ParameterTypeDefaultDescription
live-printBooleanfalseProduction rendering (true) vs test/preview with watermark (false)
debugBooleanfalseInclude the Documint payload in the response (debugPayload field)
dry-runBooleanfalseConstruct payload but skip Documint call. No side effects. Overrides live-print and debug.
MethodPathRequestResponseDescription
POST/item/{eId}/image-upload-urlImageUploadRequestImageUploadResponseGenerate presigned upload credentials

All lookups accept name (search term) and limit (max results) query parameters and return List<String> (or List<ItemLookupResult> for items).

MethodPathDescription
GET/item/lookup-suppliersSupplier names
GET/item/lookup-unitsQuantity units
GET/item/lookup-itemsItem names + EIDs
GET/item/lookup-typesClassification types
GET/item/lookup-subtypesClassification subtypes
GET/item/lookup-usecasesUse cases
GET/item/lookup-facilitiesFacilities
GET/item/lookup-departmentsDepartments
GET/item/lookup-locationsLocations
GET/item/lookup-sublocationsSublocations

The OpenAPI specification is available via Redocly at https://stage.alpha002.io.arda.cards/v1/item/docs/redoc/index.html#tag/v1.

The following fields (case-insensitive) are accepted in query filters and sort parameters:

LocatorTypeDescription
iduuidRecord ID (bitemporal)
effective_as_ofTIMESTAMPBitemporal effective timestamp
recorded_as_ofTIMESTAMPBitemporal recorded timestamp
eiduuidEntity UUID
retiredBOOLEANWhether logically deleted
tenant_iduuidOwning tenant
item_nameStringItem name
image_urlString (URL)Product image URL
classification_typeStringItem type
classification_sub_typeStringItem sub-type
use_caseStringUse case label
physical_locator_facilityStringStorage facility
physical_locator_departmentStringStorage department
physical_locator_locationStringStorage location
internal_skuStringInternal SKU
notesStringFree-form notes
card_notes_defaultStringDefault card notes
taxableBOOLEANTaxable flag
primary_supply_supplier_ref_nameStringPrimary supplier name (from the supplier reference)
primary_supply_skuStringPrimary supplier SKU
primary_supply_order_methodStringPreferred order method
primary_supply_urlString (URL)Supplier product URL
primary_supply_order_quantity_amountDecimalOrder quantity amount
primary_supply_order_quantity_unitStringOrder quantity unit
primary_supply_unit_cost_valueDecimalUnit cost
primary_supply_unit_cost_currencyStringCurrency
primary_supply_average_lead_time_lengthIntegerLead time length
primary_supply_average_lead_time_time_unitStringTime unit
secondary_supply_*(same as primary)Secondary supplier fields
default_supplyStringDefault supplier name
default_supply_eiduuidDefault supply UUID
card_sizeStringCard print size
label_sizeStringLabel print size
breadcrumb_sizeStringBreadcrumb print size
item_colorStringDisplay color

ItemService implements three interfaces:

PlantUML diagram

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:

MethodDescription
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/secondarySupply projection is re-derived from the authoritative ItemSupply (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 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:

CollaboratorResponsibility
ItemSupplyCrudServicePer-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).
ItemSupplyReactionServiceVendor-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.
CrossItemSupplyUniverseCross-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.

MethodDescription
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)

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 defaultSupply clears 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 supplyEId or 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.

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:

  1. Deduplicate and validate the input ID list
  2. Fetch items from ItemUniverse by record IDs
  3. For each item, resolve the template via the size field and render the item to a ItemPrintInfo JSON payload via ItemPrinter
  4. Verify all items resolve to the same template (current constraint)
  5. Construct a RenderJob with the template configuration and a Grid of item payloads
  6. Delegate to PdfRenderService.render() and return the RenderResult

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).

PlantUML diagram

The breadcrumb flow is identical to labels, substituting breadcrumbSize for labelSize in template resolution and POST /item/item-print-breadcrumb as the endpoint path.

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:

PlantUML diagram

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.

TableTypeDescription
ITEM_TABLE (item_bitemporal)ScopedTableMain item entity with bitemporal versioning
ITEM_SUPPLY_TABLE (item_supply_bitemporal)ChildTable (parent: ITEM)Item supply sources

The item table uses composite components to map structured value objects to flat database columns:

ComponentMaps ToColumns
ItemClassificationComponentItemClassificationtype, sub_type, use_case, gl_code
PhysicalLocatorComponentPhysicalLocatorfacility, department, location, sub_location
QuantityComponentQuantity (minQuantity)amount, unit
ItemSupplyReferenceComponentItemSupplyReference (primary, secondary)supply_eid, name, sku, order_method, url, order_quantity_, unit_cost_, lead_time_*, plus the embedded SupplierReferenceComponent columns
SupplierReferenceComponentSupplierReference.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).

The module is configured at path system.reference.item and reads:

KeyTypeDescription
colors.optionsPathStringPath to color-options.json resource
colors.basePathStringPath to colors.json resource
frontend.baseUrlStringBase URL for QR code generation
printTemplatesPathStringPath to pdf-templates.json resource
DependencySourceDescription
PdfRenderServiceshopAccess.pdfRender modulePDF rendering via Documint
BusinessAffiliateServicereference.businessAffiliate moduleSupplier reference resolution
AuthenticationComponent configurationRequest authentication
AwsConfigurationComponent configurationAWS region, S3 bucket ARNs, presign role ARNs, CDN domain
ResourcePurpose
reference/item/printing/pdf-templates.jsonTemplate ID, columns, description, active flag per size per material type
reference/item/printing/color-options.jsonAvailable item color values
reference/item/printing/colors.jsonColor asset URLs (rectangles, icons) per color value

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
0Label
1Breadcrumb
EnvironmentBase URL
Productionlive.app.arda.cards
Stagingstage.alpha002.app.arda.cards
Developmentdev.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).