Skip to content

Vendor & VendorItem

1. Overview

PropertyValue
IDFEAT-INV-VEN
StatusStable
Ownerinventory-team
Depends onMaterial / ProductVariant, UnitOfMeasure, PurchaseOrder (caller of recordPurchase)

Vendor is a supplier. VendorItem is the M:N junction linking a vendor to a principal item (Material or ProductVariant) for a specific merchant — with quoted price, UoM, multiplier, preferred flag, and a snapshot of the last invoiced terms. Principals never carry vendorId (ADR-0002).

2. Entity Model

Vendor fields

FieldTypeRequiredDescription
merchantIdtextOwner
identifiertextAuto, VEN prefix
slugtextURL-safe
namei18n jsonbDisplay
descriptioni18n jsonb
statustextACTIVATED / DEACTIVATED / ARCHIVED (default ACTIVATED)
locationjsonb{ main, sub, long, lat, postCode }
taxNumbertextTax registration
currencytextDefault VND
contactsjsonb (array)Array<IVendorContact> (default [])
notetextFree-form

VendorItem fields

FieldTypeRequiredDescription
vendorIdtextFK to Vendor
merchantIdtextDenormalized from Vendor.merchantId
itemTypetextMATERIAL / PRODUCT_VARIANT
itemIdtextFK target id (polymorphic)
uomIdtextVendor's catalog UoM (soft ref)
multiplierdecimal(15,4)UoM-to-base conversion
unitPricedecimal(15,4)Quoted price per UoM
isPreferredbooleanPartial unique per (merchantId, itemType, itemId) — at most one preferred per item
statustextACTIVATED / DEACTIVATED / ARCHIVED
lastInvoicedjsonbSnapshot from latest PO receive: { unitPrice, uomId, multiplier, orderedAt, receivedAt }

3. Lifecycle

FromEventTo
ACTIVATEDdeactivateDEACTIVATED
DEACTIVATEDactivateACTIVATED
* (non-archived)archiveARCHIVED (terminal)

Both Vendor and VendorItem share this lifecycle.

4. Operations

VendorService (vendor.service.ts — 313 lines)

MethodSignaturePurpose
createAggregate{ context, data: TCreateVendorAggregateRequest }Create Vendor + (optional) VendorItem batch in one TX. Aggregate field name: items[] (per-parent semantic — see memory rule feedback_aggregate_field_naming)
updateAggregate{ context, id, data: TUpdateVendorAggregateRequest }Patch Vendor + grant/omit VendorItem batch

VendorItemService (vendor-item.service.ts — 772 lines)

MethodSignaturePurpose
create (override){ context, data, transaction? }Create VendorItem; validates vendor.merchantId === context.merchantId; atomic isPreferred flip if isPreferred=true
updateById (override){ context, id, data, transaction? }Update; same isPreferred atomicity
setPreferred{ context, id }Promote to vendor's preferred source for this item — atomic via setPreferredAtomic repo method
activate{ context, id }DEACTIVATED → ACTIVATED
deactivate{ context, id }ACTIVATED → DEACTIVATED
recordPurchase{ vendorItemId, unitPrice, uomId, multiplier, orderedAt?, receivedAt? }Update lastInvoiced snapshot — called by PurchaseOrderService on receive; bypasses scope check (service-internal)
upsertBatch{ context, merchantId, entries: TVendorItemAssignment[], transaction }Bulk create/update — used by Vendor aggregate
omitBatch{ context, vendorId, itemIds, transaction }Soft-delete (archive) batch

5. REST Endpoints

/vendors

VerbPathAuthPermissionHandler
6× CRUD/vendorsJWT/BASICVendor.<crud>CRUD
POST/vendors/aggregateJWT/BASICVendor.createAggregateVendorService.createAggregate
PATCH/vendors/:id/aggregateJWT/BASICVendor.updateAggregateVendorService.updateAggregate

/vendor-items

VerbPathAuthPermissionHandler
6× CRUD/vendor-itemsJWT/BASICVendorItem.<crud>CRUD (with vendor↔merchant validation)
POST/vendor-items/:id/set-preferredJWT/BASICVendorItem.setPreferredsetPreferred
POST/vendor-items/:id/activateJWT/BASICVendorItem.activateactivate
POST/vendor-items/:id/deactivateJWT/BASICVendorItem.deactivatedeactivate

recordPurchase, upsertBatch, omitBatch are not exposed as REST — internal-only.

6. Events

Inbound: none directly.

Outbound: none directly. Vendor / VendorItem mutations are persisted only.

7. Key Patterns

Atomic isPreferred flip

VendorItemRepository.setPreferredAtomic performs in a single statement:

  1. Demote all isPreferred=true rows where (merchantId, itemType, itemId) = target's group AND id ≠ target.id.
  2. Promote target to isPreferred=true.

Combined with the partial unique index on (merchantId, itemType, itemId) WHERE isPreferred=true, this guarantees at most one preferred per item.

lastInvoiced snapshot from PO receive

Skips scope check because called inside an authenticated PO receive flow.

Backfill from existing PO history

inventory-0005-backfill-vendor-item.ts (one-shot migration) materializes VendorItem rows for (merchantId, vendorId, itemType, itemId) groups by snapshotting the most recent non-cancelled PO line.

8. Aggregate field naming

Per memory rule feedback_aggregate_field_naming:

AggregateChild collection field
Vendor → VendorItemsitems[] (vendor-centric: "the items this vendor supplies")
Material → VendorItemsvendors[] (material-centric: "the vendors that supply this material")

Same junction table, different semantic naming per parent context.

Proprietary and Confidential. Unauthorized copying, distribution, or use of this software is strictly prohibited.