diff --git a/Nop.Plugin.Misc.AIPlugin/.github/TOPIC_CODES.md b/Nop.Plugin.Misc.AIPlugin/.github/TOPIC_CODES.md new file mode 100644 index 0000000..3e019ca --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/.github/TOPIC_CODES.md @@ -0,0 +1,13 @@ +# Topic Codes — Nop.Plugin.Misc.FruitBankPlugin (`MGFBANKPLUG`) + +Per-repo topic registry for this plugin, per the **per-repo extension convention** in `AyCode.Core/.github/skills/docs-check/references/TOPIC_CODES.md`. Lists ONLY this repo's own topic codes; inherited (ACCORE / Mango.Nop) topics are reached through `@repo.own-dep-repos`. + +**Foundational conventions are defined once at the framework layer — not restated here:** +- ID format, type codes (`I`/`T`/`B`/`C`), ID rules, Status vocabulary, archival, registry-maintenance workflow → `AyCode.Core/.github/skills/docs-check/references/TOPIC_CODES.md` +- `` (this repo = `MGFBANKPLUG`, from the `@repo.prefix` in `.github/copilot-instructions.md`) + `` spec → `AyCode.Core/.github/REPO_PREFIXES.md` + +## This repo's own topic codes + +| Code | Topic | Scope | Docs location | +|--------|-----------|------------------------------------------------------------------------------------------------|------------------| +| `PREO` | PRE-ORDER | Customer advance orders placed before stock arrives, and their conversion into real nopCommerce orders when an inbound shipping document confirms incoming stock. | `docs/PREORDER/` | diff --git a/Nop.Plugin.Misc.AIPlugin/README.md b/Nop.Plugin.Misc.AIPlugin/README.md index 6ea5326..bd5295a 100644 --- a/Nop.Plugin.Misc.AIPlugin/README.md +++ b/Nop.Plugin.Misc.AIPlugin/README.md @@ -11,7 +11,7 @@ > For FruitBankHybridApp domain rules see the FruitBankHybridApp solution's `.github/copilot-instructions.md` > For core measurement system rules and common domain traps, see: `../../../../FruitBankHybridApp/FruitBank.Common/docs/GLOSSARY.md` -Server-side nopCommerce plugin for FruitBank, a fruit and vegetable wholesaler. Manages supplier inbound delivery (receiving), warehouse weighing (net/gross/pallet/tare weights), outbound order measurement, inventory stocktaking, AI services, and Billingo invoicing. Runs inside nopCommerce 4.80.9 (**net9.0**). +Server-side nopCommerce plugin for FruitBank, a fruit and vegetable wholesaler. Manages supplier inbound delivery (receiving), warehouse weighing (net/gross/pallet/tare weights), outbound order measurement, customer pre-orders, inventory stocktaking, AI services, and Billingo invoicing. Runs inside nopCommerce 4.80.9 (**net9.0**). Project file: `Nop.Plugin.Misc.FruitBankPlugin.csproj` @@ -25,6 +25,7 @@ Project file: `Nop.Plugin.Misc.FruitBankPlugin.csproj` | `docs/AI_SERVICES.md` | OpenAI, Cerebras, Replicate providers, FruitBankSettings, file storage, PDF conversion, audio transcription | | `docs/DATA_LAYER.md` | FruitBankDbContext, StockTakingDbContext, DbTable repositories, entity mapping, FruitBankEventConsumer | | `docs/SIGNALR/README.md` | SignalR endpoints, FruitBankDataController, InnVoice/Billingo integration, FruitBankAttributeService | +| `docs/PREORDER/README.md` | Customer pre-order workflow — placement, FCFS allocation on incoming stock, conversion to real orders, expiry | ## Folder Structure diff --git a/Nop.Plugin.Misc.AIPlugin/docs/DOMAIN_MODEL.md b/Nop.Plugin.Misc.AIPlugin/docs/DOMAIN_MODEL.md index 46ef545..487c786 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/DOMAIN_MODEL.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/DOMAIN_MODEL.md @@ -3,6 +3,7 @@ > Part of `Nop.Plugin.Misc.FruitBankPlugin`. See `README.md` for project overview. > For entity definitions, properties, and relationships see `docs/SCHEMA.md` (authoritative TOON schema). > For measurement workflows see `docs/MEASUREMENT.md`. +> For the customer pre-order workflow see `docs/PREORDER/README.md`. > For data layer details see `docs/DATA_LAYER.md`. > For core measurement system rules and common domain traps, see: `../../../../../FruitBankHybridApp/FruitBank.Common/docs/GLOSSARY.md` diff --git a/Nop.Plugin.Misc.AIPlugin/docs/MEASUREMENT.md b/Nop.Plugin.Misc.AIPlugin/docs/MEASUREMENT.md index 3640028..c8bed90 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/MEASUREMENT.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/MEASUREMENT.md @@ -3,6 +3,7 @@ > 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. diff --git a/Nop.Plugin.Misc.AIPlugin/docs/PREORDER/PREORDER_ISSUES.md b/Nop.Plugin.Misc.AIPlugin/docs/PREORDER/PREORDER_ISSUES.md new file mode 100644 index 0000000..2bb6ded --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/docs/PREORDER/PREORDER_ISSUES.md @@ -0,0 +1,10 @@ +# PRE-ORDER — Known Issues + +> Companion to [`README.md`](README.md). Topic `PREO`, prefix `MGFBANKPLUG` → entry IDs `MGFBANKPLUG-PREO-I-` (issue) / `-B-` (confirmed bug). +> ID format, Status vocabulary, type codes and archival are **not restated here** — see `../../.github/TOPIC_CODES.md` (which points to the framework registry). Entry template: follow `AyCode.Core/.github/META_ISSUES.md`. + +Scope: bugs / inconsistent or observable edge-case behaviour in the pre-order subsystem — placement, allocation/conversion, conversion window, expiry sweep, order create/append, product replacement, status derivation, pricing. + +## Active entries + +*(None yet — to be filled from the code review as needed.)* diff --git a/Nop.Plugin.Misc.AIPlugin/docs/PREORDER/PREORDER_TODO.md b/Nop.Plugin.Misc.AIPlugin/docs/PREORDER/PREORDER_TODO.md new file mode 100644 index 0000000..05b8f62 --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/docs/PREORDER/PREORDER_TODO.md @@ -0,0 +1,10 @@ +# PRE-ORDER — TODO + +> Companion to [`README.md`](README.md). Topic `PREO`, prefix `MGFBANKPLUG` → entry IDs `MGFBANKPLUG-PREO-T-`. +> Priority legend, Status vocabulary and archival are **not restated here** — see `../../.github/TOPIC_CODES.md` (which points to the framework registry). Entry template: follow `AyCode.Core/.github/META_TODO.md`. + +Scope: planned work / refactors / optimizations for the pre-order subsystem. + +## Active entries + +*(None yet — to be filled from the code review as needed.)* diff --git a/Nop.Plugin.Misc.AIPlugin/docs/PREORDER/README.md b/Nop.Plugin.Misc.AIPlugin/docs/PREORDER/README.md new file mode 100644 index 0000000..6433c0b --- /dev/null +++ b/Nop.Plugin.Misc.AIPlugin/docs/PREORDER/README.md @@ -0,0 +1,90 @@ +# 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`) | 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) | diff --git a/Nop.Plugin.Misc.AIPlugin/docs/README.md b/Nop.Plugin.Misc.AIPlugin/docs/README.md index 01870ae..cf9face 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/README.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/README.md @@ -13,6 +13,7 @@ Topic documentation for the FruitBank-specific NopCommerce plugin (Layer 3 — c ## Topic folders - [`SIGNALR/`](SIGNALR/README.md) — SignalR endpoints exposed by this plugin (project-specific variant) +- [`PREORDER/`](PREORDER/README.md) — Customer pre-order workflow: advance orders converted to real orders on incoming stock (+ `PREORDER_ISSUES.md` / `PREORDER_TODO.md`) ## Navigation diff --git a/Nop.Plugin.Misc.AIPlugin/docs/SCHEMA.md b/Nop.Plugin.Misc.AIPlugin/docs/SCHEMA.md index 14c432b..a856ccc 100644 --- a/Nop.Plugin.Misc.AIPlugin/docs/SCHEMA.md +++ b/Nop.Plugin.Misc.AIPlugin/docs/SCHEMA.md @@ -261,36 +261,46 @@ purpose: "A requested product line within a PreOrder. Tracks requested versus cumulatively fulfilled quantity as incoming stock is allocated across one or more shipping-document conversion runs." FulfilledQuantity: int purpose: "Quantity allocated from incoming stock so far; accumulates across conversion runs until it reaches RequestedQuantity." + business-logic: "this >= 0 && this <= RequestedQuantity" PreOrderId: int + purpose: "FK to the parent PreOrder." ProductId: int + purpose: "FK to the nopCommerce Product being preordered." RequestedQuantity: int purpose: "Quantity of the product the customer requested." + constraints: "positive" Status: PreOrderItemStatus - purpose: "Item lifecycle: Pending -> Fulfilled (fully allocated) / PartiallyFulfilled (partly allocated) / Dropped (expired or no incoming stock)." + purpose: "Item lifecycle: Pending / Fulfilled (fully allocated) / PartiallyFulfilled (partly allocated) / Dropped (expired or no incoming stock)." + business-logic: "set during conversion: FulfilledQuantity >= RequestedQuantity ? Fulfilled : FulfilledQuantity > 0 ? PartiallyFulfilled : Dropped; stays Pending until first allocation" UnitPriceInclTax: decimal purpose: "Gross unit price locked at preorder time. Used as the order-item price on conversion for non-measurable products; measurable products are priced 0 at conversion and weighed afterwards." + constraints: "non-negative" Id: int purpose: "Primary key / unique identification" primary-key: true PreOrder: "Customer advance order placed before the goods physically arrive" table-name: "fbPreOrder" - purpose: "Header of a customer pre-order against an upcoming inbound delivery. Pending items are allocated from incoming stock first-come-first-served by PreOrderId when a shipping document confirms arrival, then converted into a real NopCommerce Order once any quantity is fulfilled." + purpose: "Header of a customer pre-order against an upcoming inbound delivery. When a supplier shipping document confirms incoming stock, pending items are allocated first-come-first-served by PreOrderId (only within the conversion window — PreOrderConversionWindowDays, 4 days, of DateOfReceipt) and the preorder is converted into a real NopCommerce Order once any quantity is fulfilled. Preorders past DateOfReceipt are swept closed, dropping any still-Pending items." CreatedOnUtc: DateTime CustomerId: int + purpose: "FK to the nopCommerce Customer who placed the preorder." CustomerNote: string purpose: "Optional free-text note entered by the customer when placing the preorder." DateOfReceipt: DateTime - purpose: "Requested delivery date. Drives the conversion window (only preorders within PreOrderConversionWindowDays of this date are eligible for allocation) and the expiry sweep — once this date is past, any still-Pending items are Dropped." + purpose: "Requested delivery date. Drives the conversion window (only preorders within PreOrderConversionWindowDays — 4 days — of this date are eligible for allocation) and the expiry sweep — once this date is past, any still-Pending items are Dropped." OrderId: int? purpose: "FK to the real NopCommerce Order created from this preorder. Null until the first item is fulfilled; set once on first conversion and reused so subsequent shipping documents append items to the same order." PreOrderItems: List other-key: "PreOrderId" navigation: "one-to-many" Status: PreOrderStatus - purpose: "Header lifecycle: Pending -> Confirmed (all items Fulfilled) / PartiallyFulfilled (some Dropped or partial, none left Pending) / Cancelled." + purpose: "Header lifecycle: Pending / Confirmed (all items Fulfilled) / PartiallyFulfilled (some Dropped or partial, none left Pending) / Cancelled." + business-logic: "set by RefreshPreOrderStatusAsync from item states: items.All(Fulfilled) => Confirmed; (any Dropped or PartiallyFulfilled) && none Pending => PartiallyFulfilled; else Pending. Cancelled is set explicitly." StoreId: int + purpose: "nopCommerce multi-store scope." UpdatedOnUtc: DateTime + purpose: "Bumped to UtcNow on every status / allocation change." Id: int purpose: "Primary key / unique identification" primary-key: true