163 lines
7.0 KiB
Markdown
163 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/README.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.
|
||
|
||
## Shared concepts (canonical in DOMAIN_MODEL.md)
|
||
|
||
These are defined once in [`DOMAIN_MODEL.md`](DOMAIN_MODEL.md) — referenced here, not duplicated:
|
||
|
||
- **Weight formula** — `NetWeight = GrossWeight − PalletWeight − (TrayQuantity × TareWeight)`. See [DOMAIN_MODEL.md § Weight Formula](DOMAIN_MODEL.md#weight-formula) for field meanings and edge cases.
|
||
- **MeasuringStatus lifecycle** — `NotStarted (0) → Started (10) → Finnished (20) → Audited (30)`. See [DOMAIN_MODEL.md § MeasuringStatus Lifecycle](DOMAIN_MODEL.md#measuringstatus-lifecycle) for state transitions. Note: `Finnished` is an intentional double-n typo — do not "fix" it.
|
||
- **IsMeasurable Flag** — `ProductDto.IsMeasurable` master toggle for weight validation. See [DOMAIN_MODEL.md § IsMeasurable Flag](DOMAIN_MODEL.md#ismeasurable-flag) for `false`-branch semantics.
|
||
|
||
## 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 (formula in Shared concepts above)
|
||
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
|