Mango.Nop.Plugins/Nop.Plugin.Misc.AIPlugin/docs/PREORDER/README.md

91 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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) |