Skip to content

Decision Log: Connect Item.Supply to Business Affiliate References

Tracks the design decisions for connecting ItemSupply to a real supplier in the BusinessAffiliate module at the API/backend layer — the reference target and form, the reaction to supplier removal, the service-boundary and URL-routing refactorings, and the cross-Universe resolution and migration questions that follow from them.

#QuestionStatusDecisionRound
DQ-001Reference target: BusinessAffiliate vs BusinessRole(VENDOR)DecidedBusinessRole(VENDOR)R1
DQ-002Reference form: canonical Reference value object vs plain UUID+nameDecidedCanonical SupplierReference.ValueR1
DQ-003Effect of supplier removal on an ItemSupplyDecidedStale marker (PDEV-808 pattern), no deleteR1
DQ-004Extract a dedicated ItemSupplyService?DecidedYesR1
DQ-005Adopt canonical reference-data URL routes?DecidedYesR1
DQ-006Resolving a child BusinessRole across the Universe boundaryDecidedCarry affiliateEId in the referenceR1
DQ-007Event source for supplier removalDecidedBusinessRole DeleteEntity observer notification (in-memory)R3
DQ-008Migration/backfill of existing supplierEId (BA eId → VENDOR role)DecidedLazy re-link — no backfillR1
DQ-009Backward compatibility of legacy routes + api-test path migrationDecidedKeep legacy routes as aliasesR1
DQ-010Floating-vs-pinned semantics on retirementDecidedMirror Kanban (floating; pin on retire)R1
DQ-011Supply mutation qualifier set for ItemSupply creationDecidedSTRICT + create-on-the-fly only; drop LAXR1
DQ-012Creating a supply that points to a retired supplierDecidedDisallowed — reject at creationR1
DQ-013Representing an unlinked/legacy vendor name (mandatory reference eId)Supersededby DQ-014R2
DQ-014Eliminating the redundant supplier name (supplier vs supplierRef.name)DecidedSupplier descriptor: name on the reference, drop supplierR2
DQ-015Must a supplied supplier roleEId be the affiliate’s actual VENDOR role?DecidedYes — verify against businessRolesFor; reject a mismatchR3
DQ-016How do embedded primary/secondary supplies resolve on item create/update?DecidedCreate-on-the-fly (PROPAGATE-style) regardless of item qualifier, incl. name propagationR3

DQ-001: Reference target — BusinessAffiliate or BusinessRole(VENDOR)?

Section titled “DQ-001: Reference target — BusinessAffiliate or BusinessRole(VENDOR)?”

Context: Today ItemSupply.supplierEId holds a BusinessAffiliate eId, while the domain docs say a supply references the BusinessRole(VENDOR). The two must be reconciled before the reference type is designed.

OptionDescriptionTrade-offs
AReference the BusinessAffiliate eId (match current code)Simpler; a supplier is “the company”. But loses the “acting as vendor” precision and contradicts docs.
BReference the BusinessRole(VENDOR) eId (match docs)Precise about the vendor capability; aligns docs+model. Requires resolving a child entity (see DQ-006) and a backfill (DQ-008).

Recommendation: Option B — it matches the documented model and the Reference domain intent.

Decision: Option BSupplierReference.eId is the VENDOR BusinessRole eId. (Confirmed in design session 2026-06-15.)

Applied to:

  • Design Document § Overview, § Class Diagram, § Key Classes (SupplierReference)

DQ-002: Reference form — canonical Reference value object or plain UUID + name?

Section titled “DQ-002: Reference form — canonical Reference value object or plain UUID + name?”

Context: The supply link can stay a bare UUID + denormalized name, or adopt the canonical reference value-object pattern used by ItemReference / KanbanCard (eId, rId, cached name, floating/pinned).

OptionDescriptionTrade-offs
AKeep plain supplierEId: UUID? + supplier: StringLeast churn; already cross-Universe compliant. No room for retired/rId/provenance, so the stale-marker (DQ-003) has nowhere to live.
BAdopt canonical SupplierReference.Value (eId, rId, name, retired, provenance)Consistent with KanbanCard/OrderLine; carries the fields needed for staleness and tombstone reads. Requires a persistence migration.

Recommendation: Option B — required to support DQ-003 cleanly and consistent with the platform’s reference pattern.

Decision: Option B — introduce SupplierReference.Value mirroring ItemReference.Value. (Confirmed 2026-06-15.)

Applied to:

  • Design Document § Class Diagram, § Key Classes, § Implementation Scope

DQ-003: Effect of supplier removal on an ItemSupply

Section titled “DQ-003: Effect of supplier removal on an ItemSupply”

Context: UC 5 requires the Item module to react to supplier removal. The supply may be referenced by pending business workflows already committed to its values, so destructive cleanup is risky.

OptionDescriptionTrade-offs
ADelete/retire the affected ItemSupplyClean state, but breaks in-flight workflows referencing the supply.
BUnlink the supplier but keep the supply unmarkedPreserves data but gives no signal that the supplier is gone.
CMark the supplier reference stale (retired=true + pinned tombstone), keep the supplyPreserves data and signals staleness; identical to the proven Kanban↔Item (PDEV-808) pattern. More machinery (migration columns, resolution path).

Recommendation: Option C — matches the existing, proven pattern and the stated requirement to protect committed workflows.

Decision: Option C — stale-marker reaction mirroring PDEV-808; the ItemSupply is never deleted by supplier removal. (Confirmed 2026-06-15.)

Applied to:

  • Design Document § Overview, § Sequence: Supplier removed, § Testing Strategy

DQ-004: Extract a dedicated ItemSupplyService?

Section titled “DQ-004: Extract a dedicated ItemSupplyService?”

Context: Supply logic is a ~400-line private inner SupplyService inside ItemService. Refactoring item #3.1 of the ticket asks whether to extract it.

OptionDescriptionTrade-offs
AKeep the inner SupplyServiceNo refactor risk; but the class stays large and the new event-subscription responsibility is buried.
BExtract a dedicated ItemSupplyService owning the child universe and BA subscriptionClear ownership and a natural home for the stale-marker subscriber; small risk of churn in ItemService wiring.

Recommendation: Option B — gives the new reaction logic a clean owner.

Decision: Option B — extract ItemSupplyService. (Confirmed 2026-06-15.)

Applied to:

  • Design Document § Class Diagram, § Key Classes (ItemSupplyService), § Implementation Scope

DQ-005: Adopt canonical reference-data URL routes?

Section titled “DQ-005: Adopt canonical reference-data URL routes?”

Context: Refactoring item #3.2. No reference-data segment exists today; Item is /v1/{module}/item, BA is /v1/business-affiliate/.... The url-naming convention does not yet define reference-data.

OptionDescriptionTrade-offs
ALeave routes as-isNo work; but the modules remain inconsistent with the intended domain grouping.
BIntroduce canonical {version}/{domain}/{module}/{service}/{endpoint} routes (five explicit, all-singular segments) with domain = reference-data; ItemSupply gets its own item-supply/supply endpoint (e.g. /v1/reference-data/item/item/item/{id}/..., /v1/reference-data/item/item-supply/supply/{item-eId}/..., /v1/reference-data/business-affiliate/business-affiliate/business-affiliate/...). Kotlin/Ktor routes only for now — no Helm/Cfn changes.Consistent domain-scoped routing; requires url-naming.md to add the {domain} segment and api-test path migration (see DQ-009).

Recommendation: Option B — establishes the canonical convention the ticket asks for.

Decision: Option B — adopt reference-data routes; document the convention in url-naming.md. (Confirmed 2026-06-15.)

Applied to:


DQ-006: How is a child BusinessRole resolved across the Universe boundary?

Section titled “DQ-006: How is a child BusinessRole resolved across the Universe boundary?”

Context: A BusinessRole lives in a child universe parametrized by its parent BA eId, so a reference holding only the role eId cannot be resolved the way a top-level Item reference can (the Kanban pattern never faced this).

OptionDescriptionTrade-offs
ASupplierReference carries both the role eId and the parent affiliateEIdSelf-sufficient resolution via existing BusinessRoleUniverse(parentEId). Slightly larger reference; must keep affiliateEId correct.
BAdd a BusinessAffiliateService.resolveVendorRole(roleEId, asOf) that searches across affiliatesReference stays minimal (role eId only); new cross-affiliate query capability and its cost.
CReference the BA eId for resolution but tag the role typeReintroduces the BA-vs-role ambiguity DQ-001 resolved; not recommended.

Recommendation: Option A — cheapest resolution, no new query surface; affiliateEId is known at link time.

Decision: Option ASupplierReference carries a (required) affiliateEId alongside the role eId; resolution uses the existing BusinessRoleUniverse(parentEId). (Confirmed 2026-06-15.)

Applied to:

  • Design Document § Class Diagram (affiliateEId now required), § Key Classes (SupplierReference)

Context: The stale-marker reaction needs a reliable event. Today the Item module subscribes only to BusinessRole notifications and ignores deletes; it is unclear whether deleting a whole BusinessAffiliate emits per-VENDOR-role DeleteEntity events.

OptionDescriptionTrade-offs
ARely on cascade: BA deletion emits DeleteEntity for each child VENDOR role; ItemSupplyService subscribes to role deletes onlyOne subscription; depends on BA delete actually cascading role-delete events (must verify/implement).
BItemSupplyService subscribes to both BusinessRole and BusinessAffiliate DeleteEntityRobust to either deletion path; two handlers and dedup logic.

Recommendation: Option A if BA-delete cascade emits role events; otherwise B. Verify cascade behavior during Phase 1 implementation.

Decision: Deferred to implementation — verify in code whether deleting a BusinessAffiliate emits per-VENDOR-role DeleteEntity events. If yes, take Option A (subscribe to role deletes only); if not, take Option B (subscribe to both). Record the resolved choice here before Phase 3 (T-08).

Applied to: pending (recorded at T-08)


DQ-008: Migration/backfill of existing supplierEId

Section titled “DQ-008: Migration/backfill of existing supplierEId”

Context: Existing rows hold a BA eId in supplier_eid; the new model wants the VENDOR role eId. The migration must convert or re-link.

OptionDescriptionTrade-offs
AData migration: for each row, look up the BA’s VENDOR role and write its eId into SUPPLIER_REFERENCE_entity_idPreserves links; needs a deterministic “the VENDOR role” per BA (what if 0 or >1?).
BLeave legacy rows unlinked (retired/floating by name) and re-link lazily on next editSimplest migration; temporary inconsistency until rows are touched.
CDefault columns + backfill in a follow-up data taskDecouples schema change from data conversion; two steps.

Recommendation: Option A where exactly one VENDOR role exists; fall back to B for ambiguous rows. Confirm the data shape in the target environments first.

Decision: Option B — lazy re-link, no data backfill. The migration adds the new SUPPLIER_REFERENCE_* columns and drops the legacy supplier_eid / supplier columns; existing supplies become unlinked (floating by cached name) and are re-linked to a VENDOR role on their next edit. This avoids a brittle one-time BA→VENDOR-role conversion and the ambiguous 0-or-many-roles cases. (Confirmed 2026-06-15.)

Applied to:


DQ-009: Backward compatibility of legacy routes + api-test migration

Section titled “DQ-009: Backward compatibility of legacy routes + api-test migration”

Context: Introducing reference-data routes (DQ-005) affects existing API consumers and the Bruno suite, which uses /v1/item/item/... and /v1/business-affiliate/....

OptionDescriptionTrade-offs
AKeep legacy routes as permanent aliases alongside canonical routesNo consumer breakage; two route sets to maintain.
BAdd canonical routes, deprecate legacy with a removal window, migrate api-test nowClean end state; coordination with FE tickets (PDEV-764) and a deprecation period.
CReplace legacy routes outrightSmallest surface; breaks current consumers immediately.

Recommendation: Option A for this project (non-breaking), with a separate deprecation ticket — given FE work (PDEV-764) is in flight.

Decision: Option A — keep legacy routes as permanent aliases for this project; file a separate deprecation ticket. (Confirmed 2026-06-15.)

Applied to:


DQ-010: Floating-vs-pinned semantics on retirement

Section titled “DQ-010: Floating-vs-pinned semantics on retirement”

Context: The Kanban pattern keeps references floating normally and pins rId to the tombstone at retirement so the reference still resolves. Confirm the same for SupplierReference.

OptionDescriptionTrade-offs
AMirror Kanban: floating while live; pin rId + set retired on supplier removal; resolve retired refs via getRecord(rId)Proven; consistent platform behavior.
BAlways floating; detect retirement purely at read timeSimpler write path; loses the frozen last-known snapshot and depends on the role record still being queryable.

Recommendation: Option A — consistency with PDEV-808 and reliable tombstone reads.

Decision: Option A — mirror Kanban: floating while live; pin rId and set retired on supplier removal; resolve retired refs via getRecord(rId). (Confirmed 2026-06-15.)

Applied to:

  • Design Document § Sequence: Supplier removed, § Behavioral Design (resolution note)

DQ-011: Supply mutation qualifier set for ItemSupply creation

Section titled “DQ-011: Supply mutation qualifier set for ItemSupply creation”

Context: The existing system offers three supply-mutation qualifiers — STRICT, LAX, PROPAGATE. For this project the supply semantics are uniform: the caller is expected to reference a real, resolvable supplier, with one sanctioned alternative — create the supplier on-the-fly. The permissive LAX mode (accept an unlinked supply, silently tolerate mismatches) is not wanted.

OptionDescriptionTrade-offs
AKeep all three qualifiers (STRICT, LAX, PROPAGATE)Backward compatible; but retains the permissive LAX path that this project does not want and that weakens the reference guarantee.
BOffer only STRICT (default) and create-on-the-fly; remove LAXUniform, strong reference semantics; one clear escape hatch. LAX callers (if any) must move to STRICT or on-the-fly; default flips to STRICT.

Recommendation: Option B — uniform strict semantics with a single, explicit alternative is simpler to reason about and matches the project intent.

Decision: Option B — ItemSupply creation accepts only STRICT (default; the referenced VENDOR role must resolve) and create-on-the-fly (create the BusinessAffiliate + VENDOR role when the supplier is not found, then link). The on-the-fly mode applies strict validation otherwise — it does not silently tolerate mismatched data. (Confirmed 2026-06-15.)

Implementation note (R2, 2026-06-15): ItemMutationQualifier is a single enum shared between item-level mutations (bulk add() legitimately uses LAX) and supply resolution, so LAX cannot be removed outright. Accepted refinement: DQ-011 removes the LAX name-lookup upsert for supplies — under LAX a supply is left unlinked (null supplierRef, name-only), with no name-based lookup or auto-creation. STRICT (require a live ref) and PROPAGATE (create-on-the-fly) are unchanged. So the three supply behaviors are: STRICT = require a live VENDOR role; PROPAGATE = find-or-create; LAX = leave unlinked. The endpoint default qualifier remains PROPAGATE — the DQ-011-envisioned flip to a STRICT default was not applied in Phase 1. Open follow-up (PDEV-864): revisit whether the supply default should become STRICT.

Applied to:

  • Design Document § Goal, § API Contract, § Scenarios, § Testing Strategy
  • Project Plan § Phase 2, T-06
  • operationsItemVendorResolver.resolveVendorRef

DQ-012: Creating a supply that points to a retired supplier

Section titled “DQ-012: Creating a supply that points to a retired supplier”

Context: A supplier (its VENDOR BusinessRole, or the parent BusinessAffiliate) can be retired (deleted). Existing supplies that referenced it are marked stale per DQ-003. This question is about new supplies: may a newly created (or newly linked) ItemSupply target an already-retired supplier?

OptionDescriptionTrade-offs
AAllow linking to a retired supplier (immediately stale)Permissive; but lets callers create dead links on purpose, contradicting strict semantics.
BDisallow — STRICT resolution rejects a retired VENDOR role/BA; creation fails with a validation errorNew supplies always point at live suppliers; consistent with the strong reference guarantee (DQ-011). Create-on-the-fly is unaffected (it creates a live supplier).

Recommendation: Option B — a freshly created link should never start dead.

Decision: Option B — when resolving a supplier for ItemSupply creation, a retired VENDOR role (or retired parent BusinessAffiliate) is rejected: creation returns a validation AppError. Staleness applies only to supplies that linked the supplier before it was retired (DQ-003); it is never a valid starting state for a new supply. Create-on-the-fly always yields a live supplier and is therefore unaffected. (Confirmed 2026-06-15.)

Implementation note (R2, 2026-06-15): Enforcement leans on bitemporal retired-filtering. BusinessRole is a bitemporal child entity, and BusinessAffiliateService.detailsFor / businessRolesFor return only the latest non-retired records (… and (baTable.retired eq false)). So a retired VENDOR role / affiliate is simply absent from the active query → STRICT and LAX resolution fail naturally (resolved as “not a VENDOR” / NotFound). The one path that needed explicit handling is PROPAGATE: because the retired role is filtered out, PROPAGATE would otherwise re-create (resurrect) a VENDOR role with the same eId. Per the R2 ruling, PROPAGATE now checks for a retired role / affiliate (retired-inclusive read) and fails rather than resurrects.

Applied to:

  • Design Document § Scenarios (STRICT error path), § API Contract (400), § Testing Strategy
  • operationsItemVendorResolver.resolveWithExistingRef, ItemVendorResolverTest

Round 2: Questions Surfaced During Implementation

Section titled “Round 2: Questions Surfaced During Implementation”

DQ-013: Representing an unlinked/legacy vendor name

Section titled “DQ-013: Representing an unlinked/legacy vendor name”

Context: SupplierReference.eId (the VENDOR BusinessRole id) is mandatory, mirroring ItemReference. But two cases have a vendor name with no resolved role: legacy rows under lazy re-link (DQ-008), and the general need to round-trip a supply whose role link is absent. A single non-null supplier: SupplierReference.Value field (as first sketched) cannot represent “name, not yet linked.”

OptionDescriptionTrade-offs
AKeep ItemSupply.supplier: String (vendor name, always present) and add supplierRef: SupplierReference.Value? (nullable canonical link)Preserves names for legacy/lazy re-link; closest to today’s shape; smallest blast radius (.supplier usages unchanged, only .supplierEId.supplierRef?.eId). Two fields instead of one.
Bsupplier: SupplierReference.Value? (nullable); name only inside the referenceOne field, but a null reference loses the legacy name → breaks re-link-by-name.
CMake SupplierReference.eId nullable (name required, eId optional)One field, keeps name, but breaks the canonical-reference contract (ItemReference keys on a non-null eId).

Recommendation: Option A — the only option that preserves vendor names for lazy re-link while keeping SupplierReference faithful to the canonical pattern.

Decision: Option AItemSupply (and ItemSupplyReference) keep supplier: String and replace supplierEId: UUID? with supplierRef: SupplierReference.Value?. A null supplierRef means unlinked (name-only); a non-null supplierRef is the canonical link, which may be retired. (Confirmed 2026-06-15.)

Applied to:

  • Design Document § Class Diagram, § Key Classes (ItemSupply), § Functional Modules
  • operationsItemSupply.kt, ItemSupplyReference.kt, SupplierReferenceComponent, migration

DQ-014: Eliminating the redundant supplier name

Section titled “DQ-014: Eliminating the redundant supplier name”

Context (surfaced in PR review of #193): DQ-013 kept ItemSupply.supplier: String (the vendor name) and added a nullable supplierRef: SupplierReference.Value whose name cached the same vendor name — two fields holding the same string, with an out-of-sync risk. The reviewer asked to consolidate to a single source of truth.

OptionDescriptionTrade-offs
AKeep supplier: String; drop SupplierReference.name. Reference stays a strict canonical reference (non-null eId/affiliateEId); name is an intrinsic supply field.Keeps DQ-002 (canonical reference). Name lives on the supply; the reference is a pure link.
BDrop supplier: String; name lives in the reference. Make SupplierReference a supplier descriptor: name required, eId/affiliateEId nullable (null = unlinked/legacy).Single field, no duplication; name always travels with the descriptor. Relaxes DQ-002 — the reference is no longer a strict EntityPayload reference (nullable identity); the persistence existence-check moves from eId to name; rId/retired/affiliateEId/provenance are meaningful only when linked.

Recommendation: noted both; the reviewer chose B.

Decision: Option BSupplierReference becomes a supplier descriptor: name: String (required) is the single home for the vendor name; eId and affiliateEId are nullable (null = unlinked/legacy, name-only; non-null = linked to a VENDOR BusinessRole). ItemSupply.supplier: String and ItemSupplyReference.supplier: String are removed; the descriptor field is required on both. This supersedes DQ-013 and relaxes DQ-002: the supplier link is no longer a strict EntityPayload reference but a descriptor whose identity is established lazily. The V018 migration moves the legacy supplier name into supplier_ref_name (no name loss) and drops the legacy columns; the component’s existence check is keyed on name. (Confirmed 2026-06-15.)

Transitional framing (design intent): the relaxation of DQ-002 is temporary. The nullable eId/affiliateEId exist only to carry legacy unlinked supplies through the lazy re-link migration (DQ-008). As those supplies are edited they re-link and acquire a real VENDOR-role identity; once no unlinked supplies remain, eId/affiliateEId can be tightened back to non-null and SupplierReference regains its canonical reference shape (DQ-002). Treat the nullable identity as a migration-window state, not the permanent model. Future-convergence follow-up: when the count of supplies with a null supplier_ref_entity_id reaches zero (across partitions), make the fields non-null (a schema + type tightening) and restore the strict EntityPayload reference. Tracked as PDEV-860.

Applied to:

  • Design Document § Goal (Key Insight), § Decisions, § Class Diagram, § Key Classes
  • operationsSupplierReference, SupplierReferenceComponent, ItemSupply, ItemSupplyReference, V018 migration, resolver, stale/propagation logic, Model.kt

Round 3: Resolutions Confirmed During Implementation & PR Review

Section titled “Round 3: Resolutions Confirmed During Implementation & PR Review”

These close the one deferred question and record two decisions surfaced while reviewing the implementation in operations PR #193.

DQ-007: Event source for supplier removal — resolved

Section titled “DQ-007: Event source for supplier removal — resolved”

Context: R1 deferred the choice between (A) a BusinessAffiliate-deletion cascade and (B) a subscription to BusinessRole removal, pending verification of the in-code cascade.

Resolution: Option B — the Item module subscribes to the BusinessAffiliate module’s in-memory role observer and reacts to a DataAuthorityNotification.DeleteEntity for a BusinessRole of type VENDOR. ItemSupplyService.setParentService registers an ItemSupplyRoleListener via BusinessAffiliateService.addRoleObserver; on a VENDOR DeleteEntity the reaction marks referencing supplies stale (DQ-003). No new event infrastructure and no cross-Universe cascade was introduced — it reuses the existing in-process Observer pattern. (Confirmed in code 2026-06-16.)

DQ-015: Must a supplied supplier roleEId be the affiliate’s actual VENDOR role?

Section titled “DQ-015: Must a supplied supplier roleEId be the affiliate’s actual VENDOR role?”

Context: when a caller supplies a linked SupplierReference (eId + affiliateEId), resolution originally checked only that the affiliate had a VENDOR role, then persisted the client-supplied eId unverified.

Decision: Verify the role identity. Resolution now resolves the affiliate’s actual VENDOR role (via businessRolesFor) and rejects the request when the supplied eId is not that role — otherwise supplier_ref_entity_id could point at a role that never receives the vendor rename/retire reactions. Applies in STRICT, LAX, and PROPAGATE. (PR #193 review.)

DQ-016: How do embedded primary/secondary supplies resolve on item create/update?

Section titled “DQ-016: How do embedded primary/secondary supplies resolve on item create/update?”

Context: the embedded primary/secondary supplies carried on an Item create/update were written straight to the supply universe without vendor resolution, so a name-only embedded supply was persisted unlinked (no VENDOR role created/attached) and later supplier rename/retire reactions could not find it.

Decision: Resolve embedded supplies create-on-the-fly (PROPAGATE-style), regardless of the item mutation qualifier — a name-only embedded supply finds-or-creates its BusinessAffiliate + VENDOR role rather than persisting unlinked, and a name change on a linked embedded supply is propagated to the BusinessAffiliate (explicitly approved). Rationale: an embedded supply’s supplier cannot be pre-created in a separate call, so STRICT/LAX item operations still create/link the supplier on the fly; STRICT’s “verify, no side-effects” character therefore applies to the supply-field consistency check, not to the embedded supplier link. (PR #193 review.) See the design’s Supplier resolution semantics framing.

Applied to: operationsItemVendorResolver.resolveWithExistingRef (DQ-015), ItemSupplyCrudService.addMultipleToParent (DQ-016), with tests in ItemVendorResolverTest, CrossItemSupplyUniverseTest, and the item create/update suites.



Copyright: (c) Arda Systems 2025-2026, All rights reserved