Skip to content

Item: Data Model

The Item module stores a material type’s full descriptor — classification, physical attributes, print sizing, primary/secondary supply, manufacturer, identifiers — in item, with each of an item’s possible supply sources broken out into child rows in item_supply. Four supporting tables (batch_job, item_draft, tenant_settings, user_settings) round out the module but aren’t part of the Item entity itself. This page is the field-complete reference for all six; see Data Models — Reading Guide for the shared envelope shapes referenced throughout, and for why this module predates the occurrence pattern.

item_supply is a same-module child table (parent_eid, no FK — cross-Universe rule). item’s own primary_supply_*/secondary_supply_* clusters are a re-derived projection of the authoritative item_supply rows, refreshed on every write rather than being client-authoritative. Both item and item_supply hold floating references into Business Affiliate — manufacturer and supplier respectively.

PlantUML diagram

ColumnTypeNullRole
idUUIDNOT NULL (PK)identity
effective_as_oftimestampNOT NULLidentity
recorded_as_oftimestampNOT NULL, default now()identity
bts_authorvarchar(244)NOT NULLbookkeeping
eidUUIDNOT NULLidentity
previousUUIDNULL, FK → item.ididentity
retiredbooleanNOT NULL, default falsebookkeeping
tenant_idUUIDNOT NULLidentity
bts_author_sub, bts_note, bts_created_by_subvarcharNULLbookkeeping
created_by, created_at_effective, created_at_recordedvarchar / timestampNOT NULLbookkeeping
item_namevarchar(255)NOT NULLpayload
descriptionvarchar(8192)NULLpayload
image_urlvarchar(8192)NULLpayload
classification_type, classification_sub_typevarchar(255)NULLpayload
use_casevarchar(255)NULLpayload
gl_codevarchar(255)NULLpayload
physical_locator_facility/department/location/sub_locationvarchar(255)NULLpayload
internal_skuvarchar(255)NULLpayload
min_quantity_amount / min_quantity_unitdouble / varchar(255)NULLpayload
notesvarchar(8192)NULLpayload
card_notes_defaultvarchar(8192)NULLpayload
taxablebooleanNULLpayload
primary_supply_supply_eidUUIDNULLpayload — pointer into item_supply
primary_supply_supplier_ref_entity_id / _affiliate_eidUUIDNULLpayload
primary_supply_supplier_ref_namevarchar(255)NULLbookkeeping — value-snapshot
primary_supply_supplier_ref_record_idUUIDNULLbookkeeping — version-pin
primary_supply_supplier_ref_retiredbooleanNOT NULL, default falsebookkeeping
primary_supply_supplier_ref_provenance_updated_by/_atvarchar / bigintNULLbookkeeping
primary_supply_name, primary_supply_sku, primary_supply_order_method, primary_supply_urlvarcharNULLbookkeeping — re-derived from item_supply every write
primary_supply_order_quantity_amount/_unit, primary_supply_unit_cost_value/_currency, primary_supply_average_lead_time_length/_time_unitmixedNULLbookkeeping — re-derived
primary_supply_ordering_notesvarchar(8192)NULLbookkeeping — re-derived from item_supply.ordering_notes
secondary_supply_* (same sub-column set as primary_supply_*)mixedNULLsame roles as the primary cluster
default_supplyvarchar(255)NULLpayload
default_supply_eidUUIDNULLpayload
card_size, label_size, breadcrumb_size, item_colorvarchar(255) (enums)NULLpayload
manufacturer_ref_entity_id / _affiliate_eidUUIDNULLpayload
manufacturer_ref_namevarchar(255)NULLbookkeeping
manufacturer_ref_record_idUUIDNULLbookkeeping — version-pin, set only on retirement
manufacturer_ref_retired, manufacturer_ref_provenance_updated_by/_atmixedNULL/NOT NULLbookkeeping
total_inventory_count_amount/_unitdouble / varchar(255)NULLpayload
last_count_date_timestamp/_time_zonetimestamp / varchar(255)NULLpayload
identifiers_upc/_ean/_gtin/_isbn/_asinvarchar(255)NULLpayload
additional_qrs, labelsJSONBNOT NULL, default []payload
search_texttext, GENERATED STORED—bookkeeping — DB-generated search index, excluded from application writes

Constraints: PK id; FK fk_item_previous__id. No supplier/manufacturer FK anywhere — cross-Universe rule.

ColumnTypeNullRole
idUUIDNOT NULL (PK)identity
effective_as_oftimestampNOT NULLidentity
recorded_as_oftimestampNOT NULL, default now()identity
bts_authorvarchar(244)NOT NULLbookkeeping
eidUUIDNOT NULLidentity
previousUUIDNULL, no FKidentity
retiredbooleanNOT NULL, default falsebookkeeping
bts_author_sub, bts_note, bts_created_by_subvarcharNULLbookkeeping
created_by, created_at_effective, created_at_recordedvarchar / timestamp(3)NOT NULLbookkeeping
parent_eidUUIDNOT NULLidentity — floats to Item’s eId, not a pinned rId
supplier_ref_entity_id / _affiliate_eidUUIDNULLpayload
supplier_ref_namevarchar(255)NOT NULLbookkeeping — value-snapshot, preserved from the earlier supplier column
supplier_ref_record_idUUIDNULLbookkeeping — version-pin
supplier_ref_retired, supplier_ref_provenance_updated_by/_atmixedNOT NULL / NULLbookkeeping
supply_name, sku, order_method, urlvarcharNULLpayload
order_quantity_amount/_unit, unit_cost_value/_currency, average_lead_time_length/_time_unitmixedNULLpayload
ordering_notesvarchar(8192)NULLpayload — ordering information for this item from this supplier

Constraints: PK id; no FK on previous or parent_eid (cross-Universe rule). Application-level uniqueness only: one supply name per parent, enforced by ItemSupplyValidator, not a DB constraint.

TablePurposeNotable columns
batch_jobBulk-operation job tracking (shared with system.batch, not Item-specific)job_status, last_event (JSON), success_count, error_count, total, errors (JSON) — plus the standard bitemporal/bookkeeping columns
item_draftDraft store keyed by entity, not versionedentity_id (PK, UNIQUE), tenant_id, draft_value (JSON), draft_metadata (JSON), author
tenant_settingsTenant-scoped printing-settings override document (PDEV-1077)overrides (JSONB), plus the standard bitemporal/bookkeeping columns
user_settingsSame shape as tenant_settings but unscoped by tenant (a user’s overrides follow them)subject (varchar, replaces tenant_id), overrides (JSONB)

Item.Entity (the payload of EntityRecord<Item, ItemMetadata>)

Section titled “Item.Entity (the payload of EntityRecord<Item, ItemMetadata>)”
FieldTypeStored or computedNotes
eId, name, description, imageUrl, classification, useCase, glCode, locator, internalSKU, minQuantity, notes, cardNotesDefault, taxable, defaultSupply, defaultSupplyEId, cardSize, labelSize, breadcrumbSize, itemColor, manufacturer, totalInventoryCount, lastCountDate, identifiers, additionalQrs, labels1:1 with item columnsStored
primarySupply, secondarySupplyItemSupplyReference.Value?Stored, but re-derived every writereconcileItemProjection re-projects the slot from the authoritative ItemSupply before persisting
preferredSupplyItemSupplyReference?ComputeddefaultSupply name match against primary/secondary, else primary-or-secondary
nonPreferredSupplyItemSupplyReference?Computedthe slot not selected as preferred
FieldTypeStored or computedNotes
eId, supplier, name, sku, orderMethod, url, orderQuantity, unitCost, averageLeadTime1:1 with item_supply columnsStored
orderingNotesString?StoredFree text: how to order this item from this supplier. A supply created through the API carries the notes in the request, or none when the request has none; it is never seeded from the item’s cardNotesDefault. For notes that apply to every supply from a supplier, see the supplier’s BusinessRole.notes in Business Affiliate: Data Model.

ItemReference.Value (the projection published by /distinct, consumed by every module that references an Item)

Section titled “ItemReference.Value (the projection published by /distinct, consumed by every module that references an Item)”
FieldTypeStored or computedNotes
eId, rId, name, retired, provenancemixedComputedItemReference.Value.fromItem(...) — retired/provenance denormalize the item’s current state at read time
FieldTypeStored or computedNotes
card, label, breadcrumbMap of size → template configStored (packaged JSON) + Computedthe default flag per form is resolved per-caller, not stored on the item row
ColumnTargetKindNotes
item_supply.supplier_ref_entity_idBusiness Affiliate — BusinessRole (VENDOR)floatingresolved via resolveVendorRef/resolveRoleRef
item_supply.supplier_ref_record_idBusiness Affiliate BusinessRole tombstoneversion-pinpinned only on retirement
item.manufacturer_ref_entity_idBusiness Affiliate BusinessRole (MANUFACTURER)floatingresolved by resolveManufacturer
item.manufacturer_ref_record_idBusiness Affiliate BusinessRole tombstoneversion-pinpinned only on retirement
item.primary_supply_* / secondary_supply_*item.item_supply (authoritative row)value-snapshotkept in sync by reconcileItemProjection and the BA→item vendor cascade
item.tenant_idtenant scopetenant/scopeitem_supply inherits scope via the parent, enforced query-side, not by a stored column

No foreign key exists from item_supply.parent_eid to item — children reference the parent by floating eId, never a DB FK to a specific row (cross-Universe rule).

Item is Live, on the legacy URL shape. The routes below are the designed shape; today the module answers on POST /v1/item/item and PUT /v1/item/item/{entity-id} — see the API Endpoint Catalog. It is converging on what is recorded here.

Follows the request/response call-shape convention in The Orders v2 Call Shape.

MethodPathPurposeMain refusals
POSTitem/addCreate an item (supplies + manufacturer resolved in-transaction)
PUTitem/updateUpdate an item
(framework)readByEid, readByRId, forceDelete, queryNode, distinctNode, historyNode, bulkNode, draftNodeGeneric data-authority CRUD/query/history/bulk/draft surface
POSTprint-label, print-breadcrumbPrint card labels / breadcrumbs for a set of items
GETsettings/printingEffective print-template configuration
POST/PUT/DELETE/GET{parent-item-id}/supply[/{item-supply-id}]Add/update/remove/list an item’s supplies
POST/GETlookup, lookup/{pageId}Fuzzy lookup over the generated search text
GETby-codeExact code resolution (QR/UPC/EAN/GTIN/ISBN/ASIN)
POSTimage-upload/request-upload-credentialsPresigned S3 upload credentials + CDN URL
GETlookup-suppliers, lookup-units, lookup-items, lookup-types, lookup-subtypes, lookup-usecases, lookup-facilities, lookup-departments, lookup-locations, lookup-sublocationsFuzzy type-ahead lookups
POSTCSV upload routesBulk CSV item import (implemented outside this module, in system.batch.csvupload)

Canonical reference-data aliases for the supply routes also exist at /v1/reference-data/item/item-supply/supply/..., alongside legacy /v1/item/item/... paths sharing the same handlers.

Emits NewEntity/UpdateEntity on item create/update. Listens for BusinessRole change notifications — VENDOR changes drive the vendor cascade onto linked item_supply rows and their parent item projections; MANUFACTURER changes drive the manufacturer cascade.

  • Duplicate index in the migration SQL. idx_item_seondary_supplier_eid (sic, misspelled in the migration itself) is created twice — the second CREATE INDEX is a no-op, not a functional bug, but it’s a pre-existing typo in V010__item_supply.sql, left as found.
  • This module predates the occurrence-kind pattern. No ItemOccurrenceKind exists; change notification uses the generic DataAuthorityNotification<Item, ItemMetadata> sealed type (NewEntity/UpdateEntity/DeleteEntity) instead of a domain-specific kind enum.
  • An empty supply slot. The item treats a present slot that carries no user data as absent, and drops it on write (ItemSupplyReference.hasSupplyData()). Non-blank orderingNotes count as data, so a slot that carries only notes is not dropped. A slot with notes is refused unless it identifies its supplier: a non-blank name, a vendor link carrying both ids, or, on update, a supply link. On create a supply link does not count, because the create makes a new supply under that id.
  • ordering_notes was seeded once from the item’s card notes. V037__item_supply_ordering_notes.sql added the three columns and wrote two kinds of new record. First, one for every live supply of every live item with non-blank card_notes_default, carrying those notes. Then one for each such item, filling the copy on each slot that has a supplier name and identifies its supplier: a non-blank name, a vendor link carrying both its role and affiliate ids, or a link to a live supply of the item (a write drops a supply link that names none). A slot with no supplier name reads as absent whatever it links, and a slot that identifies nothing reads as empty and is dropped on the next write, so neither gets a copy. Each new record follows the entity’s most current record (previous). It is recorded at the migration’s transaction time truncated to the millisecond, and takes effect at that same time, or one millisecond after the current record when that one is dated later, so it is the current record in either case and a read of a future date made after the migration already sees the notes. card_notes_default itself is unchanged, and rows written before the migration read ordering_notes as NULL.
  • primary_supply_*/secondary_supply_* on item are a maintained projection, not client-authoritative data — every write re-derives them from the authoritative item_supply rows, so treat item_supply as the source of truth for supply details even though the fields are readable directly off item.