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

176 lines
7.0 KiB
Markdown

# 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