# Order-Draft Workflow > Part of `Nop.Plugin.Misc.FruitBankPlugin`. See `../README.md` for the docs index, `../../README.md` for project overview. > Closely coupled to the pre-order subsystem — see [`../PREORDER/README.md`](../PREORDER/README.md): an approved draft creates `PreOrder`s and runs the same conversion. > Known issues / planned work: [`ORDERDRAFT_ISSUES.md`](ORDERDRAFT_ISSUES.md). An **order draft** is an AI-assisted intake channel. External systems (Viber bot, email processor, dictation app) post a raw free-text message; the AI extracts delivery dates and product demands; a draft is created for admin review. On approval the draft becomes one or more `PreOrder`s, which then flow through the existing **PRE-ORDER conversion** — so OrderDraft is a new, admin-gated *placement* channel into that pipeline, not a parallel order path. ## Entity Hierarchy ``` OrderDraft ← one inbound message for one delivery date └─ OrderDraftItem ← one parsed product-demand line [1:N] ExternalUserPartnerMapping ← external username → nopCommerce CustomerId (learned) ProductTextMapping ← raw text → admin-chosen ProductId (learned, for ranking) ``` All four tables live in `FruitBank.Common/Entities/` and are registered on `FruitBankDbContext`. `OrderDraftStatus`: `Pending` → `Approved` / `Rejected` / `Expired`. ## Workflow ### 1. Intake `OrderDraftApiController.Create` (`Controllers/`): auth via `X-FruitBank-ApiKey` header (matched against `FruitBankSettings.OrderDraftApiKey`), body `{ externalUsername, messageText }`. ### 2. Draft build `OrderDraftService.CreateDraftsFromMessageAsync`: - Partner resolution from `ExternalUserPartnerMapping` (may be `null` → admin assigns later). - AI parse (`OrderDraftParserService.ParseMessageAsync`): one draft per delivery date; `isPreorder = deliveryDate > today + PreorderThresholdDays (4)`. - Per item (`ProcessDraftItemAsync`): historic `ProductTextMapping` weighting (last 5 admin decisions) + AI canonical name/keywords + `SearchProductsAsync` + AI ranking → `SuggestedProductIdsJson`, first suggestion pre-selected. ### 3. Admin review `OrderDraftAdminController` (`Areas/Admin/`): DataTables list (urgent = Pending > 20h), Detail page with candidates, `UpdateItem` / `AssignCustomer`. ### 4. Approval (the entry point into PRE-ORDER) `OrderDraftService.ApproveAsync`: - Validates: partner assigned, every item has `SelectedProductId`. - Saves `ProductTextMapping` learning records. - Splits items by `IsPreorder`; **both branches create a `PreOrder`** via `InsertPreOrderAsync`, then call `PreOrderConversionService.ConvertPreOrdersForProductsAsync`. The "immediate" branch relies on the 4-day conversion window to convert at once. ### 5. Expiry `OrderDraftService.ExpireOldDraftsAsync`: Pending drafts older than 14 days → `Expired` (background job). ## AI touchpoints Three distinct AI calls, all in `OrderDraftParserService` via `OpenAIApiService.GetSimpleResponseAsync` (OpenAI Chat Completions). OrderDraft uses **OpenAI exclusively** — unlike `QuickOrderController` / `OrderController` parsing, which use `CerebrasApiService`. | # | Call | Input → Output | Used in | |---|---|---|---| | 1 | `ParseMessageAsync` | raw message → delivery-date groups of `{ rawProductText, requestedQuantity }`; one draft per date | `CreateDraftsFromMessageAsync` | | 2 | `ResolveProductNameAsync` | one raw product text → `{ canonicalName, searchKeywords[] }` (keywords feed NopCommerce `SearchProductsAsync`) | `ProcessDraftItemAsync` | | 3 | `RankCandidatesAsync` | raw text + canonical name + candidate `(id, name)[]` → ranked product-id list; only when >1 non-historic search hit | `ProcessDraftItemAsync` | Resolution is **hybrid**: AI proposes candidates, but ordering is front-loaded by historic admin decisions (`ProductTextMapping`, last 5 for the same raw text) — a DB heuristic, not AI. Product search itself (`SearchProductsAsync`) is NopCommerce full-text, not AI. **Model:** `GetModelName()` from `FruitBankSettings.OpenAIModelName` (default `gpt-3.5-turbo`; code paths for `gpt-4o-mini`/`4.1-mini`/`4.1-nano`/`5-nano` at `temperature=0.2`, and a `gpt-5` path at `reasoning_effort=minimal`). **Not yet AI-wired:** partner identification (plain `ExternalUserPartnerMapping` username lookup), quantity/unit normalisation (relies on the parse prompt only), confidence/uncertainty scoring (admin approves everything manually), and the preorder-vs-immediate split (fixed 4-day `PreorderThresholdDays`, not AI). ## Key Source | Type | Location | |---|---| | `OrderDraftService` / `IOrderDraftService` | `Services/` | | `OrderDraftParserService` (AI parse / resolve / rank) | `Services/` | | `OrderDraftApiController` | `Controllers/` | | `OrderDraftAdminController` (+ models) | `Areas/Admin/` | | `OrderDraftDbTable` / `OrderDraftItemDbTable` / `ExternalUserPartnerMapDbTable` / `ProductTextMappingDbTable` | `Domains/DataLayer/` | | `OrderDraft` / `OrderDraftItem` / `ExternalUserPartnerMapping` / `ProductTextMapping` entities | `FruitBank.Common/Entities/` |