Design: Item Manufacturer Field
Overview
Section titled “Overview”The parts catalog gains an optional Item Manufacturer — who makes a part, distinct from the
supplier you buy it from. Instead of a plain string, a manufacturer is a BusinessAffiliate carrying
a BusinessRole of type MANUFACTURER. This is the exact model already used for suppliers (a
BusinessAffiliate with a VENDOR role), so the design reuses that machinery rather than
duplicating it: it executes PDEV-875
first, generalizing SupplierReference into a role-agnostic BusinessRoleReference (a role
field; the same { name, eId, affiliateEId, rId, retired, provenance } wire shape), then adds a
single-valued manufacturer link on Item.
Two behaviors define the feature. First, resolution: when an item is written with a manufacturer,
the backend links the reference to a live MANUFACTURER role — with the caller choosing, via an
optional mode parameter that defaults to PROPAGATE, between STRICT (reject an unlinked
reference) and PROPAGATE (find-or-create the affiliate + role atomically). Second, consistency: a listener keeps every
linked item’s stored manufacturer in sync when the role is renamed or retired — identical to the
supplier reverse-sync — which is why the full denormalization (retired / rId / provenance) and
the retired guard are kept, not dropped.
The value is surfaced in the item form (Ordering Details typeahead), the catalog filter and column,
and the card / Kanban / label / breadcrumb print payloads. Manufacturer-management screens,
print-template testData.json, and bulk-import are explicit follow-ups (see Out of Scope).
Decision Summary
Section titled “Decision Summary”| # | Decision | Chosen Option |
|---|---|---|
| DQ-001 | Where does find-or-create live? | Backend resolver (atomic), not BFF choreography |
| DQ-002 | Fixed behavior or caller-selected? | STRICT/PROPAGATE mode; optional, defaults to PROPAGATE (client always explicit); hard-required deferred to PDEV-1107 |
| DQ-003 | New ManufacturerReference or generalize? | Generalize SupplierReference → BusinessRoleReference (execute PDEV-875) |
| DQ-004 | Denormalization (retired/rId/provenance) + retired guard? | Kept — parity with ItemSupply, forward-compat with mgmt screens |
| DQ-005 | Keep the role→item listener? | Required — keeps Item.manufacturer.name consistent unless item deleted/retired |
| DQ-006 | Lookup endpoint | Reuse the generic role-filtered business-affiliate lookup (role=MANUFACTURER) |
| DQ-007 | Frontend contract mode | Always reference — net-new, no legacy/probe |
| DQ-008 | Execution scope | FE + BE + printing (incl. Kanban); templates & bulk-import → follow-ups |
| DQ-009 | PDEV-875 handling | Executed inline as Phase 1 T-01; this project closes it |
Full rationale in Decision Log.
Structural Design
Section titled “Structural Design”Class Diagram
Section titled “Class Diagram”Per DQ-003, one value object (BusinessRoleReference) serves both links, distinguished by its
role. Crucially, role is supplied per slot — a compile-time constant injected by the
component factory (VENDOR for the supplier_ref slot, MANUFACTURER for manufacturer_ref) — not
a persisted or serialized field. So there is no role column, no migration, and no backfill, and
the wire JSON shape stays {name, eId, affiliateEId, rId, retired, provenance} (PDEV-875 invariant).
Reads remain single-query: the reference’s denormalized columns live on the owner row and the role is
never looked up. ItemSupply.supplier keeps role = VENDOR; Item.manufacturer is a new,
single-valued role = MANUFACTURER reference — no primary/secondary, no default pointer (contrast the
supplier’s primarySupply/secondarySupply). The reference is a cross-Universe soft
reference (no SQL FK); name is the source of truth, eId/affiliateEId link to the role and its
parent affiliate.
Key Classes and Interfaces
Section titled “Key Classes and Interfaces”BusinessRoleType (enum)
Section titled “BusinessRoleType (enum)”- Package:
cards.arda.operations.reference.businessaffiliates.business - Change: add
MANUFACTURER. No DB migration — the column is a freeVARCHARwith no CHECK. - Design decision: DQ-003.
BusinessRoleReference (was SupplierReference)
Section titled “BusinessRoleReference (was SupplierReference)”- Package:
cards.arda.operations.reference.businessaffiliates.domain - Responsibility: canonical, role-agnostic soft reference from an owner (supply or item) to a
BusinessRole. - Key fields:
name,eId?,affiliateEId?,rId?,retired,provenance?, androle: BusinessRoleType— the latter is a non-serialized, non-persisted field (DQ-010, sub-form a): excluded from the JSON and from the flattened columns, populated per slot at the deserialize/component boundary (supplier_ref⇒VENDOR,manufacturer_ref⇒MANUFACTURER). It carries a code comment stating it is not persisted and how it is populated; the resolver/listener read it from the VO. - Key methods: smart constructor
invoke(...)—namenot blank;eId != null ⇒ affiliateEId != null. - Design decision: DQ-003, DQ-004, DQ-010.
Item (add manufacturer)
Section titled “Item (add manufacturer)”- Package:
cards.arda.operations.reference.item.business - Change:
manufacturer: BusinessRoleReference.Value?(optional).validateallows empty/absent. - Persisted via
businessRoleReferenceComponent("manufacturer_ref")inItemPersistence; migration adds the full flattened column set onitem(name, entity_id, affiliate_eid, record_id, retired, provenance_*). - Design decision: DQ-003, DQ-004.
Manufacturer resolver (generalized from ItemVendorResolver)
Section titled “Manufacturer resolver (generalized from ItemVendorResolver)”- Package:
cards.arda.operations.reference.item.service - Responsibility: resolve a
BusinessRoleReferencefor a targetBusinessRoleTypeunder aSTRICT/PROPAGATEmode — defaulting toPROPAGATEwhen the caller omits it (DQ-002, backward-compatible; the frontend still always passes it explicitly). - Key method:
resolve(ref, role, mode = PROPAGATE, asOf, tenantId, baService): Result<BusinessRoleReference.Value?>.PROPAGATEreusesfindByNameAndRole/findByName/createBusinessRole/addand keeps the retired guard (DQ-012 from the supplier work).STRICTvalidates a fully-linked reference and rejects an unlinked non-empty one. - Retirement granularity (DQ-004): per-role when a
BusinessRoleretires, all-roles when theBusinessAffiliateretires. PROPAGATE may attach aMANUFACTURERrole to a live affiliate even if that affiliate’sVENDORrole is retired, but refuses a retired affiliate (no resurrection). - Design decision: DQ-001, DQ-002.
Role listener + item-side cross query
Section titled “Role listener + item-side cross query”- Package:
cards.arda.operations.reference.item.service - Responsibility: react to
MANUFACTURERrole rename/retire and keep linked items’ stored manufacturer consistent. - New query:
findItemsByManufacturerRole(roleEId, asOf)— tenant-scoped, the item-level analog ofCrossItemSupplyUniverse.findSuppliesBySupplierRole. - Design decision: DQ-005.
Behavioral Design
Section titled “Behavioral Design”Sequence Diagrams
Section titled “Sequence Diagrams”Manufacturer resolution during an item write (happy path shown for PROPAGATE).
Entry point is the item create/update service, which passes the caller-supplied mode to the resolver
before persisting, inside the existing withTenant { … } transaction — so affiliate/role
creation and the item write commit or roll back together (DQ-001). Resolution runs only when the
incoming manufacturer name/link differs from the stored one. Error paths: under STRICT, a non-empty
reference lacking eId/affiliateEId fails validation (400); under PROPAGATE, linking to a
retired affiliate is rejected by the retired guard; a blank/absent name stores null (unlinked, no
resolution).
Consistency reaction when a manufacturer role is renamed or retired.
The listener is registered via BusinessAffiliateService.addRoleObserver and reacts only to
MANUFACTURER notifications (it ignores VENDOR and others). Items already deleted/retired are skipped
(DQ-005). The refresh is projection-only (no re-resolution) to avoid a cascade loop, and follows the
same bitemporal heal-on-write discipline (coordinate-tie avoidance) established by the supplier work.
API Contract
Section titled “API Contract”Item create/update gains an optional resolution-mode parameter that defaults to PROPAGATE
when omitted (DQ-002). Making it required-with-no-default would break every existing caller of the
shared item-write endpoint (a missing required field is rejected, unlike an unknown extra field
which the backend ignores) and would break independent backend-before-frontend deploys — so the hard
requirement is deferred to PDEV-1107. The Arda
frontend always sends the mode explicitly, so intent is explicit within our stack; the default exists
only for not-yet-upgraded callers.
- Method:
POST/PUT - Path: existing item routes (e.g.
/v1/reference-data/item/item), plus optional mode parameterresolution-mode=STRICT|PROPAGATE(query or body field; absent ⇒PROPAGATE). - Authentication: Bearer API key + tenant/author headers (unchanged).
- Request: item payload with
manufacturer: BusinessRoleReference({ name, eId?, affiliateEId?, … }); the role (MANUFACTURER) is implied by themanufacturerslot and is not part of the serialized payload (DQ-010); manufacturer omitted/blank ⇒ unlinked. - Response: the persisted item with the resolved (linked) manufacturer reference.
- Error responses:
400—STRICTwith a non-empty but unlinked reference. (Omittingresolution-modeis not an error today — it defaults toPROPAGATE; it becomes an error under PDEV-1107.)409/validation —PROPAGATEattempt to link a retired affiliate (retired guard).500— persistence/transaction failure (whole write rolls back).
Manufacturer suggestions reuse the existing generic lookup (DQ-006):
- Method:
GET - Path:
/v1/reference-data/business-affiliate/lookup?name=<search>&role=MANUFACTURER - Response:
BusinessAffiliateLookupResult(entries carry the ids needed to build a linked reference).
Implementation Scope
Section titled “Implementation Scope”Files to Create
Section titled “Files to Create”| File | Package/Path | Purpose |
|---|---|---|
V0xx__item_manufacturer_ref.sql | operations item migrations | Add manufacturer_ref_* columns to item |
| manufacturer resolver entry point | …/reference/item/service | Thin MANUFACTURER entry into the shared resolver |
| listener + cross query | …/reference/item/service + …/item/persistence | findItemsByManufacturerRole + role reaction |
ManufacturerTypeahead (or reuse AsyncTypeahead) | arda-frontend-app src/components/items | Ordering-Details typeahead |
lookupManufacturers + adapter | arda-frontend-app src/lib | Generic-lookup client + …AsOptions adapter |
| BFF route | arda-frontend-app src/app/api/arda/…/lookup-manufacturers | Pass-through to generic lookup |
Files to Modify
Section titled “Files to Modify”| File | Change Description |
|---|---|
SupplierReference.kt → BusinessRoleReference.kt (+ component, factory, ~46 imports) | PDEV-875 rename + role field |
BusinessAffiliate.kt (BusinessRoleType) | Add MANUFACTURER |
Item.kt, ItemPersistence.kt | Add manufacturer field + component |
ItemVendorResolver / item CRUD service | Generalize to role + mode; thread resolution-mode |
ItemSupplyRoleListener (or peer) + item service/cascade | React to MANUFACTURER |
item print builders — ItemPrinter.kt, ItemPrintingService.kt, KanbanCardPrinter.kt | Thread item_manufacturer (card/Kanban) / manufacturer (label/breadcrumb) |
FE types/items.ts, types/arda-api.ts, constants/types.ts, mappers/ardaMappers.ts, store/slices/cacheSlice.ts, ItemFormPanel.tsx, items grid/filter config | Manufacturer field, always-reference mapping, typeahead, filter, column |
Out of Scope
Section titled “Out of Scope”- Manufacturer management screens (managed entity CRUD/merge). Denormalized fields are added now to support them later (DQ-004).
- Print-template layout /
testData.jsonin the printing-templates repo — follow-up ticket (DQ-008). - Bulk-import round-trip of
item.manufacturer— follow-up ticket, created after the backend is complete (DQ-008). api-test/Bruno API tests — backend behavior is covered by ContainerizedPostgres integration tests.
Testing Strategy
Section titled “Testing Strategy”Per-task TDD: unit tests first (compiling, failing), then implementation, then integration/E2E, then a self-contained commit. The PDEV-875 rename is a refactor — the existing suite stays green.
Unit Tests
Section titled “Unit Tests”| Test | Target | Validates |
|---|---|---|
smart-constructor incl. role | BusinessRoleReference.Value | invariants (blank name; eId ⇒ affiliateEId) |
| resolver — default mode | manufacturer resolver | absent resolution-mode resolves as PROPAGATE (backward-compatible) |
resolver — STRICT | manufacturer resolver | rejects non-empty unlinked reference; passes fully-linked |
resolver — PROPAGATE | manufacturer resolver | find existing / reuse same-name BA / create new / retired guard |
| component round-trip | BusinessRoleReferenceComponent | flatten/materialize incl. manufacturer_ref_* |
| mapper round-trip | FE ardaMappers | always-reference build + read incl. empty/undefined |
| typeahead | FE ManufacturerTypeahead/adapter | suggestions, select existing, add new, clear |
Integration Tests
Section titled “Integration Tests”| Test | Setup | Validates |
|---|---|---|
| item create/update round-trip | ContainerizedPostgres | manufacturer persisted + re-linked; both modes |
| listener rename | ContainerizedPostgres | role rename updates linked items’ manufacturer.name |
| listener retire | ContainerizedPostgres | role retire marks linked items’ reference retired |
| deleted/retired item skipped | ContainerizedPostgres | listener does not touch deleted/retired items |
| generic lookup | ContainerizedPostgres | role=MANUFACTURER returns matching affiliates |
| VENDOR non-regression | ContainerizedPostgres | supplier path unchanged after generalization |
Frontend E2E (mock mode)
Section titled “Frontend E2E (mock mode)”| Test | Validates |
|---|---|
| create item w/ manufacturer (existing + new via typeahead), reopen, edit, clear | form round-trip |
| filter catalog by manufacturer; toggle Manufacturer column | catalog integration |
References
Section titled “References”- Goal
- Decision Log
- Project Plan
- Workbook analysis —
workbooks/notebooks/manufacturer-field/ba-manufacturer-changes-analysis.md - Supplier/vendor precedent — PDEV-731, PDEV-928 (
operationsbusinessaffiliates+itemmodules)
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved