Core
Orders
Order Creation Manual

Manual Order Creation

How a seller creates an order directly from the dashboard — either via the full OrderForm or via the AI paste-and-parse QuickOrderEntry. 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

7. Env vars & config

VarRequiredPurposeWhat breaks
NEXT_PUBLIC_OPENAI_API_KEYnoBrowser key used by ai-parser.ts for QuickOrderEntryWithout it the parser silently falls back to regex; accuracy drops but flow still works
DATABASE_URLyesPrisma connection500 on any order create
JWT_SECRETyesValidates access token on POST /api/orders401

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 loadCause: orderNumber is built from prisma.order.count({where:{userId}}) + 1 at orders.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 same ORD-<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 orderCause: Order.customer is a JSON snapshot (orders.ts:L537). Editing the Customer row 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 unitPrice looks ignoredCause: if item.productId is present, the route overwrites unitPrice from product.price / variant.price (orders.ts:L423,L438). Only items without productId (free-text AI-parsed items) honor the client price. → Fix: pass variantId to pick variant pricing; never rely on FE price for catalog items.
  • AI parse returns garbageCause: NEXT_PUBLIC_OPENAI_API_KEY unset 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 calls incrementUsage after commit (orders.ts:L619). If a seller hits their plan's order cap, checkSubscriptionLimit('orders') at orders.ts:L371 rejects with 403 before any DB work.
  • Customer upsert silently swallows errorsorders.ts:L507-L510 wraps the upsert in try/catch and only console.warns. An order will commit even if the Customer row fails to write.

9. Extension points

  • New source (e.g. pos): add to the source enum in createOrderSchema at validation.ts:L141. The route already passes orderData.source through.
  • Server-side tax: plug into tax-service.ts inside the item loop (orders.ts:L398-L471) before computing totalAmount.
  • Atomic order numbering: replace order.count + concat with a DB sequence or a Counter table update ... returning inside the existing transaction.
  • Server-side AI parsing: move ai-parser.ts into a backend route to hide the OpenAI key and share logic with the WhatsApp inbox.

10. Related docs