Design: Connect Item.Supply to Business Affiliate References
This is a lightweight TLD proposal organized under the early-design-review headings — Goal, Scope, Functional Modules & Endpoints, and Scenarios — followed by the decision traceability, testing strategy, and references the implementing team needs.
Linear ticket: PDEV-731 — Connect Item.Supply to Business Affiliate References (parent PDEV-182, project “Reference / Supplier Management”).
ItemSupply records (and the primarySupply / secondarySupply snapshots on
Item) currently link to a supplier through an ad-hoc pair of fields —
supplierEId: EntityId? (holding a BusinessAffiliate eId) plus a
denormalized supplier: String. The goal is to replace that ad-hoc link with a
canonical reference value object, SupplierReference.Value, that targets the
specific BusinessRole with role VENDOR (per DQ-001), following the
established cross-Universe reference pattern (eId / rId / cached name,
floating-or-pinned; per DQ-002), and to make the Item module react correctly when
a supplier is removed.
Use Cases
Section titled “Use Cases”- Add primary/secondary supplies on item creation — (a) with a pre-existing supplier; (b) creating the supplier on-the-fly.
- Edit primary/secondary supplies for an existing item — (a) assign a pre-existing supply; (b) create a new supply with a pre-existing supplier; (c) create a new supply, supplier on-the-fly.
- List all supplies associated with an item.
- Remove a supply — (a) when it is not primary/secondary; (b) when it is primary/secondary (guarded).
- Remove a supplier — emit notifications and have the Item module react by marking affected supplies stale.
V0 Scoping
Section titled “V0 Scoping”- API / backend services layer only (
operations); front-end is tracked separately (e.g. PDEV-764). - Only the VENDOR
BusinessRoleis in scope; Customer/Carrier/Operator roles are not.
Constraints & Assumptions
Section titled “Constraints & Assumptions”- Cross-Universe rule: no foreign keys and no shared transactions across the Item and BusinessAffiliate universes; resolution always goes through the owning service interface.
- Follow Arda Kotlin conventions (Result/AppError, Exposed, smart constructors) and the bitemporal table conventions.
- A CHANGELOG entry is required per PR, per each repo’s model.
- Assumption: the existing in-code wiring is the baseline being redesigned (not greenfield) — see Key Insight.
Key Insight
Section titled “Key Insight”The ticket premise (“supplies don’t refer to real suppliers — supplier entity
not live”) is out of date. operations already wires Item↔BusinessAffiliate:
supplierEId, ItemVendorResolver.resolveVendor, the STRICT/LAX/PROPAGATE
qualifier modes, and VENDOR name-change propagation all exist. So this is a
redesign, and the one genuinely missing behavior is UC 5 — the reaction
to supplier removal, which today is explicitly ignored.
Two derived design moves shape the rest of the proposal:
- Stale, don’t delete (DQ-003): on supplier removal, mark the supply’s
supplier reference stale (
retired = true, reference pinned to the supplier’s tombstone), exactly mirroring how aKanbanCard’sItemReferencereacts when itsItemis deleted (PDEV-808). Supply data is preserved so pending workflows already committed to those values still resolve. - Mutation semantics (DQ-011/DQ-012): supply resolution recognizes three
qualifiers —
STRICT(the referenced VENDOR role must resolve to a live supplier),PROPAGATE(create-on-the-fly: find or create the BusinessAffiliate- VENDOR role, then link), and
LAX(leave the supply unlinked — name-only, no name-lookup upsert). DQ-011 removed the LAX name-lookup upsert, not theLAXvalue itself (the qualifier is shared with item-level bulk mutations). The endpoint default isPROPAGATE. A new supply may never start dead — resolving against a retired supplier is rejected:STRICT/LAXnaturally (the retired role/affiliate is filtered from active bitemporal queries) andPROPAGATEexplicitly (it must not resurrect a retired role).
- VENDOR role, then link), and
Decisions
Section titled “Decisions”| # | Decision | Chosen Option |
|---|---|---|
| DQ-001 | Which entity does a supply reference — BA or BusinessRole(VENDOR)? | BusinessRole(VENDOR) |
| DQ-002 | Reference form — canonical Reference value object vs plain UUID+name | Canonical SupplierReference.Value |
| DQ-003 | Effect of supplier removal on an ItemSupply | Stale marker (PDEV-808 pattern), no delete |
| DQ-004 | Extract a dedicated ItemSupplyService? | Yes — extract from inner SupplyService |
| DQ-005 | Adopt canonical reference-data URL routes? | Yes |
| DQ-006 | How is a child BusinessRole resolved across the Universe boundary? | Carry affiliateEId in the reference |
| DQ-007 | Event source for supplier removal (cascade vs BA-delete subscription) | Deferred — verify cascade in code (T-08) |
| DQ-008 | Migration/backfill of existing supplierEId (BA eId → VENDOR role) | Lazy re-link — no backfill |
| DQ-009 | Backward compatibility of legacy routes + api-test path migration | Keep legacy routes as aliases |
| DQ-010 | Floating-vs-pinned semantics on retirement | Mirror Kanban (floating; pin on retire) |
| DQ-011 | Supply mutation qualifier set for ItemSupply creation | STRICT / PROPAGATE (create-on-the-fly) / LAX (unlinked); drop LAX name-lookup upsert |
| DQ-012 | Creating a supply that points to a retired supplier | Disallowed — bitemporal filter (STRICT/LAX) + explicit PROPAGATE reject |
| DQ-013 | Representing an unlinked/legacy vendor name | Superseded by DQ-014 |
| DQ-014 | Eliminating the redundant supplier name | Supplier descriptor: name on the reference (required); eId/affiliateEId nullable; drop ItemSupply.supplier |
Full rationale in Decision Log.
Where the work touches — the blast radius across repositories, modules, and resources.
Repositories & Components
Section titled “Repositories & Components”| Repository | Touched | Blast radius |
|---|---|---|
operations | Item module (business/endpoints/service/persistence) + BusinessAffiliate module (service possibly extended) | Reference value object, dedicated service, Flyway migration, canonical routes, unit + integration tests |
api-test | Item + BusinessAffiliate Bruno collections | New supplier-linking and stale-on-removal scenarios |
documentation | current-system, domain/information-model, url-naming.md | Design page, model alignment, reference-data route convention |
- Resources: a Flyway migration on the Item database (the
ITEM_TABLEsupply components and theITEM_SUPPLY_TABLE). No new AWS infrastructure and no new libraries.
Files to Create
Section titled “Files to Create”| File | Package/Path | Purpose |
|---|---|---|
SupplierReference.kt | reference/item/domain | Canonical supplier reference value object |
SupplierReferenceComponent.kt | reference/item/domain/persistence | Flattened SUPPLIER_REFERENCE_* columns |
ItemSupplyService.kt | reference/item/service | Extracted dedicated supply service |
VNNN__supplier_reference.sql | resources/item/database/migrations | Replace supplier columns; add reference + retired + provenance |
Files to Modify
Section titled “Files to Modify”| File | Change Description |
|---|---|
ItemSupply.kt | Replace supplierEId+supplier with supplier: SupplierReference.Value |
ItemSupplyReference.kt | Embed SupplierReference in primary/secondary snapshots |
Item.kt | Adjust supply snapshot fields / validation |
ItemService.kt | Remove inner SupplyService; delegate to ItemSupplyService |
ItemVendorResolver.kt | Retarget resolution to VENDOR role; produce SupplierReference |
ItemSupplyPersistence.kt / ItemPersistence.kt | New reference columns; drop legacy supplier columns |
ItemEndpoint.kt / Module.kt | Canonical reference-data routes (+ legacy aliases per DQ-009) |
BusinessAffiliateService.kt | Role-by-eId resolution and/or BA-delete cascade events (per DQ-006/007) |
Out of Scope
Section titled “Out of Scope”- Front-end design and implementation (separate tickets, e.g. PDEV-764).
- Customer/Carrier/Operator roles — only VENDOR is in scope.
- Bulk supplier reconciliation tooling beyond the one-time migration backfill.
Functional Modules & Endpoints
Section titled “Functional Modules & Endpoints”Module / Layer Class Diagram
Section titled “Module / Layer Class Diagram”Classes are grouped by the functional decomposition hierarchy
{domain}/{module}/{layer} — domain reference-data, modules item and
business-affiliate, and the layers business, endpoints, service,
persistence. Solid composition arrows are within a module; dashed arrows
crossing the two modules are cross-Universe soft references (no FK, resolution
via the owning service interface).
SupplierReference.Value (in the item module’s business layer) takes the
ItemReference.Value shape (rId, name, retired, provenance) but — per
DQ-014 — is a supplier descriptor, not a strict EntityPayload reference:
its name is required (the single home for the vendor name), while its
eId (the VENDOR BusinessRole, per DQ-001) and affiliateEId (parent BA, per
DQ-006) are nullable — null means unlinked/legacy (name-only), non-null
means linked and resolvable via BusinessRoleUniverse(affiliateEId). This
relaxes DQ-002 transitionally: the nullable identity is a migration-window
state that carries legacy unlinked supplies through lazy re-link (DQ-008); once
no unlinked supplies remain, eId/affiliateEId are tightened back to non-null
and the reference regains its canonical shape (see DQ-014 future-convergence
follow-up, PDEV-860). Per DQ-004, ItemSupplyService is extracted into the item
module’s service layer as a peer collaborator of ItemService.
Key Classes and Interfaces
Section titled “Key Classes and Interfaces”SupplierReference
Section titled “SupplierReference”- Package:
cards.arda.operations.reference.item.domain(new) - Responsibility: Cross-Universe supplier descriptor — always carries
the vendor
name; optionally links to the VENDORBusinessRolethat supplies it. - Key fields:
name: String(required — single source of the vendor name),eId: EntityId?(VENDOR role eId — null = unlinked),affiliateEId: UUID?(parent BA eId — null = unlinked),rId(pinned record id, null when floating),retired: Boolean = false,provenance: Provenance.Value?. Not anEntityPayload(identity is lazy). - Design decision: DQ-001, DQ-002 (relaxed), DQ-006, DQ-010, DQ-014.
ItemSupply (modified)
Section titled “ItemSupply (modified)”- Package:
cards.arda.operations.reference.item.business - Responsibility: A way an item can be acquired from a supplier.
- Change (DQ-014): drop the redundant
supplier: String; the requiredsupplier: SupplierReference.Valuedescriptor carries the vendor name and the optional VENDOR-role link (possiblyretired). - Design decision: DQ-001, DQ-002, DQ-014.
ItemSupplyService (new)
Section titled “ItemSupplyService (new)”- Package:
cards.arda.operations.reference.item.service(new file) - Responsibility: Owns the ItemSupply child universe; create/update/remove/
list supplies; resolve supplier references through
BusinessAffiliateService; subscribe to BusinessAffiliate/BusinessRole events and apply name-change propagation and stale-marking. - Key methods:
supplies(parentEId, time),createItemSupply(...),updateItemSupply(...),removeItemSupply(...),resolveSupplier(...),handleVendorNotification(...). - Design decision: DQ-004.
BusinessAffiliateService (possibly extended)
Section titled “BusinessAffiliateService (possibly extended)”- Package:
cards.arda.operations.reference.businessaffiliates.service - Possible additions (pending DQ-006/DQ-007): a “resolve VENDOR role by role eId” capability and/or BusinessAffiliate-deletion notifications that cascade to VENDOR role removals.
API Contract / Endpoints
Section titled “API Contract / Endpoints”Canonical routes follow the Arda structure
{version}/{domain}/{module}/{service}/{endpoint} — five explicit, all-singular
segments; segments stay explicit even when literals repeat (DQ-005). This
project introduces the reference-data domain segment — which url-naming.md
does not yet define; T-17 adds the convention. Per the service split (DQ-004),
ItemSupply is exposed on its own endpoint (item-supply/supply) rather
than under the item endpoint. Legacy routes are retained as aliases per DQ-009.
Scope: these canonical routes are added in the Kotlin/Ktor routing only for now — no Helm chart route/ingress changes and no CloudFormation/CDK changes.
Canonical paths (module/service/endpoint all explicit):
| Resource | {module} | {service} | {endpoint} | Path |
|---|---|---|---|---|
| Item | item | item | item | /v1/reference-data/item/item/item/{id}/… |
| ItemSupply | item | item-supply | supply | /v1/reference-data/item/item-supply/supply/{item-eId}/… |
| BusinessAffiliate | business-affiliate | business-affiliate | business-affiliate | /v1/reference-data/business-affiliate/business-affiliate/business-affiliate/{id}/… |
Supply routes (on the dedicated item-supply/supply endpoint), keyed by the parent {item-eId}:
- Add / list:
POST/v1/reference-data/item/item-supply/supply/{item-eId}/add·GET.../supply/{item-eId}/list - Update / remove:
PUT.../supply/{item-eId}/{supply-id}/update·DELETE.../supply/{item-eId}/{supply-id}/delete(guarded if primary/secondary) - Each canonical route shares the legacy handler; the legacy paths (
/v1/item/item/...,/v1/business-affiliate/business-affiliate/...) remain as aliases (DQ-009). - Authentication: Bearer token +
X-Tenant-Id(unchanged). - Query params (DQ-011):
mutation-mode∈ {STRICT,PROPAGATE(create-on-the-fly),LAX(unlinked) }, defaultPROPAGATE;effectiveAsOf. - Error responses:
400— invalidSupplierReference: STRICT with an unresolvable role, or a resolved-but-retired supplier (DQ-012).404— item or supply not found.409— remove of a supply still referenced as primary/secondary.
Scenarios
Section titled “Scenarios”Sequence walkthroughs across the actors (client, Item module, BusinessAffiliate module) that validate the proposal.
Scenario: Add a supply with a pre-existing supplier (UC 1a / 2a)
Section titled “Scenario: Add a supply with a pre-existing supplier (UC 1a / 2a)”The caller supplies a SupplierReference carrying the VENDOR role eId and its
parent affiliateEId (required per DQ-006, since the role is a child entity).
ItemSupplyService resolves it through BusinessAffiliateService —
BusinessRoleUniverse(affiliateEId) — to validate it and refresh the cached
name, then persists the supply.
Error path (DQ-011, DQ-012): if the role eId does not resolve to a VENDOR role,
or the resolved VENDOR role / parent BusinessAffiliate is retired, STRICT
returns a validation AppError — a new supply may not start dead. Under LAX
the supply is left unlinked; under PROPAGATE the supplier is created
on-the-fly (next scenario), except that a retired supplier is rejected rather
than resurrected.
Scenario: Add a supply creating the supplier on-the-fly (UC 1b / 2c)
Section titled “Scenario: Add a supply creating the supplier on-the-fly (UC 1b / 2c)”In PROPAGATE (create-on-the-fly) mode with no resolvable supplier,
ItemSupplyService asks BusinessAffiliateService to create the
BusinessAffiliate and its VENDOR role, then links the supply.
Scenario: Supplier removed — mark supplies stale (UC 5)
Section titled “Scenario: Supplier removed — mark supplies stale (UC 5)”Mirrors PDEV-808. ItemSupplyService observes a VENDOR BusinessRole deletion,
finds every supply referencing that role, stamps retired = true + provenance,
freezes rId to the tombstone, persists the supply (no delete), and updates the
parent Item’s primary/secondary references the same way.
Resolution on read (per DQ-010): a floating reference resolves via the live VENDOR role; a retired+pinned reference resolves via the supplier tombstone record so the supply still renders its last-known supplier state.
Testing Strategy
Section titled “Testing Strategy”Validates the scenarios above plus the persistence and migration changes.
Unit Tests
Section titled “Unit Tests”| Test | Target | Validates |
|---|---|---|
| Resolve existing VENDOR role (STRICT) | ItemSupplyService.resolveSupplier | Reference validated, name cached |
| Create supplier on-the-fly | ItemSupplyService.createItemSupply | BA + VENDOR role created and linked |
| STRICT against unresolvable role rejected | ItemSupplyService.resolveSupplier | Validation AppError, no LAX fallback (DQ-011) |
| STRICT against retired supplier rejected | ItemSupplyService.resolveSupplier | Validation AppError — no new dead link (DQ-012) |
| Supplier removal marks stale | ItemSupplyService notification handler | retired=true, rId pinned, supply not deleted |
| Stale reference resolves to tombstone | reference resolution | Read returns last-known supplier state |
| Remove primary/secondary supply blocked | ItemSupplyService.removeItemSupply | Returns conflict per existing guard |
Integration Tests
Section titled “Integration Tests”| Test | Setup | Validates |
|---|---|---|
| End-to-end create+link with ContainerizedPostgres | migrated schema | Persistence of SUPPLIER_REFERENCE_* columns |
| Migration (lazy re-link) | seed legacy supplier_eid rows | Migration applies; legacy rows read back as unlinked/floating, re-link on edit (DQ-008) |
API Tests
Section titled “API Tests”| Test | Method | Path | Expected |
|---|---|---|---|
| Add supply with existing supplier | POST | .../reference-data/item/item-supply/supply/{item-eId}/add | 200, reference resolved |
| Add supply creating supplier | POST | .../reference-data/item/item-supply/supply/{item-eId}/add | 200, BA+role created |
| List supplies | GET | .../reference-data/item/item-supply/supply/{item-eId}/list | 200, includes supplier ref |
| Remove primary supply | DELETE | .../item/item-supply/supply/{item-eId}/{supply-id}/delete | 409 guard |
| Supplier removal staleness | DELETE BA role then GET supply | — | supply present, retired=true |
References
Section titled “References”- PDEV-731 — Connect Item.Supply to Business Affiliate References — the project ticket
- Decision Log
- Project Plan
- Goal
- Early Design Review proposal format — PDEV-766 workshop comment
- Entity references pattern —
domain/information-model/meta/entity-references.md - Data Authority / cross-service isolation —
current-system/architecture/patterns/module/data-authority-pattern.md - Kanban-reacts-to-Item-deletion (PDEV-808) —
resources/kanban/service/ServiceImpl.kt, migrationV011__item_reference_provenance.sql(operations)
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved