Commodity
Introduction
The Commodity entity represents individual freight items, packages, or cargo pieces in the TMS. As an aggregate root, Commodity supports sophisticated features including hierarchical container structures (commodities containing other commodities), multiple tracking numbers, warehouse location tracking, and automatic charge recalculation triggers.
Commodities are highly flexible, supporting various measurement systems (imperial/metric), package types, inventory integration, and custom values for extensibility. The entity automatically maintains calculated totals for weight, volume, and value, cascading changes to parent containers when nested items are modified.
Entity Structure
Properties
| Property Name | Type | Required | Description |
|---|---|---|---|
| CommodityId | int | Yes | Unique identifier for the commodity (primary key) |
| OrganizationId | int | Yes | Organization owning this commodity (multi-tenancy) |
| Description | string | Yes | Commodity description/name |
| Pieces | int | Yes | Number of pieces/packages |
| CommodityTypeId | int? | No | Type classification (e.g., Electronics, Textiles) |
| CommodityType | CommodityType | No | Navigation to type definition |
| CommodityStatusId | int? | No | Current status (e.g., In Warehouse, Shipped) |
| CommodityStatus | CommodityStatus | No | Navigation to status definition |
| PackageTypeId | int? | No | Package type (carton, pallet, crate, etc.) |
| PackageType | PackageType | No | Navigation to package type |
| ContainerCommodityId | int? | No | Parent container ID for hierarchical nesting |
| ContainerCommodity | Commodity | No | Parent container (self-reference) |
| ContainerCommodities | ICollection<Commodity> | No | Child commodities in this container |
| Length | decimal? | No | Length dimension |
| Width | decimal? | No | Width dimension |
| Height | decimal? | No | Height dimension |
| DimensionsUnit | DimensionsUnit | Yes | Unit for dimensions (Inch, Centimeter, Meter) |
| Weight | decimal? | No | Weight per piece (or total if WeightByTotal=true) |
| WeightTotal | decimal? | No | Total weight (auto-calculated: Weight × Pieces) |
| WeightByTotal | bool | Yes | If true, Weight field is total (not per piece) |
| WeightUnit | WeightUnit | Yes | Unit for weight (Pound, Kilogram, Ton) |
| VolumePiece | decimal? | No | Volume per piece (auto-calculated from dimensions) |
| VolumeTotal | decimal? | No | Total volume (auto-calculated: VolumePiece × Pieces) |
| VolumeUnit | VolumeUnit | Yes | Unit for volume (CubicFoot, CubicMeter, Liter) |
| Quantity | int? | No | Quantity (distinct from Pieces - e.g., units within packages) |
| Unit | string? | No | Unit of measure for Quantity (e.g., "EA", "BOX") |
| UnitaryValue | decimal? | No | Value per unit |
| UnitaryValueTotal | decimal? | No | Total value (auto-calculated) |
| ValueByTotal | bool | Yes | If true, UnitaryValue is total value |
| BillToContactId | int? | No | Contact to bill for this commodity (charge filtering) |
| BillToContact | Contact | No | Navigation to billing contact |
| WarehouseLocationId | int? | No | Current warehouse location |
| WarehouseLocation | WarehouseLocation | No | Navigation to warehouse location |
| JobId | Guid? | No | Associated job |
| Job | Job | No | Navigation to job |
| InventoryItemId | int? | No | Linked inventory/SKU item |
| InventoryItem | InventoryItem | No | Navigation to inventory item |
| SerialNumber | string? | No | Serial number for the commodity |
| Note | string? | No | Additional notes/comments |
| IsDeleted | bool? | No | Soft delete flag (default: false) |
| CustomValues | Dictionary<string, object?> | No | Extensible custom properties dictionary |
| CustomValues contact IDs | int? | No | Any custom value key that stores a contact ID can be resolved through GraphQL getContact(idPropertyName) |
| SearchVector | NpgsqlTsVector | No | Full-text search vector (PostgreSQL) |
| Created | DateTime | Yes | Creation timestamp (inherited from AuditableEntity) |
| CreatedBy | string | Yes | User ID who created (inherited) |
| LastModified | DateTime | Yes | Last modification timestamp (inherited) |
| LastModifiedBy | string | Yes | User ID who last modified (inherited) |
Relationships (Navigation Properties)
| Relationship | Type | Description |
|---|---|---|
| OrderCommodities | ICollection<OrderCommodity> | Junction to orders containing this commodity |
| CommodityTrackingNumbers | ICollection<CommodityTrackingNumber> | Multiple tracking numbers |
| CommodityTags | ICollection<CommodityTag> | Tags assigned to this commodity |
| AllTags | ICollection<CommodityAllTagsView> | View of all tags (flattened) |
| ContainerCommodity | Commodity | Parent container (self-reference) |
| ContainerCommodities | ICollection<Commodity> | Child commodities in this container |
| Organization | Organization | Parent organization |
| CommodityType | CommodityType | Type classification |
| CommodityStatus | CommodityStatus | Current status |
| PackageType | PackageType | Package classification |
| WarehouseLocation | WarehouseLocation | Current warehouse location |
| Job | Job | Associated job |
| InventoryItem | InventoryItem | Linked inventory SKU |
| BillToContact | Contact | Billing contact for charge filtering |
| Dynamic custom-value contact | Contact | Resolved by GraphQL getContact(idPropertyName) from a contact ID stored in CustomValues |
Enumerations
DimensionsUnit:
Inch(0)Centimeter(1)Meter(2)
WeightUnit:
Pound(0)Kilogram(1)Ton(2)
VolumeUnit:
CubicFoot(0)CubicMeter(1)Liter(2)
Key Concepts
1. Hierarchical Container System
Commodities can contain other commodities through the ContainerCommodityId relationship:
Container Commodity (ContainerCommodityId = null)
├── Commodity 1 (ContainerCommodityId = Container.CommodityId)
├── Commodity 2 (ContainerCommodityId = Container.CommodityId)
└── Commodity 3 (ContainerCommodityId = Container.CommodityId)
Use Cases:
- Palletized shipments (pallet contains cartons)
- Containerized cargo (container contains packages)
- Consolidated shipments (master package contains individual items)
2. Automatic Totals Calculation
The entity automatically calculates and maintains totals:
- VolumeTotal = VolumePiece × Pieces
- WeightTotal = Weight × Pieces (unless WeightByTotal=true)
- UnitaryValueTotal = UnitaryValue × Pieces/Quantity (unless ValueByTotal=true)
Totals are refreshed automatically when dimensions, weight, pieces, or container contents change.
3. Charge Recalculation Tracking
An internal flag (_requireChargeRecalculation) tracks when properties affecting charges change:
- Weight, dimensions, pieces, quantity
- Package type, commodity type
- BillToContactId (charge filtering)
Use RequiresChargeRecalculation() to check the flag and ResetChargeRecalculationFlag() after recalculating.
4. Dynamic Contact Resolution
Commodity GraphQL now exposes getContact(idPropertyName), which treats the named CustomValues entry as a contact ID and returns the matching contact in the same organization. This is useful when commodity type configuration adds role-specific contact fields without adding hard-coded entity columns.
query {
getCommodities(organizationId: 1, filter: "commodityId:123", take: 1) {
items {
commodityId
description
getContact(idPropertyName: "shipperContactId") {
contactId
name
}
}
}
}
If the custom value is missing or cannot be read as an integer contact ID, the resolver returns null.
5. Warehouse Location Cascading
When a container's warehouse location changes, the change automatically cascades to all nested commodities:
Container.ChangeWarehouseLocationId(newLocationId);
// All ContainerCommodities automatically updated to same location
YAML Configuration
Creating a Simple Commodity
# Create individual commodity
- task: Commodity/Create@1
name: createElectronicsCommodity
inputs:
organizationId: ${organizationId}
commodity:
description: "Electronic Components - PCB Boards"
pieces: 10
length: 24
width: 18
height: 6
dimensionsUnit: Inch
weight: 15.5
weightByTotal: false # 15.5 lbs per piece
weightUnit: Pound
volumeUnit: CubicFoot
packageTypeId: ${cartonTypeId}
commodityTypeId: ${electronicsTypeId}
commodityStatusId: ${inStockStatusId}
quantity: 100
unit: "EA"
unitaryValue: 250
valueByTotal: false # $250 per piece
customValues:
sku: "PCB-2025-A1"
lotNumber: "LOT-001"
expiryDate: "2027-12-31"
hazmat: false
outputs:
- commodity: commodity
Creating a Container with Nested Items
# Step 1: Create container commodity (pallet)
- task: Commodity/Create@1
name: createPalletContainer
inputs:
organizationId: ${organizationId}
commodity:
description: "Pallet of Electronics"
pieces: 1
length: 48
width: 40
height: 60
dimensionsUnit: Inch
weight: 0 # Will auto-calculate from contents
weightByTotal: true
weightUnit: Pound
volumeUnit: CubicFoot
packageTypeId: ${palletTypeId}
warehouseLocationId: ${warehouseLocId}
outputs:
- commodity: pallet
# Step 2: Create items and add to container
- task: Commodity/Create@1
name: createCarton1
inputs:
organizationId: ${organizationId}
commodity:
description: "Carton 1 - Laptops"
pieces: 5
weight: 8.5
weightByTotal: false
dimensionsUnit: Inch
weightUnit: Pound
volumeUnit: CubicFoot
parentCommodityId: ${pallet.commodityId}
outputs:
- commodity: carton1
- task: Commodity/Create@1
name: createCarton2
inputs:
organizationId: ${organizationId}
commodity:
description: "Carton 2 - Monitors"
pieces: 3
weight: 12.0
weightByTotal: false
dimensionsUnit: Inch
weightUnit: Pound
volumeUnit: CubicFoot
parentCommodityId: ${pallet.commodityId}
outputs:
- commodity: carton2
# Pallet's weight and volume now auto-calculated from contents
Adding Tracking Numbers
# Add multiple tracking numbers to commodity
- task: CommodityTrackingNumber/Create@1
name: addUPSTracking
inputs:
organizationId: ${organizationId}
trackingNumber:
commodityId: ${commodityId}
trackingNumber: "1Z999AA10123456784"
trackingNumberType: "UPS"
isPrimary: true
- task: CommodityTrackingNumber/Create@1
name: addFedExTracking
inputs:
organizationId: ${organizationId}
trackingNumber:
commodityId: ${commodityId}
trackingNumber: "FEDEX-987654321"
trackingNumberType: "FedEx"
isPrimary: false
Updating Commodity Dimensions (Triggers Charge Recalculation)
# Update dimensions - automatically recalculates volume and flags for charge recalc
- task: Commodity/Update@1
name: updateCommodityDimensions
inputs:
organizationId: ${organizationId}
commodityId: ${commodityId}
commodity:
length: 30
width: 20
height: 8
# Domain logic automatically:
# - Recalculates volume from new dimensions
# - Sets requiresChargeRecalculation flag
# - Triggers CommodityDimensionsChangedEvent
Moving Commodity to Different Container
# Move commodity from one container to another
- task: Commodity/Update@1
name: moveCommodityToNewContainer
inputs:
organizationId: ${organizationId}
commodityId: ${itemId}
commodity:
parentCommodityId: ${newContainerId}
# Alternatively, remove from container (set parent to null)
- task: Commodity/Update@1
name: removeFromContainer
inputs:
organizationId: ${organizationId}
commodityId: ${itemId}
commodity:
parentCommodityId: null
Querying Commodity with Relationships
# Query commodity with all nested data using GraphQL
- task: Query/GraphQL
name: getCommodityWithRelationships
inputs:
organizationId: ${organizationId}
query: |
query GetCommodity($commodityId: ID!) {
commodity(id: $commodityId) {
commodityId
description
pieces
length
width
height
weight
volume
containerCommodities {
commodityId
description
commodityType { name }
commodityStatus { statusName }
commodityTrackingNumbers { trackingNumber trackingNumberType }
}
commodityTrackingNumbers { trackingNumber trackingNumberType isPrimary }
commodityTags { tag { name color } }
orderCommodities { order { orderNumber } }
packageType { name }
commodityType { name }
commodityStatus { statusName }
warehouseLocation { locationName warehouseZone { zoneName } }
inventoryItem { sku }
job { jobNumber }
billToContact { name }
}
}
variables:
commodityId: ${commodityId}
outputs:
- response.commodity: fullCommodity
Warehouse Location Management
# Assign commodity to warehouse location (cascades to nested items)
- task: Commodity/Update@1
name: assignWarehouseLocation
inputs:
organizationId: ${organizationId}
commodityId: ${containerCommodityId}
commodity:
warehouseLocationId: ${newLocationId}
# Domain logic automatically updates all nested ContainerCommodities to same location
Key Methods
The Commodity entity provides rich business logic:
Container Management
AddItemToContainer(params Commodity[])- Add items to container, updates container totalsRemoveItemFromContainer(Commodity)- Remove item from container, updates container totalsMoveToContainer(Commodity)- Move this commodity to different containerChangeContainerCommodityId(int?)- Set/change parent container
Tracking Numbers
AddCommodityTrackingNumber(string, string?, bool)- Add tracking number with type and primary flagChangeCommodityTrackingNumbers(IEnumerable<CommodityTrackingNumber>)- Replace tracking number list
Dimension & Weight Management
ChangeLength(decimal?)- Update length, recalculates volume, flags charge recalcChangeWidth(decimal?)- Update width, recalculates volume, flags charge recalcChangeHeight(decimal?)- Update height, recalculates volume, flags charge recalcChangeDimensionsUnit(DimensionsUnit)- Change measurement systemChangeWeight(decimal?)- Update weight, recalculates total, flags charge recalcChangeWeightUnit(WeightUnit)- Change weight measurement systemChangePieces(int)- Update pieces, recalculates all totals, flags charge recalc
Totals Calculation
RefreshVolumeTotal()- Recalculate volume from dimensions/pieces or container contentsRefreshWeightTotal()- Recalculate total weight from pieces or container contentsRefreshValueTotal()- Recalculate total valueRefreshValues()- Recalculate all totals
Charge Recalculation
RequiresChargeRecalculation()- Check if changes require charge recalculationResetChargeRecalculationFlag()- Reset flag after recalculating charges
Status & Type Management
ChangeCommodityStatusId(int?)- Update status (cascades to container items)ChangeCommodityStatus(CommodityStatus)- Update status by entityChangeCommodityTypeId(int?)- Change type, flags charge recalcChangePackageTypeId(int?)- Change package type, flags charge recalc
Billing & Charge Filtering
ChangeBillToContactId(int?)- Set billing contact, flags charge recalcChangeBillToContact(Contact?)- Set billing contact by entity
Other Property Updates
ChangeDescription(string)- Update descriptionChangeWarehouseLocationId(int?)- Update location (cascades to container items)ChangeInventoryItemId(int?)- Link to inventory SKUChangeJobId(Guid?)- Associate with jobChangeCustomValues(Dictionary<string, object?>)- Update custom valuesChangeIsDeleted(bool?)- Soft delete
Utility Methods
Copy()- Create deep copy including container contentsCopyChanges(Commodity)- Copy properties from another commodity
Domain Events
The Commodity entity publishes domain events:
CommodityStatusChangedByNameEvent
Published when commodity status is changed.
Use Cases:
- Trigger workflows on status changes (e.g., "Shipped" → send notification)
- Update external warehouse systems
- Cascade status to related entities
CommodityTrackingNumbersChangedEvent
Published when tracking numbers are added, removed, or modified.
Use Cases:
- Sync tracking numbers to carrier systems
- Notify customers of tracking availability
- Update order tracking information
Aggregate Boundary
As an aggregate root, Commodity controls:
Direct Children:
- CommodityTrackingNumber - Managed through commodity methods
- CommodityTag - Tag assignments
- ContainerCommodities - Nested items (hierarchical self-reference)
External References:
- OrderCommodity - Junction to orders (Order is separate aggregate)
- CommodityType, CommodityStatus, PackageType - Reference data
- WarehouseLocation - Reference to location
- InventoryItem - Reference to inventory SKU
- Job - Reference to job aggregate
- BillToContact - Reference to contact
Consistency Rules:
- Always add/remove container items through
AddItemToContainer()/RemoveItemFromContainer() - Totals are automatically maintained - don't set directly
- Status changes cascade to nested container items
- Warehouse location changes cascade to nested items
- Charge recalculation flag tracks relevant changes
Use Cases
1. Palletized Shipment
# Create pallet container
- task: Commodity/Create@1
name: createPallet
inputs:
organizationId: ${organizationId}
commodity:
description: "Pallet #1"
pieces: 1
packageTypeId: ${palletTypeId}
weightByTotal: true
dimensionsUnit: Inch
weightUnit: Pound
volumeUnit: CubicFoot
outputs:
- commodity: pallet
# Add cartons to pallet by setting their parent
- task: Loops/ForEach
name: addCartonsToPallet
inputs:
items: ${cartonList}
actions:
- task: Commodity/Update@1
name: addCartonToContainer
inputs:
organizationId: ${organizationId}
commodityId: ${item.commodityId}
commodity:
parentCommodityId: ${pallet.commodityId}
# Pallet weight/volume auto-calculated from cartons
2. Inventory Item Tracking
# Create commodity linked to inventory SKU
- task: Commodity/Create@1
name: createInventoryCommodity
inputs:
organizationId: ${organizationId}
commodity:
description: "Widget Model A"
inventoryItemId: ${widgetSkuId}
pieces: 50
warehouseLocationId: ${binLocationId}
commodityStatusId: ${inStockStatusId}
customValues:
batchNumber: "BATCH-2025-01"
receivedDate: ${today}
outputs:
- commodity: inventoryCommodity
3. Multi-Customer Consolidation with Charge Filtering
# Container with items for different customers
- task: Commodity/Create@1
name: createConsolidatedContainer
inputs:
organizationId: ${organizationId}
commodity:
description: "Consolidated Container"
pieces: 1
outputs:
- commodity: container
# Customer A's items (BillToContactId set)
- task: Commodity/Create@1
name: createCustomerAItems
inputs:
organizationId: ${organizationId}
commodity:
description: "Customer A Items"
pieces: 10
billToContactId: ${customerAId}
parentCommodityId: ${container.commodityId}
outputs:
- commodity: customerAItems
# Customer B's items
- task: Commodity/Create@1
name: createCustomerBItems
inputs:
organizationId: ${organizationId}
commodity:
description: "Customer B Items"
pieces: 5
billToContactId: ${customerBId}
parentCommodityId: ${container.commodityId}
outputs:
- commodity: customerBItems
# Charges with ApplyToContactId will only count matching commodities
Best Practices
1. Container Hierarchy
- Use containers for pallets, master cartons, or consolidations
- Always use
AddItemToContainer()/RemoveItemFromContainer()methods - Don't set ContainerCommodityId directly
- Container totals auto-calculate from contents