Manual Order Creation
How a seller creates an order directly from the dashboard — either via the full
OrderFormor via the AI paste-and-parseQuickOrderEntry. Audience: new dev with Node/React experience, no Eziseller context.
1. Overview
Sellers manage orders that originate off-platform (phone calls, in-person, WhatsApp screenshots, etc.) by creating them manually from the dashboard. Two UI flows funnel into the same backend endpoint: a full form where every field is entered by hand, and a "quick entry" textarea that sends a pasted message to OpenAI in the browser, producing a draft the seller edits before submitting. Both flows hit POST /api/orders, which validates with Zod, upserts the customer, atomically reserves inventory, and writes the Order + OrderItem rows in a single transaction. This path is distinct from the public catalog checkout (no Razorpay) and from draft orders (a draft is not an order yet — see draft-orders.md).
2. Architecture
The frontend is a thin client — all business rules (pricing recomputation, stock check, customer upsert, order numbering) live in the Express route. The AI parser runs client-side and only produces a suggested payload; the seller always confirms it inside OrderForm before submit.
3. Data model
Order.customer is a JSON snapshot on the order row (name/phone/email/address) — there is no FK to Customer. The Customer table is a separate CRM-style list maintained by phone-number upsert. See schema.prisma.
4. Key flows
4.1 OrderForm submit
Prices from the client are ignored for items with a productId; the backend re-reads product.price / variant.price (orders.ts:L423-L441). Low-stock notifications fire after commit and never fail the order.
4.2 QuickOrderEntry (AI paste)
The parser runs entirely in the browser and returns a suggestion only — the server has no "AI mode" and applies the same validation regardless of source. See ai-parser.md.
5. Lifecycle
A freshly created manual order is always status=new, financialStatus=pending, fulfillmentStatus=unfulfilled. Full transitions in order-lifecycle.md.
6. Key files
- backend/routes/orders.ts:L371-L385 — route entry, auth + subscription gate + Zod
- backend/routes/orders.ts:L390-L391 —
ORD-${Date.now()}-${count+1}order number - backend/routes/orders.ts:L398-L471 — per-item re-pricing from DB
- backend/routes/orders.ts:L476-L511 — customer upsert by phone (non-blocking)
- backend/routes/orders.ts:L514-L584 — transaction: inventory + order + status history
- backend/routes/orders.ts:L619 —
incrementUsage(userId, 'orders')after commit - backend/lib/order-creation-service.ts — shared helpers used by public/webhook paths (not the manual route today, but the reference impl for future refactor)
- backend/lib/validation.ts:L130-L157 —
orderItemSchema+createOrderSchema - src/components/orders/OrderForm.tsx:L56-L140 — the full form and
onSubmit - src/components/orders/QuickOrderEntry.tsx:L23-L90 — paste box, calls
orderParser.parseMessage - src/lib/ai-parser.ts:L39-L116 — OpenAI call
- src/lib/ai-parser.ts:L275 — singleton constructed with
NEXT_PUBLIC_OPENAI_API_KEY - src/lib/api/orders.ts — frontend
CreateOrderDatatype + fetch wrapper
7. Env vars & config
| Var | Required | Purpose | What breaks |
|---|---|---|---|
NEXT_PUBLIC_OPENAI_API_KEY | no | Browser key used by ai-parser.ts for QuickOrderEntry | Without it the parser silently falls back to regex; accuracy drops but flow still works |
DATABASE_URL | yes | Prisma connection | 500 on any order create |
JWT_SECRET | yes | Validates access token on POST /api/orders | 401 |
Note: NEXT_PUBLIC_* means the OpenAI key is exposed to the browser. That is intentional today (per-seller BYO key in a future iteration), but treat it as a known risk.
8. Gotchas & troubleshooting
- Duplicate order numbers under load → Cause:
orderNumberis built fromprisma.order.count({where:{userId}}) + 1atorders.ts:L390-L391. The count runs outside the transaction, and there is no retry or unique-violation catch. Two concurrent creates for the same seller can produce the sameORD-<ts>-0042. → Fix: same fix tracked in invoice-numbering.md — move to a sequence table or a retry-on-unique-violation loop. Until then, collisions are rare but possible for sellers doing bulk imports. - Customer record not linked to the order → Cause:
Order.customeris a JSON snapshot (orders.ts:L537). Editing theCustomerrow later does not change historical orders. → Fix: this is intentional (historical accuracy). When you need current customer data, join by phone, not by FK. - Client-sent
unitPricelooks ignored → Cause: ifitem.productIdis present, the route overwritesunitPricefromproduct.price/variant.price(orders.ts:L423,L438). Only items withoutproductId(free-text AI-parsed items) honor the client price. → Fix: passvariantIdto pick variant pricing; never rely on FE price for catalog items. - AI parse returns garbage → Cause:
NEXT_PUBLIC_OPENAI_API_KEYunset or OpenAI down — parser falls back to regex (ai-parser.ts:L31-L36). → Fix: check browser console for "AI parsing failed, falling back to regex". - Usage counter mismatch → Unlike the public catalog bug where unauthenticated orders skipped
incrementUsage, the manual path is always authenticated and always callsincrementUsageafter commit (orders.ts:L619). If a seller hits their plan's order cap,checkSubscriptionLimit('orders')atorders.ts:L371rejects with 403 before any DB work. - Customer upsert silently swallows errors →
orders.ts:L507-L510wraps the upsert in try/catch and onlyconsole.warns. An order will commit even if theCustomerrow fails to write.
9. Extension points
- New source (e.g.
pos): add to thesourceenum increateOrderSchemaat validation.ts:L141. The route already passesorderData.sourcethrough. - Server-side tax: plug into tax-service.ts inside the item loop (
orders.ts:L398-L471) before computingtotalAmount. - Atomic order numbering: replace
order.count+ concat with a DB sequence or aCountertableupdate ... returninginside the existing transaction. - Server-side AI parsing: move
ai-parser.tsinto a backend route to hide the OpenAI key and share logic with the WhatsApp inbox.
10. Related docs
- order-lifecycle.md — status/financial/fulfillment state machine
- draft-orders.md — the "save for later" counterpart to manual creation
- order-creation-public-checkout.md — anonymous buyer path with Razorpay
- ../products/inventory.md —
checkAndReduceInventorysemantics - ../tax-and-invoices/invoice-numbering.md — same race as order numbering
- ../../02-integrations/messaging/ai-parser.md — OpenAI prompt details