# 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 pallets** — `FruitBankDataController.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 Measuring** — `CustomOrderSignalREndpoint.StartMeasuring(orderId, userId)` - Sets `MeasurementOwnerId` via GenericAttribute - Order status set to Processing, Payment to Pending - Broadcasts `SendOrderChanged` 2. **Weigh pallets** — `CustomOrderSignalREndpoint.AddOrUpdateMeasuredOrderItemPallet()` - Same weight formula as shipping - Triggers price recalculation after each pallet - Broadcasts `SendOrderItemPalletChanged` 3. **Complete order** — `CustomOrderSignalREndpoint.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 session** — `StockSignalREndpointServer.AddStockTaking()` - Auto-populates `StockTakingItem` for each product - Captures `OriginalStockQuantity` (current stock) and `InProcessOrdersQuantity` (reserved in pending orders) - `TotalOriginalQuantity = OriginalStockQuantity + InProcessOrdersQuantity` 2. **Weigh items** — `StockSignalREndpointServer.AddOrUpdateMeasuredStockTakingItemPallet()` - Same weight formula - Refreshes `MeasuredStockQuantity` and `MeasuredNetWeight` from pallets 3. **Close session** — `StockSignalREndpointServer.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