# Pre-Order Workflow

> Part of `Nop.Plugin.Misc.FruitBankPlugin`. See `../README.md` for the docs index, `../../README.md` for project overview.
> For entity definitions (`PreOrder`, `PreOrderItem`, `PreOrderStatus`, `PreOrderItemStatus`) see `../SCHEMA.md` (authoritative TOON schema).
> For the entity-hierarchy overview see `../DOMAIN_MODEL.md`; for the inbound shipping flow that triggers conversion see `../MEASUREMENT.md` (Workflow 1).
> Known issues / planned work: [`PREORDER_ISSUES.md`](PREORDER_ISSUES.md) / [`PREORDER_TODO.md`](PREORDER_TODO.md).

Customers place **pre-orders** for products that are not yet physically in stock. When a supplier shipping document confirms incoming stock, pending pre-orders are allocated against that stock and converted into real nopCommerce orders. This is the fourth domain workflow alongside the three measurement workflows (shipping, order, stocktaking).

## Entity Hierarchy

```
PreOrder (fbPreOrder)                  ← customer advance-order header
  └─ PreOrderItem (fbPreOrderItem)     ← requested product line          [1:N]

PreOrder.OrderId ─▶ Order              ← real nopCommerce order, created on first fulfilment [N:1, nullable]
```

Both entities live in `FruitBank.Common/Entities/` (shared with FruitBankHybridApp), inherit `MgEntityBase`, and use LinqToDB.

## Status Lifecycle

```
PreOrderStatus:      Pending(0) → Confirmed(10) / PartiallyFulfilled(20) / Cancelled(30)
PreOrderItemStatus:  Pending(0) → Fulfilled(10) / PartiallyFulfilled(20) / Dropped(30)
```

Header status is **derived from item states** by `PreOrderDbContext.RefreshPreOrderStatusAsync`:

| Condition | PreOrder.Status |
|---|---|
| all items `Fulfilled` | `Confirmed` |
| any `Dropped`/`PartiallyFulfilled` AND none `Pending` | `PartiallyFulfilled` |
| otherwise | `Pending` |
| customer/admin cancel | `Cancelled` (set explicitly; active items → `Dropped`) |

## Workflow

### 1. Placement

`PreOrderController.PlacePreOrder` (public), also `OrderController` and `PreOrderAdminController` (admin):

- Available products are filtered by the per-product pre-order window (`PreOrderWindowStart` / `PreOrderWindowEnd` product GenericAttributes).
- Unit price is captured at placement into `PreOrderItem.UnitPriceInclTax` (price lock).
- `PreOrderDbContext.InsertPreOrderAsync` saves the header (`Status = Pending`) and items (`FulfilledQuantity = 0`, `Status = Pending`).

### 2. Conversion (on incoming stock)

`PreOrderConversionService.ConvertPreOrdersForProductsAsync(productIds, shippingDocumentId)`:

1. **Expiry sweep** first (see §4).
2. Fetch `Pending`/`PartiallyFulfilled` items for the products (FCFS-ordered by `PreOrderId`).
3. **Conversion-window filter** — only preorders whose `DateOfReceipt` is within `PreOrderConversionWindowDays` (4 days) are eligible; farther-out deliveries wait for a later document.
4. Build the incoming-quantity pool (`AvailableQuantity` minus quantities already committed to preorders).
5. Allocate FCFS: per item `fulfill = min(RequestedQuantity − FulfilledQuantity, available)`; set item status `Fulfilled` / `PartiallyFulfilled`.
6. **First fulfilment** for a preorder → `CreateOrderAsync` (creates the nopCommerce `Order`, saves `PreOrder.OrderId`). **Subsequent documents** → `AppendItemsToOrderAsync` (adds the newly-fulfilled items to the same order).
7. Dropped items are recorded in an order note (never become order items).

### 3. Triggers

| Source | When |
|---|---|
| `FruitBankEventConsumer` (`EntityInserted/UpdatedEvent<ShippingItem>`) | shipping item with a matched product + quantity (background `Task.Run`) |
| `FruitBankDataController.UpdateShippingItem` | shipping-item quantity increase |
| `FileManagerController` | shipping-document processing (batch over the document's products) |
| `OrderController` / `PreOrderAdminController` | manual / immediate-from-stock |

`IncomingQuantity` is kept in sync by `FruitBankDataController.AddShippingItem` / `UpdateShippingItem` → `PreOrderConversionService.SyncIncomingQuantityAsync`. Product replacement on a shipping item is handled by `ReplaceShippingItemProductAsync` (`PreOrderConversionService.Replace.cs`), which moves stock/incoming, swaps order items, repoints preorder items, and re-triggers conversion for the new product.

### 4. Expiry

`SweepExpiredPreordersAsync` runs at the start of every conversion run: for preorders past `DateOfReceipt` still holding `Pending`/`PartiallyFulfilled` items, any still-`Pending` item becomes `Dropped` and the header status is refreshed. Already-fulfilled quantities stay (they are already on a real order).

## Pricing

| Product | Order-item price on conversion |
|---|---|
| Non-measurable | `UnitPriceInclTax × FulfilledQuantity` (locked at placement) |
| Measurable | `0` at conversion — weighed afterwards (see `../MEASUREMENT.md` Workflow 2) |

## Key Source

| Type | Location |
|---|---|
| `PreOrderConversionService` (+ `.Replace` partial) | `Services/` |
| `PreOrderDbContext` (`InsertPreOrderAsync`, `RefreshPreOrderStatusAsync`, `CancelPreOrderAsync`) | `Domains/DataLayer/` |
| `PreOrderDbTable` / `PreOrderItemDbTable` (+ `IPreOrderDbSet` / `IPreOrderItemDbSet`) | `Domains/DataLayer/` |
| `PreOrderController` / `CustomerPreOrderController` | `Controllers/` |
| `PreOrderAdminController` / `PreOrderAvailabilityController` | `Areas/Admin/Controllers/` |
| `PreOrder` / `PreOrderItem` entities | `FruitBank.Common/Entities/` (FruitBankHybridApp solution) |
