Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/docs/MEASUREMENT.md

7.0 KiB

Measurement Workflows

Part of Nop.Plugin.Misc.FruitBankPlugin. See README.md for project overview. For entity model see docs/DOMAIN_MODEL.md. For SignalR endpoints see docs/SIGNALR_ENDPOINTS.md. For core measurement system rules and common domain traps, see: ../../../../../FruitBankHybridApp/FruitBank.Common/docs/GLOSSARY.md

Three parallel measurement workflows share the same weight formula and MeasuringStatus lifecycle.

MeasuringStatus Lifecycle

NotStarted (0)  →  Started (10)  →  Finnished (20)  →  Audited (30)
   [no record]      [record created]   [IsMeasured=true]   [RevisorId > 0]
  • NotStarted: No pallet record exists yet
  • Started: Pallet record created (Id > 0) but IsMeasured = false
  • Finnished: IsMeasured = true — weights recorded
  • Audited: RevisorId > 0 — quality auditor confirmed (OrderItemPallet only)

IsMeasurable Flag

ProductDto.IsMeasurable is the master toggle (stored as GenericAttribute). When false:

  • Weight validation is bypassed
  • One pallet record is still created with GrossWeight = 0.0
  • Only TrayQuantity is recorded (for tare-weight calculations)
  • Product is not included in average weight checks

Workflow 1: Inbound Shipping

Supplier truck arrives at the warehouse. Goods are weighed and compared to the delivery document.

Shipping (truck)
  → ShippingDocument (supplier delivery note)
    → ShippingItem (product line)
      → ShippingItemPallet (individual measurement)

Flow

  1. Create Shipping — truck registration (LicencePlate, ShippingDate, CargoCompany)
  2. Add ShippingDocument — link to Partner (supplier), upload PDF delivery note
  3. Add ShippingItem — product line from document. IsMeasurable copied from ProductDto.IsMeasurable
  4. Weigh palletsFruitBankDataController.AddOrUpdateMeasuredShippingItemPallet()
    • Each pallet: GrossWeight, PalletWeight, TareWeight, TrayQuantity
    • NetWeight computed: GrossWeight - PalletWeight - (TrayQuantity * TareWeight)
  5. Cascade update — after each pallet measurement:
    • ShippingItem: MeasuredGrossWeight, MeasuredNetWeight, MeasuredQuantity recalculated from pallets
    • ShippingDocument.IsAllMeasured = all items measured
    • Shipping.IsAllMeasured = all documents measured
    • Product AverageWeight updated from measured data

Event Cascade (FruitBankEventConsumer)

ShippingItemPallet deleted → ShippingItem measuring values refreshed
ShippingItem inserted/updated → ShippingDocument.IsAllMeasured rechecked
ShippingDocument inserted/updated → Shipping.IsAllMeasured rechecked
Shipping deleted → cascade delete ShippingDocuments
ShippingDocument deleted → cascade delete ShippingItems
ShippingItem deleted → cascade delete ShippingItemPallets

ShippingItem: Declared vs Measured

Declared (from document) Measured (warehouse) Discrepancy tracked
QuantityOnDocument MeasuredQuantity Yes
GrossWeightOnDocument MeasuredGrossWeight Yes
NetWeightOnDocument MeasuredNetWeight Yes

Workflow 2: Outbound Order

Customer order is prepared. Items are weighed before dispatch.

OrderDto (order)
  → OrderItemDto (line item)
    → OrderItemPallet (measurement record)

Flow

  1. Start MeasuringCustomOrderSignalREndpoint.StartMeasuring(orderId, userId)
    • Sets MeasurementOwnerId via GenericAttribute
    • Order status set to Processing, Payment to Pending
    • Broadcasts SendOrderChanged
  2. Weigh palletsCustomOrderSignalREndpoint.AddOrUpdateMeasuredOrderItemPallet()
    • Same weight formula as shipping
    • Triggers price recalculation after each pallet
    • Broadcasts SendOrderItemPalletChanged
  3. Complete orderCustomOrderSignalREndpoint.SetOrderStatusToComplete(orderId, revisorId)
    • Validates: all order items must have pallets
    • Recalculates final prices via CustomPriceCalculationService
    • Updates product stock quantities and weights
    • Sets RevisorId on each OrderItemPallet → MeasuringStatus becomes Audited
    • Order status → Complete
    • Publishes OrderPaidEvent for nopCommerce integration

Average Weight Validation

After measuring, OrderItemDto.AverageWeightIsValid checks:

deviation = |ProductDto.AverageWeight - OrderItemDto.AverageWeight|
percentage = (deviation / ProductDto.AverageWeight) * 100
valid = percentage < ProductDto.AverageWeightTreshold

OrderDto.IsAllOrderItemAvgWeightValid = all items pass this check.

PendingMeasurementCheckoutFilter

ASP.NET Core action filter on the Checkout ConfirmOrder action.

Step Action
Intercept ConfirmOrder POST in CheckoutController
Check OrderMeasurementService.IsPendingMeasurementAsync()
If pending Set OrderStatus=Processing, PaymentStatus=Pending, redirect to PendingMeasurementWarning
If not pending Allow normal checkout flow

Workflow 3: StockTaking

Inventory session compares logical stock with physical count.

StockTaking (session)
  → StockTakingItem (product line)
    → StockTakingItemPallet (measurement record)

Flow

  1. Create sessionStockSignalREndpointServer.AddStockTaking()
    • Auto-populates StockTakingItem for each product
    • Captures OriginalStockQuantity (current stock) and InProcessOrdersQuantity (reserved in pending orders)
    • TotalOriginalQuantity = OriginalStockQuantity + InProcessOrdersQuantity
  2. Weigh itemsStockSignalREndpointServer.AddOrUpdateMeasuredStockTakingItemPallet()
    • Same weight formula
    • Refreshes MeasuredStockQuantity and MeasuredNetWeight from pallets
  3. Close sessionStockSignalREndpointServer.CloseStockTaking()
    • Validates: all items must be measured
    • For each item: QuantityDiff = MeasuredStockQuantity - TotalOriginalQuantity
    • Updates Product.StockQuantity with the diff
    • Updates Product.NetWeight with measured weight
    • Creates stock quantity history entries
    • Marks session as closed (IsClosed = true)

CustomPriceCalculationService

Extends nopCommerce's PriceCalculationService. Overrides order item pricing based on measurement data.

Scenario Price calculation
Measurable product FinalPrice = NetWeight * UnitPriceExclTax (weight-based)
Non-measurable product FinalPrice = Quantity * UnitPriceExclTax (standard quantity-based)

Methods:

Method Purpose
CalculateOrderItemFinalPrice(OrderItemDto) Single item price calculation
CheckAndUpdateOrderItemFinalPricesAsync(int orderId) Recalculates all items in an order
CheckAndUpdateOrderTotalPrice(int orderId) Updates order total from item sum

Stock Update Flow

When an order is completed or a stocktaking session closes, FruitBankDbContext.UpdateStockQuantityAndWeightAsync():

  1. Updates Product.StockQuantity (add/subtract delta)
  2. Updates Product.NetWeight via GenericAttribute
  3. Creates StockQuantityHistory entry with extension record for audit trail