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”The Item aggregate: the bitemporal Item entity with its embedded value objects (classification, locator, standard identifiers, supply-reference snapshots), the child ItemSupply entities keyed by parent, the shared SupplierReference descriptor, and the print-size/color enumerations.
Inventory Count
Section titled “Inventory Count”Two nullable fields record on-hand inventory on the item: totalInventoryCount — a Quantity (amount + unit) holding the most recent counted amount — and lastCountDate — a DateTime for when that count was taken. Both are optional and independently clearable; an omitted field on update is written as null. totalInventoryCount.unit participates in the shared unit lookup (/item/lookup-units).
Because a stored Quantity is always complete (both amount and unit) or absent, a partially-specified count is normalized at the input layer so a valid value is persisted: an amount with no unit defaults the unit to each, a unit with no amount defaults the amount to 0, and an empty count (neither provided) is stored as null.
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.
Item Identifiers (PDEV-1251)
Section titled “Item Identifiers (PDEV-1251)”Three additive field groups connect an item to the outside world of barcodes and catalogs:
identifiers: ItemIdentifiers?— five optional standard identifiers, each a distinct value class with a smart constructor (Upc,Ean,Gtin,Isbn,Asin— never a nakedString). Validation is shape-only (UPC-A^\d{12}$, EAN^\d{8}|\d{13}$, GTIN^\d{14}$, ISBN^\d{9}[\dX]$or^\d{13}$with a lowercasexcheck character normalized to uppercase, ASIN^B[A-Z0-9]{9}$with a digit guard); no check digits are verified on write, and a malformed value is rejected with a 400 through validating serializers. Identifiers are not unique per tenant — several items may carry the same code, and/by-codereturns all of them.additionalQrs: List<String>— text values rendered as QR codes for the item, resolvable by exact scan.labels: List<String>— free-form text labels for ad hoc categorization and search.
Both lists ride the item row as jsonb columns and are bounded (identifierLists config; defaults: QRs ≤ 64 entries, labels ≤ 128 entries, ≤ 256 characters per entry) with exact duplicates rejected and order preserved as written. On the input DTO the lists are nullable-optional — absent, null, and [] are equivalent — so clients that predate the fields are unaffected.
A database-generated search_text column (immutable expression, stored) concatenates the item’s searchable surface — name, description, SKUs, classification, locator fields, supply and manufacturer names, the five identifiers, the complete labels array, and the leading 256 characters of notes — and backs the fuzzy /lookup endpoint with a single GIN trigram index. Because every bitemporal version row carries its own generated value, time-travel lookups match each version’s own contents.
API Endpoints
Section titled “API Endpoints”The module base path is /v1/item. All routes 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.
Identifier Search (PDEV-1251)
Section titled “Identifier Search (PDEV-1251)”Two resource-scoped routes resolve items by their searchable text surface and by scanned codes. The endpoint layer performs request validation and response formatting only; ItemService (via ItemLookupService) owns the business logic — code-candidate expansion, the result cap — and the read-only transaction boundaries.
| Method | Path | Request | Response | Description |
|---|---|---|---|---|
POST | /item/lookup?lookup=<s> | Query body (filter, sort, paginate) | PageResult | Fuzzy lookup over search_text — first page |
GET | /item/lookup/{page}?lookup=<s> | — | PageResult | Subsequent page from a cursor; the lookup string is re-supplied |
GET | /item/by-code?code=<c> | — | Array of item records | Exact resolution of a scanned/typed code |
/lookup follows the /query family’s shape: the caller’s filter composes (AND) with the fuzzy predicate (search_text % s OR search_text ILIKE '%s%', both served by the GIN trigram index), ordering is relevance first (trigram word distance), then the caller’s sort, then eid as a stable tiebreak, and pagination uses the standard cursor machinery. Both routes accept the standard effective-as-of / recorded-as-of parameters.
/by-code expands the code once into its equivalence set — GTIN zero-padding families (8/12/13/14 digits) plus ISBN 10↔13 conversion with recomputed check digits (979… ISBN-13s have no ISBN-10 form and expand one-directionally) — then resolves it with one indexed query: identifier equalities against the candidate set OR jsonb containment of the verbatim code in additionalQrs. Duplicates are legal, so the response is a plain array (expected cardinality 0–few) capped server-side (byCode.maxResults, default 50). A blank lookup/code is a 400.
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
Section titled “Lookups”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 (aggregated across minQuantity, supply order quantities, and totalInventoryCount) |
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 |
identifiers_upc | String | UPC-A identifier |
identifiers_ean | String | EAN identifier |
identifiers_gtin | String | GTIN-14 identifier |
identifiers_isbn | String | ISBN identifier |
identifiers_asin | String | ASIN identifier |
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 |
lookupItems(lookup, query, asOf) | Fuzzy lookup over the item search surface, /query-shaped page (PDEV-1251) |
findByCode(code, asOf) | Exact code resolution with GTIN/ISBN candidate expansion and result cap (PDEV-1251) |
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(image URL checks and the DQ-015 list bounds, delegating the list checks to the payload’s ownvalidateLists) - Fuzzy-match lookup methods for typeahead fields
lookupPage— the/lookupquery assembly: fuzzy predicate over the generatedsearch_textcomposed with the caller’s filter, relevance-first ordering, standard cursors (PDEV-1251)byCode— the/by-codequery assembly: identifier equalities against the pre-expanded candidate set OR jsonb containment of the verbatim code (the@>operator lives in the operations-wide commons,cards.arda.operations.common.lib) (PDEV-1251)
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, totalInventoryCount) | amount, unit |
DateTimeComponent | DateTime (lastCountDate) | timestamp, time_zone |
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 |
BusinessRoleReferenceComponent | BusinessRoleReference (manufacturer — MANUFACTURER role) | manufacturer_ref_name, manufacturer_ref_entity_id, manufacturer_ref_affiliate_eid, manufacturer_ref_record_id, manufacturer_ref_retired, manufacturer_ref_provenance_updated_by, manufacturer_ref_provenance_updated_at |
ItemIdentifiersComponent | ItemIdentifiers | identifiers_upc, identifiers_ean, identifiers_gtin, identifiers_isbn, identifiers_asin (each independently nullable) |
The QR and label lists are jsonb columns (additional_qrs, labels, NOT NULL DEFAULT '[]'), and search_text is a database-generated stored column excluded from Exposed writes. Migration V024 adds the columns and the generated expression; V025 creates the supporting indexes concurrently — one GIN gin_trgm_ops on search_text (the only fuzzy index, bounding GIN write amplification on the bitemporal table to one entry per version row), five partial b-trees (tenant_id, identifiers_*) WHERE … IS NOT NULL, and a GIN jsonb_path_ops on additional_qrs. An EXPLAIN-based CI test asserts every search predicate is index-served, and a checked-in seeding script (scripts/db/seed-item-lookup-benchmark.sql) makes the 50k-item p95 benchmark repeatable.
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 |
identifierLists.maxLabels | Int | Label list bound (default 128) — PDEV-1251 |
identifierLists.maxAdditionalQrs | Int | Additional-QR list bound (default 64) — PDEV-1251 |
identifierLists.maxEntryLength | Int | Per-entry character bound for both lists (default 256) — PDEV-1251 |
byCode.maxResults | Int | Server-side result cap for /by-code (default 50) — PDEV-1251 |
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