Order Status Conversational Chat Flow
This document is the plan for a guided, conversational order status experience inside the AI chat widget. When a customer asks about their order, the AI proactively asks for the order number, parses whatever format the customer types (e.g. order: 222, #222, just 222), fetches live data from Shopify Admin API, and replies with full order details — all without storing any order data in the database.
Prerequisites: Phase 12 (on-the-fly order lookup via order-lookup.server.js) complete; read_orders scope granted; fetchOrderByShop, formatOrderSummary, and extractOrderRefFromMessage are already in place. This phase enhances and extends that foundation.
1. Objective
Section titled “1. Objective”- Guided conversation: When a customer’s message signals order intent (e.g. “where is my order”, “track my package”, “order status”), the AI asks a clarifying question: “Sure! What is your order number or order ID?” — without making a Shopify API call prematurely.
- Flexible order ID parsing: Accept any reasonable format the customer types:
order: 222,order #222,#222,222,1001,order1234, etc. Strip noise, extract the numeric/alphanumeric identifier. - Rich Shopify data fetch: Query more order fields than Phase 12 (customer name, email, shipping address, tracking number, fulfillment events, estimated delivery) to give the AI more context for a detailed, accurate reply.
- Full context to OpenRouter: Pass all fetched order data plus the conversation history into the LLM prompt so the AI can answer naturally: status, items, tracking, estimated arrival, etc.
- Stateless for orders: Orders are never stored in the database. Each chat turn fetches live data from Shopify. Session state (are we waiting for an order number?) is tracked only in-memory within the RAG flow.
- Graceful fallbacks: Order not found → helpful message. Multiple orders with same number → show the most recent. Shopify API error → polite error reply. Customer provides invalid input → ask again once, then suggest contacting support.
2. How It Works Today (Reference Baseline)
Section titled “2. How It Works Today (Reference Baseline)”The current order flow in app/lib/rag.server.js (Phase 12):
User sends message → extractOrderRefFromMessage(message) with regex /#?\d{4,}/ or /order\s+(#?\w+)/i → If ref found OR orderNameOrId passed in body → fetchOrderByShop → formatOrderSummary → inject into prompt → LLM replies using that order contextGaps this phase addresses:
| Gap | Current Behaviour | Target Behaviour |
|---|---|---|
| No order number in message | AI doesn’t ask; replies “I don’t have info” | AI detects intent → asks for order number |
| Order ref parsing | Only matches # + 4+ digits or order <word> | Matches order: 222, 222, 1001, order-222, ORD-222 etc. |
| Shopify data fields | id, name, createdAt, financial/fulfillment status, total, line items | + customer name, email, phone, shipping address, tracking number, carrier, estimated delivery |
| Prompt injection | Short plain-text block | Structured, labelled block with all relevant fields |
| Conversational state | No tracking of “waiting for order number” | awaitingOrderNumber flag in session metadata |
3. Conversational State Design
Section titled “3. Conversational State Design”3.1 Session metadata field
Section titled “3.1 Session metadata field”Add a metadata JSON column to the ChatSession Prisma model to persist lightweight conversation state without a new table.
// In prisma/schema.prisma — ChatSession modelmodel ChatSession { // ... existing fields ... metadata Json? @default("{}")}Shape of metadata:
{ "awaitingOrderNumber": true, "orderIntentMessageId": "<chatMessageId of the message that triggered order intent>"}3.2 State transitions
Section titled “3.2 State transitions”[Normal chat] │ ├─ User message has order intent keyword (no order number yet) │ → Set session.metadata.awaitingOrderNumber = true │ → AI asks: "Sure! What is your order number or order ID?" │ → No Shopify call, no embedding, no RAG (direct reply path) │[Awaiting order number] │ ├─ User replies with something containing a parseable order number │ → parseOrderRef(message) → orderRef found │ → Clear awaitingOrderNumber flag │ → fetchOrderByShop → build rich prompt → LLM reply │ ├─ User replies with something but no parseable order number │ → AI replies: "I couldn't find an order number in that. Could you share just the number, e.g. 1001?" │ → Keep awaitingOrderNumber = true (one retry) │ └─ User changes topic entirely (no order signals at all) → Clear awaitingOrderNumber flag → Treat as normal RAG message4. Order Reference Parsing
Section titled “4. Order Reference Parsing”4.1 Current regex (Phase 12)
Section titled “4.1 Current regex (Phase 12)”const m = text.match(/#?\d{4,}/) || text.match(/order\s+(#?\w+)/i);Limitation: requires 4+ digit numbers; misses order: 222, short 3-digit order IDs, ORD-222, etc.
4.2 Enhanced parseOrderRef(text) function
Section titled “4.2 Enhanced parseOrderRef(text) function”Replace extractOrderRefFromMessage in rag.server.js with a new exported helper in order-lookup.server.js:
/** * Parse an order reference from any format the customer might type. * Returns the cleaned ref string (e.g. "1001", "ORD-222") or null. * * Handles: * "order: 222" → "222" * "order #1001" → "1001" * "order-1001" → "1001" * "#1001" → "1001" * "1001" → "1001" (standalone number, 3+ digits) * "ORD-222" → "ORD-222" * "my order is 222" → "222" */export function parseOrderRef(text) { if (!text || typeof text !== "string") return null; const t = text.trim();
// "order: 222", "order: #222", "order : 1001" const colonMatch = t.match(/order\s*:\s*#?(\w+)/i); if (colonMatch) return colonMatch[1].trim();
// "order #1001", "order 1001", "order-1001" const wordMatch = t.match(/order[\s\-#]+#?(\w+)/i); if (wordMatch) return wordMatch[1].trim();
// "#1001" or "#ORD-222" const hashMatch = t.match(/^#([\w\-]+)/); if (hashMatch) return hashMatch[1].trim();
// Standalone number: 3+ digits const numMatch = t.match(/\b(\d{3,})\b/); if (numMatch) return numMatch[1].trim();
return null;}4.3 Intent detection — hasOrderIntent(text)
Section titled “4.3 Intent detection — hasOrderIntent(text)”New helper to detect when a user is asking about an order (without providing a number yet):
const ORDER_INTENT_KEYWORDS = [ "order status", "my order", "track", "where is my", "when will", "delivery", "shipped", "shipping", "package", "dispatch", "order #", "order number", "fulfillment", "out for delivery", "arrived"];
export function hasOrderIntent(text) { if (!text) return false; const lower = text.toLowerCase(); return ORDER_INTENT_KEYWORDS.some((kw) => lower.includes(kw));}5. Enhanced Shopify Order Query
Section titled “5. Enhanced Shopify Order Query”5.1 Current query fields (Phase 12)
Section titled “5.1 Current query fields (Phase 12)”id, name, createdAt, displayFinancialStatus, displayFulfillmentStatus,totalPriceSet, lineItems(first: 20) { title, quantity }5.2 Enhanced query fields
Section titled “5.2 Enhanced query fields”Add to fetchOrderByShop in order-lookup.server.js:
query getOrderByName($query: String!) { orders(first: 1, query: $query) { edges { node { id name createdAt displayFinancialStatus displayFulfillmentStatus cancelledAt cancelReason note tags totalPriceSet { shopMoney { amount currencyCode } }
# Customer customer { firstName lastName email phone }
# Shipping address shippingAddress { firstName lastName address1 address2 city province country zip phone }
# Line items lineItems(first: 20) { edges { node { title quantity variant { sku } originalUnitPriceSet { shopMoney { amount currencyCode } } } } }
# Fulfillments (tracking) fulfillments { status createdAt updatedAt trackingInfo { number url company } estimatedDeliveryAt } } } }}5.3 Enhanced formatOrderSummary(order)
Section titled “5.3 Enhanced formatOrderSummary(order)”Rewrite the formatter to include all new fields, structured for clear AI comprehension:
export function formatOrderSummary(order) { if (!order) return ""; const lines = [];
lines.push(`Order: ${order.name}`); lines.push(`Placed: ${order.createdAt}`); lines.push(`Payment status: ${order.displayFinancialStatus}`); lines.push(`Fulfillment status: ${order.displayFulfillmentStatus}`);
if (order.cancelledAt) { lines.push(`Cancelled: ${order.cancelledAt}`); if (order.cancelReason) lines.push(`Cancel reason: ${order.cancelReason}`); }
if (order.totalPriceSet?.shopMoney) { const { amount, currencyCode } = order.totalPriceSet.shopMoney; lines.push(`Total: ${amount} ${currencyCode}`); }
// Customer if (order.customer) { const c = order.customer; const name = [c.firstName, c.lastName].filter(Boolean).join(" "); if (name) lines.push(`Customer: ${name}`); if (c.email) lines.push(`Customer email: ${c.email}`); if (c.phone) lines.push(`Customer phone: ${c.phone}`); }
// Shipping address if (order.shippingAddress) { const a = order.shippingAddress; const addrParts = [a.address1, a.address2, a.city, a.province, a.country, a.zip].filter(Boolean); lines.push(`Shipping to: ${addrParts.join(", ")}`); }
// Line items const items = order.lineItems?.edges ?? []; if (items.length > 0) { lines.push("Items ordered:"); items.forEach(({ node }) => { const price = node.originalUnitPriceSet?.shopMoney; const priceStr = price ? ` @ ${price.amount} ${price.currencyCode}` : ""; lines.push(` - ${node.title} x${node.quantity}${priceStr}`); }); }
// Fulfillments and tracking const fulfillments = order.fulfillments ?? []; if (fulfillments.length > 0) { lines.push("Fulfillments:"); fulfillments.forEach((f) => { lines.push(` Status: ${f.status}`); if (f.estimatedDeliveryAt) lines.push(` Estimated delivery: ${f.estimatedDeliveryAt}`); f.trackingInfo?.forEach((t) => { if (t.number) lines.push(` Tracking: ${t.number}${t.company ? ` (${t.company})` : ""}`); if (t.url) lines.push(` Tracking URL: ${t.url}`); }); }); }
if (order.note) lines.push(`Note: ${order.note}`);
return lines.join("\n");}6. RAG Flow Changes — rag.server.js
Section titled “6. RAG Flow Changes — rag.server.js”6.1 Updated generateChatReply logic
Section titled “6.1 Updated generateChatReply logic”The function gains two new behaviours based on session state:
ENTRY ↓Load session (existing or new) ↓Read session.metadata (awaitingOrderNumber flag) ↓[BRANCH A] awaitingOrderNumber === true → Try parseOrderRef(message) → If found: clear flag, fetch order, build full prompt, call LLM → If not found AND topic changed: clear flag, fall through to normal RAG → If not found AND topic still looks like order: reply asking again (no LLM call)
[BRANCH B] awaitingOrderNumber !== true → Try parseOrderRef(message) OR check orderNameOrId from request body → If ref found: fetch order immediately, build full prompt, call LLM → If no ref BUT hasOrderIntent(message): set awaitingOrderNumber=true, return clarifying question (NO embed/LLM call) → Normal RAG flow otherwise6.2 Session metadata update helpers (inline in rag.server.js)
Section titled “6.2 Session metadata update helpers (inline in rag.server.js)”async function setAwaitingOrderNumber(sessionId, value) { const session = await prisma.chatSession.findUnique({ where: { id: sessionId } }); const meta = (session?.metadata ?? {}); await prisma.chatSession.update({ where: { id: sessionId }, data: { metadata: { ...meta, awaitingOrderNumber: value } }, });}6.3 No-embed path for clarifying question
Section titled “6.3 No-embed path for clarifying question”When the AI is asking for the order number, we skip embedding and vector search entirely — return a hardcoded clarifying reply:
const CLARIFYING_REPLY = "Sure! Could you please share your order number or order ID? " + "You can find it in your confirmation email (e.g. #1001 or just 1001).";This saves embedding cost and latency for what is just a clarifying turn.
7. Prompt Injection Changes — prompt-builder.server.js
Section titled “7. Prompt Injection Changes — prompt-builder.server.js”The system prompt’s order block is enhanced with a stronger instruction:
--- Order Details (from Shopify, live data) ---Use this information to answer the customer's questions about their order.If tracking is available, include the tracking number and URL in your reply.If the order is cancelled, explain the cancel reason if provided.If estimated delivery is available, mention it.
{formatted order summary here}No changes needed to the function signature — buildPrompt(chunks, message, history, { orderContext }) continues to work the same way.
8. API Route Changes — api.chat.jsx
Section titled “8. API Route Changes — api.chat.jsx”No changes needed to the route handler itself. The orderNameOrId field is already extracted from the request body and passed to generateChatReply. The conversational state logic all lives in the RAG layer.
The frontend widget does not need to send orderName or orderId explicitly — the chat flow handles it conversationally.
9. Frontend Widget Changes — chat-widget.js
Section titled “9. Frontend Widget Changes — chat-widget.js”9.1 No structural changes needed
Section titled “9.1 No structural changes needed”The widget already sends { shopDomain, visitorId, sessionId, message } and displays whatever reply comes back. The AI will ask for the order number in natural language and the customer will reply as normal text — no special UI needed.
9.2 Optional: “Order Status” quick-reply button
Section titled “9.2 Optional: “Order Status” quick-reply button”As a UX enhancement, add a quick-reply chip at the start of a conversation:
[👋 Welcome message]Quick options: [Track My Order] [Product Questions] [Contact Support]Clicking “Track My Order” sends the message "I want to check my order status" automatically. The AI will then ask for the order number. This is purely a frontend enhancement and does not require backend changes.
Implementation: In chat-widget.js, after the welcome message renders, inject a <div class="appifire-quick-replies"> with buttons that call the existing sendMessage(text) function. Remove quick-reply buttons after the first user message.
10. Database Migration
Section titled “10. Database Migration”One Prisma migration is required to add metadata to ChatSession:
-- Migration: add metadata column to chat_sessionsALTER TABLE "chat_sessions" ADD COLUMN "metadata" JSONB DEFAULT '{}';Prisma schema change:
model ChatSession { id String @id @default(uuid()) shopId String visitorId String displayNumber Int startedAt DateTime @default(now()) metadata Json? @default("{}") // ← new field
shop Shop @relation(fields: [shopId], references: [id]) messages ChatMessage[]
@@map("chat_sessions")}Run: npx prisma migrate dev --name add-chat-session-metadata
11. Files to Create / Modify
Section titled “11. Files to Create / Modify”| File | Action | What changes |
|---|---|---|
prisma/schema.prisma | Modify | Add metadata Json? to ChatSession |
app/lib/order-lookup.server.js | Modify | Add parseOrderRef(), hasOrderIntent(); enhance fetchOrderByShop query; rewrite formatOrderSummary |
app/lib/rag.server.js | Modify | Replace extractOrderRefFromMessage with parseOrderRef/hasOrderIntent; add awaitingOrderNumber state logic; add setAwaitingOrderNumber helper |
app/lib/prompt-builder.server.js | Modify | Strengthen order block instructions in system prompt |
extensions/appifire-chat/assets/chat-widget.js | Modify (optional) | Add quick-reply buttons for “Track My Order” |
12. Full Conversational Example
Section titled “12. Full Conversational Example”Customer: "Where is my order?"
AI: "Sure! Could you please share your order number or order ID? You can find it in your confirmation email (e.g. #1001 or just 1001)."
Customer: "order: 222"
[Backend: parseOrderRef("order: 222") → "222" fetchOrderByShop(shop, "222") → order object formatOrderSummary(order) → rich text block buildPrompt(chunks, message, history, { orderContext }) → LLM messages callOpenRouterChat(messages, plan) → reply]
AI: "Here are the details for order **#222**:
- **Status:** Fulfilled ✓- **Items:** Blue Running Shoes x1 @ 89.99 USD- **Shipped to:** 123 Main St, New York, NY, USA- **Tracking:** UPS 1Z999AA10123456784 [Track your package →](https://wwwapps.ups.com/tracking/...)- **Estimated delivery:** March 28, 2026
Is there anything else I can help you with?"13. Error & Edge Cases
Section titled “13. Error & Edge Cases”| Scenario | Handling |
|---|---|
| Order not found in Shopify | AI replies: “I couldn’t find order #222. Double-check the number in your confirmation email, or contact our support team.” |
| Customer provides garbled input (e.g. “it’s the number I got”) | AI asks one more time: “I couldn’t spot an order number there. Could you share just the number, like 1001?” then after second failure: “Let me connect you with our support team instead.” |
| Shopify API returns error (network/auth) | Log error server-side; AI replies: “I’m having trouble retrieving your order right now. Please try again in a moment or contact support.” |
read_orders scope not granted | fetchOrderByShop returns { order: null, error: "Shopify API error: 403" }; AI replies with support redirect message |
| Customer changes topic mid-flow (e.g. “actually, what’s your return policy?”) | hasOrderIntent returns false → clear awaitingOrderNumber flag → treat as normal RAG message |
| Multiple orders same number (shouldn’t happen; Shopify order names are unique per shop) | first: 1 in query returns the most recent; note in summary if needed |
14. Security Considerations
Section titled “14. Security Considerations”- No customer verification: The chat does not verify that the order belongs to the customer. Any visitor who knows or guesses an order number can see its details. This is consistent with Shopify’s standard order status page behaviour (accessible via order number + email or just link).
- What is exposed: Order name, status, items, shipping address, tracking number. No payment card info is ever returned by the Shopify orders API.
- Access token: Uses the shop’s app access token (stored in DB). Token has
read_ordersscope only — no ability to modify orders. - Rate limiting: Shopify Admin API rate limits apply. Each chat turn that includes an order lookup = 1 GraphQL query. Cost is minimal.
15. Implementation Sequence
Section titled “15. Implementation Sequence”- Prisma migration — Add
metadatatoChatSession; run migration on dev and production. order-lookup.server.js— AddparseOrderRef,hasOrderIntent; enhance GraphQL query; rewriteformatOrderSummary.rag.server.js— Replace old regex helper; add session state logic; add clarifying reply path.prompt-builder.server.js— Update order block system prompt text.- Manual test — In the chat widget on the test store:
- Ask “where is my order” → verify AI asks for order number.
- Reply with
order: 222→ verify order details appear in reply. - Reply with
#1001→ verify correct parsing. - Reply with an invalid number → verify graceful not-found message.
- Change topic mid-flow → verify
awaitingOrderNumberis cleared.
- Widget quick-reply buttons — Add “Track My Order” chip in
chat-widget.js.
16. Local Debug Script (Order Lookup)
Section titled “16. Local Debug Script (Order Lookup)”Use this helper script to test order lookup without going through the storefront chat widget:
- Script path:
scripts/test-order-lookup.mjs
Examples:
node scripts/test-order-lookup.mjs --shop=appifire-ai-chat-test-store.myshopify.com --orderRef=#1001node scripts/test-order-lookup.mjs --shop=appifire-ai-chat-test-store.myshopify.com --input="My order number is 1001"Expected output includes:
ok,found,error- parsed reference (
parsedOrderRef) - token source (
session_tableorshops_table) orderName- enriched
orderSummaryfromformatOrderSummary()