# 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 the customer pre-order workflow (advance orders converted on stock arrival) see `docs/PREORDER/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
