CONTEXT.md
Vit Store
A Mongolia-based ecommerce store (amerikvitamin.mn) selling supplement brands (NatureBell, MicroIngredients, Nutricost, etc.) with a storefront, admin dashboard, and server, deployed on Cloudflare Workers via Alchemy. Shopping is in Mongolian; prices in MNT.
Language
Customer:
A person who places orders. Identified by phone number (unique). Has an optional
psid (Facebook Page-Scoped ID) once they've ordered via Messenger.
Avoid: Client, buyer, account, user (user = admin/staff, see below)
User: An admin/staff member who operates the dashboard or (future) admin Messenger agent. Avoid: Customer, account
Page: A Facebook Page the Messenger bot is connected to. Distinguishes the test Page (now) from the production Page (future cutover). Avoid: Facebook page (just "Page")
PSID: Facebook Page-Scoped ID. Per-Page, opaque identifier for a person messaging the Page. Not the same as a facebook username; not stable across Pages. Avoid: facebook id, messenger id
Pre-order conversation:
A Messenger conversation before the customer has placed any order. Identity
lives only in the Flue agent session (Durable Object keyed by PSID). No
customer row, no DB writes.
Order:
A purchase placed by a Customer. Requires a phone number (customerPhone).
Carries address, delivery zone/provider, line items, total, and status.
Delivery fee:
The flat fee added to every Order's product total. 6,000 MNT today
(deliveryFee in @vit/shared/constants), sourced from the delivery provider.
The historical chat export shows 5,000 MNT — a legacy manual-pricing artifact
where the store ate the 1,000 MNT difference to ease chat orders. NOT a spec;
the Messenger agent must use the same 6,000 as the site (via order.addOrder,
which adds deliveryFee itself). The 5k is being phased out. Customers also ask
when delivery will arrive; default answer in existing chats is "margaash" / next
day unless a more precise delivery status is available.
Payment:
A request for payment attached to an Order. After Messenger order confirmation,
the bot presents two Messenger buttons: "QPay-р төлөх" and "Дансаар шилжүүлэх".
QPay opens a new QPay-only storefront page (/payment/qpay/{paymentNumber}?ct=...)
that shows QR/app links/checking only — no transfer option, to avoid confusion.
Existing APIs support this: order.addOrder returns paymentNumber +
checkoutToken; payment.createQr returns qr_image, qPay_shortUrl, and app
URLs; payment.checkQpayPayment verifies QPay and applies stock. Transfer sends
bank account + total in Messenger and includes a "Шилжүүлсэн" button. The bot
does not verify screenshots (abusable); pressing "Шилжүүлсэн" or saying
"hiisen"/"хийсэн" records/acknowledges a transfer claim and tells the customer
the order proceeds after admin/bank confirmation. Admin confirms real account
balance from dashboard / future Haan Bank client automation.
Transfer confirmation: The admin act of marking a bank-transfer Payment as confirmed (applies stock) or failed. Today done via dashboard tRPC mutation; the Messenger postback path is being retired.
Admin alert:
An outbound Messenger message pushed to the store owner's fixed PSID
(RECIPIENT_ID) to notify them of a new order, a claimed transfer, or a
confirmed payment. One-way; not a conversation.
Agent surfaces
Product discovery & advice: the customer-facing agent accepts BOTH typed
product fragments (e.g. "Selenium 200 mg bga yu") AND inbound photos, from
day one. Text is matched via searchProducts; photos are identified via a
vision model and matched to the catalog. No staged rollout between the two.
Real Messenger chats also include advice/comparison flows: "энэ юунд сайн бэ",
"али нь сайн бэ", "найрлага нь юу вэ", "яаж хэрэглэх вэ", and symptom/goal
requests (e.g. collagen, magnesium forms, children's vitamin D). The agent
should answer in the practical admin style: explain what the supplement is
commonly used for, compare forms/ingredients, mention label directions, and
recommend what might help. It must not claim products heal/cure/treat disease
or guarantee outcomes. Avoid heavy "consult a physician" boilerplate in normal
sales chats; use a brief safety caveat only for obvious high-risk cases
(children, pregnancy, medications, severe/chronic symptoms).
Image pipeline: the channel handler (trusted code) fetches the inbound
photo from Meta's CDN immediately, uploads it to the existing R2 bucket
(vit-store-bucket-prod, same one apps/server uses for product images) under
a dedicated messenger-inbound/ prefix, and dispatches only the R2 key to the
agent. The agent's identify_product_from_image tool reads the R2 object, calls
the vision model, and returns a rich text extraction (brand, name, dose, visible
label text, suggested query). The image itself never enters durable session
history — only text I/O is stored. R2 objects under messenger-inbound/ are
auto-expired after 3 days via an R2 lifecycle rule (zero-code cleanup; preserves
a short debug/tuning window without permanent storage).
Cart & ordering: Messenger bot v1 supports the full order flow with
confirmation gates. A conversational cart lives in the Flue DO session state,
surviving the 24-hour window. Product cards carry a "Захиалах" quick reply
(add_to_cart:productId:...); a "Сагс харах / Захиалга баталгаажуулах" button
on the cart summary commits. Commit triggers phone → address → delivery zone
resolution → notes collection → customer confirmation, then the addOrder
tool. The agent never computes totals itself — addOrder adds the 6,000 MNT
delivery fee.
Delivery zone resolution: Storefront Customers provide a natural address
and do not choose a TU Delivery zone. Storefront Orders keep addressZoneId
NULL until an admin selects a zone while sending the Order to TU Delivery; that
send stores the exact selected zone with no numeric fallback. Messenger may
still collect and submit a Customer-confirmed zone. A shared resolver may rank
or prefill zone choices using Google Maps/Places normalization, static
Ulaanbaatar landmark knowledge, historical Order examples, and a model ranker,
but the admin dispatch value remains explicit. Resolver accuracy stays a
separate workstream from the Messenger chatbot.
Model: one agent, one model — cloudflare/@cf/moonshotai/kimi-k2.6 via
Workers AI (no API key, AI Gateway on by default). Same model for the vision
tool call and the main chat turn. No second agent, no second model.
Tool style: Messenger bot v1 uses normal Flue defineTool tools (direct
tool calling), not Codemode. The bot is simple enough that direct tools are
more debuggable and keep the v1 critical path smaller. Codemode is reserved for
the separate delivery-zone resolver eval workstream and the future admin
Messenger agent, where tool count and multi-call orchestration will be much
higher.
Latency / UX: the agent runs two sequential model calls on a photo
(vision extraction, then reply composition) for a ~3–9s customer-perceived
total. The webhook still acks Meta in <1s (fetch + R2 put + dispatch). During
the agent's generation the channel sends Messenger typing indicators
(typing_on, re-fired periodically) with NO interim message. Typing clears when
the reply (product card) is sent.
Storefront: Astro SSR site (apps/storev2) on Cloudflare.
Server: Hono + tRPC on Cloudflare Workers (apps/server). Owns the existing
admin-alert Messenger outbound (messages.ts) and the (being-retired) inbound
postback webhook.
Flue Messenger channel: A new Flue application using @flue/messenger that
owns inbound Messenger Page events and dispatches them to a customer-facing
agent. Sole inbound consumer of the Page. Lives separately from apps/server.