176 lines
7.0 KiB
Markdown
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
|