5.4 KiB
Pre-Order Workflow
Part of
Nop.Plugin.Misc.FruitBankPlugin. See../README.mdfor the docs index,../../README.mdfor 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_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/PreOrderWindowEndproduct GenericAttributes). - Unit price is captured at placement into
PreOrderItem.UnitPriceInclTax(price lock). PreOrderDbContext.InsertPreOrderAsyncsaves the header (Status = Pending) and items (FulfilledQuantity = 0,Status = Pending).
2. Conversion (on incoming stock)
PreOrderConversionService.ConvertPreOrdersForProductsAsync(productIds, shippingDocumentId):
- Expiry sweep first (see §4).
- Fetch
Pending/PartiallyFulfilleditems for the products (FCFS-ordered byPreOrderId). - Conversion-window filter — only preorders whose
DateOfReceiptis withinPreOrderConversionWindowDays(4 days) are eligible; farther-out deliveries wait for a later document. - Build the incoming-quantity pool (
AvailableQuantityminus quantities already committed to preorders). - Allocate FCFS: per item
fulfill = min(RequestedQuantity − FulfilledQuantity, available); set item statusFulfilled/PartiallyFulfilled. - First fulfilment for a preorder →
CreateOrderAsync(creates the nopCommerceOrder, savesPreOrder.OrderId). Subsequent documents →AppendItemsToOrderAsync(adds the newly-fulfilled items to the same order). - 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) |