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

5.4 KiB
Raw Blame History

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_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 documentsAppendItemsToOrderAsync (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 / UpdateShippingItemPreOrderConversionService.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)