70 lines
5.0 KiB
Markdown
70 lines
5.0 KiB
Markdown
# 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/` |
|