diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index f4f84a9..0bdff91 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -21,13 +21,5 @@ npx prisma db push --accept-data-loss echo "Regenerating Prisma Client..." npx prisma generate -if [ "$RUN_TEMPLATE_SEED_ON_START" = "true" ]; then - echo "Seeding predefined email templates..." - # Run seed script; if it fails, log the error but continue starting the app - if ! npx tsx scripts/seed-templates.ts; then - echo "Template seeding failed, continuing without seeded templates." - fi -fi - echo "Starting application..." exec "$@" diff --git a/docs/ai-features-configuration.md b/docs/ai-features-configuration.md new file mode 100644 index 0000000..81a8177 --- /dev/null +++ b/docs/ai-features-configuration.md @@ -0,0 +1,53 @@ +# AI features — configuration and flows (high level) + +This document describes how customer-facing AI is wired today: entry points, major pipeline steps, configuration stored on `Store`, and known gaps. Use it when changing ticket creation, Gmail behavior, or module gating. + +## Surfaces + +| Surface | Primary code | Notes | +|--------|----------------|------| +| **Gmail inbound** | [`src/lib/gmail/emailProcessor.ts`](../src/lib/gmail/emailProcessor.ts), webhook [`src/app/api/webhooks/gmail/route.ts`](../src/app/api/webhooks/gmail/route.ts) | Processes one unread message; builds a reply via [`generateAutoReplyPreview`](../src/lib/gmail/autoReplyPreview.ts) and sends it. | +| **Storefront widget** | [`src/app/api/storefront-chat/messages/route.ts`](../src/app/api/storefront-chat/messages/route.ts), [`src/lib/ai/storefrontGroq.ts`](../src/lib/ai/storefrontGroq.ts) | Public API; resolves store by `shop` domain; runs the same L1 orchestrator as email with `channel: "widget"`. | + +## Gmail reply composition (`generateAutoReplyPreview`) + +Rough order: + +1. **Safety** — [`screenInboundEmailForAbuse`](../src/lib/ai/gmailSafety.ts); flagged messages get a minimal HTML response and no order data. +2. **Classification** — [`classifyCustomerEmail`](../src/lib/ai/gmailGroq.ts) (Groq) yields Gmail intent/mood for logging and guardrails; order-context lines are built with heuristics in the same file (when to load cached orders, when to ask for order number first, off-topic detection, etc.). +3. **L1 orchestrator** — If not skipped (off-topic, too sparse, needs order id first, hint unmatched), [`runL1Orchestrator`](../src/lib/ai/orchestrator/runL1.ts) runs with `channel: "email"` and `emailFrom` set to the sender. +4. **HTML fallbacks** — If L1 returns empty text, built-in HTML snippets apply (off-topic boundary, need order id, hint unmatched, minimal acknowledgment). Vendor-editable HTML templates were removed; there is no DB template fallback. + +Relevant store fields on this path: `businessName`, `email`, `aiModules`, `modulePolicies` (see below). + +## L1 orchestrator (`runL1Orchestrator`) + +Single implementation for **widget** and **email**: + +1. **Retrieval** — [`getBlendedStorefrontContext`](../src/lib/storefront-chat/retrieval.ts) blends knowledge chunks / FAQs for the prompt context. +2. **Intent → module** — [`routeCustomerIntent`](../src/lib/ai/orchestrator/intentRouter.ts) (Groq JSON) maps the customer message to an [`L1Module`](../src/lib/ai/orchestrator/types.ts). +3. **Module gating** — [`parseAiModulesJson`](../src/lib/ai/orchestrator/aiModules.ts) + [`isModuleEnabled`](../src/lib/ai/orchestrator/aiModules.ts). Disabled modules fall back to **FAQ**. **`FAQ` is always treated as enabled** (cannot be turned off via JSON). **`UNKNOWN`** stays enabled for routing stability. +4. **Pillar** — Each module has a pillar in [`MODULE_MANIFESTS`](../src/lib/ai/orchestrator/manifests.ts): `K` (knowledge), `T` (transactional / DB), `X` (ticket handoff copy), `CAT` (catalog metafields). +5. **Verification** — Widget: OTP-verified session on [`StorefrontChatConversation`](../prisma/schema.prisma) (asserted email + expiry). Email: trust `From` as customer email for order lookup (see `runL1` and order tools). +6. **Tools** — Orders: [`orderTools`](../src/lib/ai/tools/orderTools.ts). Products: [`productTools`](../src/lib/ai/tools/productTools.ts) for search + product metafield summaries in L1 (no separate category-metadetails path in the vendor UI). +7. **Reply** — Mostly [`composeReply`](../src/lib/ai/orchestrator/runL1.ts) (Groq) over assembled facts. For pillar **`X`**, the customer sees fixed **handoff** copy from `ticketHandoffText` — **no ticket row is created in the database**. + +Vendor UI for `aiModules` and `modulePolicies`: dashboard **JUNO Engine → Modules**; persistence via `GET`/`PATCH` [`/api/stores/[storeId]`](../src/app/api/stores/[storeId]/route.ts). + +## Knowledge base (separate from module toggles) + +- **Mode and content** — [`Store.knowledgeBaseMode`](../prisma/schema.prisma), `knowledgeTemplate` JSON, documents, FAQs, structured chunks. APIs under [`src/app/api/stores/[storeId]/knowledge-template`](../src/app/api/stores/[storeId]/knowledge-template/route.ts) and KB subroutes. +- **Public query** — e.g. [`src/app/api/stores/[storeId]/kb/query/route.ts`](../src/app/api/stores/[storeId]/kb/query/route.ts) for RAG-style use. + +Module toggles do **not** turn off the knowledge base; they only restrict which **L1 modules** can run after routing. + +## Known gaps (intentional follow-ups) + +1. **Ticket-led (`X`) flows** — The orchestrator acknowledges and promises human follow-up but **does not create a ticket entity**. Vendor **Tickets** in the dashboard are derived from [`EmailLog`](../src/app/api/juno-engine/tickets/route.ts), not from L1. +2. **Spec vs router** — Vendor policy text is stored per [`L1Module`](../src/lib/ai/orchestrator/types.ts) in `Store.modulePolicies` (AI modules UI). The [`intentRouter`](../src/lib/ai/orchestrator/intentRouter.ts) only emits declared modules. Extending automation requires adding a module in types, `MODULE_MANIFESTS`, `DEFAULT_ENABLED` in `aiModules.ts`, and orchestrator branches in `runL1.ts`. +3. **Gmail vs widget parity** — Same L1 core; Gmail adds a pre-L1 classification layer and different skip rules for when L1 runs. + +## Environment / models + +- Groq and related env vars are used by `groqClient`, `gmailGroq`, `intentRouter`, and `composeReply`. +- Rate limits and leases for Gmail inbound: [`gmailAutoReplyGuards`](../src/lib/gmail/gmailAutoReplyGuards.ts). diff --git a/docs/gmail-templates-catalog.md b/docs/gmail-templates-catalog.md index 24a4cc4..9ee4993 100644 --- a/docs/gmail-templates-catalog.md +++ b/docs/gmail-templates-catalog.md @@ -619,12 +619,11 @@ You can paste this into **variables** metadata in the UI if your editor supports - **Intent/mood:** Groq classifies the incoming message (`classifyCustomerEmail` in `src/lib/ai/gmailGroq.ts`). Returns `source: "groq" \| "fallback"` when the API or JSON parse fails (defaults to `general` + `neutral`). - **Sensitive-topic heuristic:** `looksLikeSensitiveOrderTopic` in `src/lib/ai/gmailOrderDisclosure.ts` aligns disclosure with shipping/refund/billing wording when the classifier misses. - **Hints:** `stripQuotedEmailTrailForHints` + `extractOrderNumberHints` in `src/lib/ai/gmailContext.ts` reduce false hints from quoted threads. -- **Reply body:** `processIncomingEmail` in `src/lib/gmail/emailProcessor.ts` loads a template with `findTemplateWithFallback` and renders `{{variables}}`. **No** `generateReplyAndTemplateFields` / **no** auto-`insertTemplate` from Gmail. -- Templates are keyed by `storeId` + `intent` + `mood` (see `src/lib/juno/templatesRepo.ts`). +- **Reply body:** `processIncomingEmail` calls `generateAutoReplyPreview` (`src/lib/gmail/autoReplyPreview.ts`): L1 orchestrator when guardrails allow, else built-in fallback HTML. Vendor-editable DB templates and `generateReplyAndTemplateFields` were removed. - **Order context:** `buildOrderSummaryForEmail` reads **CachedOrder** only; keep cache in sync with Shopify. -- **Fallback HTML:** `buildOffTopicFallbackHtml`, `buildNeedOrderIdFallbackHtml`, `buildHintUnmatchedFallbackHtml`, or `buildMinimalFallbackHtml` if no template matches. +- **Fallback HTML:** `buildOffTopicFallbackHtml`, `buildNeedOrderIdFallbackHtml`, `buildHintUnmatchedFallbackHtml`, or `buildMinimalFallbackHtml` when L1 is skipped or returns empty. - **Abuse / idempotency:** Per-sender caps via `EmailLog` (`checkGmailAutoReplyGuards`); duplicate inbound Gmail `message` ids use `GmailInboundAutoReply` so the same message is not auto-replied twice. Env: `GMAIL_AUTO_REPLY_MAX_PER_SENDER_PER_HOUR` (default 15), `GMAIL_AUTO_REPLY_BURST_MAX_PER_10MIN` (default 6); set to `0` to disable that cap. --- -*Generated for JUNOHUB — intents/moods aligned with `src/lib/ai/gmailTaxonomy.ts` and `prisma/schema.prisma` `Template` model.* \ No newline at end of file +*Historical doc: intents/moods still align with `src/lib/ai/gmailTaxonomy.ts`. The `Template` table may exist in older DBs but is no longer used by the Gmail reply path.* \ No newline at end of file diff --git a/docs/vendor_use-case_implemenation.md b/docs/vendor_use-case_implemenation.md new file mode 100644 index 0000000..8c5ef9c --- /dev/null +++ b/docs/vendor_use-case_implemenation.md @@ -0,0 +1,773 @@ +# Vendor use-case knowledge modules & L1 agentic implementation plan + +**Read this first (integration).** This file is an **implementation-oriented plan** meant to be **merged into an existing codebase** (Shopify sync, dashboard, DB already in place). **You are not required to follow** the **module codes**, **`shared_field_id`** strings, **table/column names**, **tool names**, **flags**, **slot keys**, or any other **rigid-looking** labels in this document when they would **create conflict** with your current schema, migrations, types, or APIs. Those conventions exist **only** to keep the spec **internally consistent** and to explain **how** the system should behave. **Map concepts, not strings:** keep the **ideas** (which flows are L1 vs ticket vs KB, verification rules, prerequisites, tool-call *shape*); **rename and wire** to whatever your project already uses. If spec and repo disagree on names, **default to the repo** unless you explicitly refactor. + +> **Same caution, shorter:** Identifiers in this doc are **illustrative**, not a mandate. **Codebase wins** over literal tags here. Preserve **behavior and structure**, not every ID. + +**Purpose.** This document defines **shortlisted customer-support use-cases** as **optional sub-modules**: vendor **knowledge (FAQ-style)**, **L1 transactional automation** (read/act on data you already sync from Shopify into your database), and **ticketing** (human resolution). It also specifies **two customer interfaces** (email + storefront widget), **verification** (email + OTP only), and **constraints** so implementations stay consistent. + +**Scope (agentic layer only).** Shopify store, vendor dashboard, and **database hydration from Shopify** are **assumed to exist**. This plan covers the **L1 automation layer** (intent handling, retrieval, slot filling, verification, safe responses, ticket creation). It does **not** describe full end-to-end replacement of human support. + +**Data access (tool calls).** The agent does **not** “know” order lines, inventory, tracking, or **category metadetails** by default. Implementation must expose **tool calls** (or equivalent server-side functions) that **query your database** using your **actual schema** and **field names** for each store. Answers for **transactional** flows and for **catalog metadata** flows are only as good as (a) what is synced into the DB and (b) which tools the orchestrator is allowed to invoke for that module. Document concrete tool names, parameters, and return shapes in engineering specs **for your real schema** — those names **need not** match any examples elsewhere in this document (see **Read this first** above). + +**Excluded use-cases** (out of scope for this framework): + +- Subscription / repeat orders +- Wholesale / B2B / large orders +- Account & security (e.g. Shopify permission / identity beyond email+OTP) +- VIP / loyalty context +- **Shopify customer login** and **magic-link** verification (explicitly not used) + +--- + +## 0. What we automate vs what humans do + +### 0.1 L1 only — not the whole pipeline + +- **L1** = deflection and **safe, bounded** automation: answer from **knowledge**, **read** order/product state from your DB, perform **allowed** actions (e.g. cancel when rules say yes), and **hand off** when the job requires judgment, negotiation, or operational access beyond the playbook. +- **Tickets** = default path when the use-case is **human work** (order change, warranty processing, payment disputes, complaint resolution, return/exchange **handling**, etc.). There is **no generic end-to-end workflow** for tickets in the agentic sense: a **real human** owns the ticket. The agent may **draft context** for the human (summary, slots filled, policy snippets), but **resolution is human**. + +### 0.2 Three pillars (every module maps to one primary pillar) + +| Pillar | Meaning | Typical outputs | +|--------|---------|-----------------| +| **Knowledge (FAQ)** | Answers from vendor-configured text + embeddings / RAG; **no** order mutation; may not need identity. | Policy explanations, links, “how we work.” | +| **Transactional L1** | Uses **verified identity** + **prerequisites** (e.g. order id); **reads** DB (and optionally **acts** where allowed). | Order status, tracking snapshot, cancel eligibility + cancel, refund **status** lookup, inventory lookup. | +| **Ticket-led** | After verification (when needed), **create or continue a ticket**; customer-facing reply is acknowledgement + expectations; **human** executes. | Order change, warranty, payment disputes, wrong-item resolution, return/exchange **initiation**, complaints, many shipment exceptions. | +| **Catalog metadata (DB-backed)** | Answers grounded in **structured fields** synced from the storefront (e.g. **category metadetails**), retrieved via **tool calls** — not free-form vendor FAQ text for those facts. | Category-wide sizing notes, materials, care, compatibility text the merchant attached at **category** level; applies to **all products** in that category. | + +**Note:** “Refund **policy**” (e.g. “how long do refunds take?”) is **Knowledge**. “**Refund status** for my order” is **Transactional** (needs identification + order/return context in DB). + +**Category metadetails module (vendor toggle).** Merchants can maintain **details at the category level** in Shopify (and equivalents); once synced to your DB, that data can power answers (e.g. fit, fabric, voltage) **across every product in those categories**. In the product, the vendor has a **single switch**: **enable** or **disable** use of category metadetails for automation — **no per-category granularity** in v1. When **disabled**, the agent must **not** use tool calls that return category metadetails (fall back to KB, PDP links, or ticket). When **enabled**, the agent may answer from **metadetails of all categories/products** the store has synced, subject to intent and **tool-call** availability. + +### 0.3 Tickets and email + +- Tickets are **internal** objects; **conversation with the customer** for ticket-heavy flows is still primarily **email** (and mirrored logic on the widget where applicable). +- If the customer is **already on email**, opening a ticket (e.g. return initiation, complaint) can be **initiated automatically** from that thread once rules are satisfied. +- If the customer is on the **widget**, they complete **email + OTP + session** first; then “open a ticket” creates the ticket and **subsequent human↔customer** dialogue happens over **email** (or the same channels you define in `sf_support_channels`). + +--- + +## 1. Customer interfaces + +### 1.1 Email + +- **Identity signal:** the **sender address** (`From:`) is the customer email for that thread. +- **Strict rule (transactional):** for any **fiber** / sensitive action (order status, cancel, refund status, shipping details tied to an order, etc.), the **only** email identity you trust for lookup is **`From` matches the email on the order / customer record** in your database. The customer **cannot** ask for someone else’s order from their own inbox; they must **send from the email that placed the order** (e.g. they cannot use their personal Gmail to query a friend’s order placed under the friend’s email). +- **Verification path on email:** **no OTP** in v1 of this plan for email (optional future); trust is **email alignment** with DB. If `From` is not associated with any customer/order, respond with a **generic** safe message (do not confirm whether an order exists for another email) and instruct them to contact support from the **email used at checkout**. +- **Multi-order disambiguation:** if `From` matches a customer with **multiple open or recent orders**, L1 **lists** candidate orders (e.g. id, date, short descriptor) and asks **which order** they mean before fetching full details or acting. + +### 1.2 Storefront widget (chat) + +- **Unauthenticated path:** if the user only needs **Knowledge (FAQ)** and the vendor has enabled that module, answer using RAG/KB **without** collecting email (no session required for pure FAQ). +- **Transactional path:** to run any **Transactional L1** or **Ticket-led** flow that depends on **who** the customer is: + 1. Collect **email address** (the one they assert is their order email). + 2. Start a **server-side session** bound to that email attempt; show the user that the session is **valid for a fixed TTL** (e.g. **1 hour**) and that further questions in that window use this context. + 3. Send **OTP** to that email; user enters OTP in the widget. + 4. On success, mark session **verified** for that email; proceed with slot filling (order id, product name, etc.) and L1 or ticket creation. +- **No Shopify login, no magic links** — only **email + OTP** for widget verification. + +### 1.3 OTP (widget) — behavioral requirements + +- Rate-limit sends and attempts; invalidate OTP after use or expiry. +- Session store: `session_id` → `asserted_email`, `verified_at`, `expires_at`, `store_id`, optional `bound_order_id` after disambiguation. +- After TTL expiry, **re-OTP** required for any new transactional request. + +--- + +## 2. Verification, confidentiality, and prerequisites (cross-cutting) + +### 2.1 Constraint: no cross-email order access + +- You **must not** disclose order data unless the **verified channel identity** matches the **customer/order email** in the database (email channel: `From`; widget: OTP-proved email). +- **Friend’s order:** impossible to service from the wrong inbox; customer must use the **purchaser’s email** (and that person must be the one in the thread or OTP flow). + +### 2.2 Prerequisites pattern + +For each **Transactional** or **Ticket-led** module, define: + +- **Required slots** (e.g. `order_id`, `product_query`, `issue_subtype`). +- **Verification gate:** `email_aligned` (email) or `otp_verified_session` (widget). +- **L1 action:** read-only, read+act (limited), or **create ticket only**. + +### 2.3 Slot filling and back-and-forth + +- Orchestrator state: `collect_slots` → `verify` (widget OTP) → `execute_l1` or `open_ticket` → `respond`. +- Ask **only** for the **next missing** prerequisite. +- Reuse **one verified session** on the widget for multiple questions within TTL (e.g. status then tracking for same order). + +### 2.4 Module anatomy: **three types** of configuration / questions + +Every **use-case module** is implemented as a bundle of **three** layers. The vendor dashboard and engineering manifests should reflect these explicitly (tabs, accordions, or sections). + +| Type | Name | Who fills it | What it controls | +|------|------|----------------|------------------| +| **Type 1** | **Policy & messaging** | **Vendor** (questionnaire) | Text the agent may cite or blend into replies: tone-adjacent facts, “what we tell customers,” links, windows, exclusions — stored as `shared_field_id` (or your equivalent). **Does not** replace DB facts for transactional answers. | +| **Type 2** | **Verification & prerequisites** | **Platform rules** (+ minimal vendor toggles if you add them later) | **Who** is allowed to run this flow and **which slots** must be collected first: `From` email vs order email, widget **email + OTP + session TTL**, `order_id`, `product_name`, disambiguation when multiple orders match, etc. | +| **Type 3** | **L1 automation & ticketing** | **Engineering** (manifest) + **vendor enable** | After Type 2 passes: **tool calls** / DB reads, optional **writes** (e.g. cancel), composition of the reply; and **when** to **open a ticket**, what to attach, and that **humans** own ticket resolution. | + +**How they work together (example: `ORDER_STATUS`):** + +- **Type 1:** Vendor answers how long processing usually takes, what you say if pre-ship delay, shipping regions, how customers find order # — so the reply can **wrap** factual status with **on-brand** messaging. +- **Type 2:** Customer must be verified (**email** on channel = email on order, or **widget OTP**); **`order_id`** required or disambiguated from a short list. +- **Type 3:** L1 **reads** order + fulfillment from DB via tools, **replies** with status + tracking snippets + Type 1 copy; if the case is **outside playbook** (data missing, anomaly), **open ticket** for a human. + +**Modules that are “mostly FAQ”** still have all three types logically: **Type 1** = full questionnaire; **Type 2** = often **none** (no identity); **Type 3** = RAG/KB retrieval only, plus **optional** “suggest human / ticket” if out of scope. + +**Modules that are “mostly ticket”** have heavy **Type 3** ticket path: L1 may only **intake + acknowledge + create ticket**; **Type 1** still supplies policy snippets in the auto-reply. + +--- + +## 3. Use-case catalog — categorization (vetted) + +**Legend** + +- **K** = Knowledge (FAQ / KB; module enabled gates automation). +- **T** = Transactional L1 (DB read / limited write after verification + slots). +- **X** = Ticket-led (human resolves; L1 may intake + acknowledge + ticket). +- **Catalog metadata (DB-backed)** = see §0.2; answers use **tool calls** against synced **category metadetails** when **`CATEGORY_METADETAILS`** is enabled (no per-category toggle in v1). + +| Module code | Customer-facing use-case | Primary pillar | Prerequisites (typical) | L1 automation (agentic) | Human / ticket | +|-------------|----------------------------|----------------|---------------------------|---------------------------|----------------| +| `ALL` | Global store context | — | — | Informs all replies | — | +| `FAQ` | General policies & “how we work” | **K** | None for pure policy | RAG / structured FAQ; **refund policy** lives here, not “status” | If out of scope → suggest email or ticket | +| `CATEGORY_METADETAILS` | Category-level product details from storefront sync | **Catalog metadata (DB-backed)** | Product or category resolution (name/SKU → category); **module must be enabled by vendor** | **Tool calls** to DB: read **category metadetails** fields for the resolved category(ies); use for factual answers store-wide | If disabled, off, or no row → KB / PDP link / ticket | +| `PRODUCT_FIT` | Product fit, sizing, compatibility | **K** *(optional light T: product resolve)* | Product name/SKU for specificity | Answer from KB + **category metadetails (if `CATEGORY_METADETAILS` enabled)** via tools + product metafields/chunks; link to PDP chart | Edge cases → ticket | +| `STORE_LOCAL` | Store hours, pickup, local | **K** | None | KB only | — | +| `FEEDBACK` | After-resolution feedback | **K** | Optional ticket id | Link to survey / ask once | Negative follow-up → human | +| `HUMAN_ESCALATION` | “I want a person” | **K** / routing | None or context | Ack + routing copy + create ticket if policy says so | Human handles queue | +| `ORDER_STATUS` | Where is my order | **T** | Verified identity + `order_id` (or disambiguation) | Read order state from DB; reply with status summary | If anomaly outside playbook → ticket | +| `ORDER_SUMMARY` | Order summary / receipt recap | **T** | Same | Read line items / totals from DB | Tax invoice edge cases → ticket if vendor policy says | +| `SHIPPING_TRACKING` | Carrier, tracking, ETA | **T** | Same | Read tracking fields from DB for that order | Carrier claim / dispute → **X** | +| `ORDER_CANCEL` | Cancel order | **T** | Same + business rules in DB | Eligibility check + cancel if allowed; else explain from KB | If ambiguous or edge case → **X** | +| `REFUND_STATUS` | Status of **my** refund / return payout | **T** | Verified identity + order (or return) reference | Read refund/return state from DB if modeled; else templated “we’ll update you” + **X** | Human if data missing | +| `INVENTORY_STOCK` | Is this in stock | **T** | `product_name` / SKU (widget may need session only if tied to customer cart—optional) | Read inventory from DB | Disambiguation across many SKUs | +| `NOT_RECEIVED_MARKED_DELIVERED` | Tracking says delivered, I don’t have the parcel | **X** | Verified identity + `order_id` | L1: confirm tracking snapshot + vendor policy snippet + **open ticket** | Human investigates carrier / claim | +| `SHIPMENT_STUCK_OR_DELAYED` | Shipment not moving / very late vs expectation | **X** | Verified identity + `order_id` | L1: last known scan + policy snippet + **open ticket** | Human chases carrier / ops | +| `PAYMENT_PROBLEM` | Double charge, wrong amount, promo failed, etc. | **X** | Verified identity + **order reference when payment is order-scoped**; some sub-intents need **last4 / date** as slots | L1: structured intake + **ticket**; **no** automated money movement | Human + finance tools | +| `ORDER_CHANGE` | Change address / items / qty | **X** | Verified identity + `order_id` + requested change | L1: **open ticket** + acknowledgement (human decides feasibility) | Human confirms “possible / not possible” | +| `RETURN_EXCHANGE` | Start return or exchange | **X** | Verified identity + `order_id` (+ reason) | L1: policy from KB + **open ticket** (or portal link if you add later) | Human / ops fulfills RMA | +| `WARRANTY` | Warranty claim (post–return window) | **X** | Verified identity + proof-of-purchase / order | L1: intake + **ticket** | Human adjudicates | +| `WRONG_ITEM` | Wrong / damaged / missing item | **X** | Verified identity + `order_id` + issue type (+ photos on email) | L1: acknowledge + policy + **ticket** with structured fields | Human resolves (reship/refund) | +| `COMPLAINT` | Strong dissatisfaction | **X** | **Strict verification** same as other fiber flows | L1: empathetic ack + **ticket** + severity | Human owns resolution | + +### 3.1 Per module: Type 1 (policy) · Type 2 (verification) · Type 3 (L1 / ticket) + +Use this matrix with **§2.4** and **§7**. **Type 1** detail lives in the **§7** tables (`shared_field_id`). **Type 2** and **Type 3** are the minimum spec for manifests and runtime. + +| Module | **Type 1 — Policy & messaging** (vendor Qs) | **Type 2 — Verification & prerequisites** | **Type 3 — L1 automation & ticketing** | +|--------|---------------------------------------------|---------------------------------------------|--------------------------------------| +| `ALL` | Global G1–G12 (applies across modules) | N/A at module level | Wires into prompts for every reply | +| `FAQ` | FQ1–FQ8 | **None** for anonymous FAQ | RAG / structured FAQ; OOD → suggest email or ticket | +| `CATEGORY_METADETAILS` | **None** (store toggle only) | Vendor **enables** feature; **product/category** resolution (name/SKU → category) | **Tool calls** read category metadetails from DB; if off/empty → fallback KB or ticket | +| `PRODUCT_FIT` | PF1–PF4 | **Optional** product/slot for specificity | KB + optional metadetails tools; edge → ticket | +| `STORE_LOCAL` | SL1–SL4 | **None** | KB-only replies | +| `FEEDBACK` | FB1–FB3 | Optional link to prior ticket/context | Send CSAT / link; negative path → human | +| `HUMAN_ESCALATION` | HE1–HE4 | **None** or minimal context | Ack + routing; **create ticket** if policy requires | +| `ORDER_STATUS` | OS1–OS5 (+ ALL for order# help) | **Email = order email** OR **widget OTP session**; **`order_id`** or **multi-order pick** | **L1:** tool read order/fulfillment → reply; **Ticket:** anomaly / missing data / out-of-playbook | +| `ORDER_SUMMARY` | SM1–SM4 (+ ALL) | Same as order-scoped flows | **L1:** read line items/totals → reply; **Ticket:** tax/invoice edge cases per vendor policy | +| `SHIPPING_TRACKING` | ST1–ST6 (+ ALL) | Same as `ORDER_STATUS` | **L1:** read tracking fields → reply; **Ticket:** carrier dispute / claim handoff | +| `ORDER_CANCEL` | CX1–CX5 (+ ALL) | Same as `ORDER_STATUS` | **L1:** eligibility + cancel if allowed; else KB + **Ticket** if ambiguous | +| `REFUND_STATUS` | RF1–RF6 (+ ALL) | Verified identity + **order/return** reference | **L1:** read refund state if in DB; else ack + **Ticket** | +| `INVENTORY_STOCK` | IV1–IV5 | **`product_name`/SKU**; widget OTP **optional** per product policy | **L1:** inventory tool read; heavy disambiguation → clarify or ticket | +| `NOT_RECEIVED_MARKED_DELIVERED` | NR1–NR2 (+ ALL) | Same as order-scoped | **L1:** tracking snapshot + policy text + **always open ticket** for human | +| `SHIPMENT_STUCK_OR_DELAYED` | SD1–SD3 (+ ALL) | Same as order-scoped | **L1:** last scan + policy + **open ticket** | +| `PAYMENT_PROBLEM` | PP1–PP6 (+ ALL) | Verified identity; **order id** when payment is order-scoped; else intake fields | **L1:** structured intake + **ticket**; no automated money movement | +| `ORDER_CHANGE` | OC1–OC4 (+ ALL) | Same as order-scoped + **requested change** captured | **L1:** **open ticket** + ack only; human decides feasibility | +| `RETURN_EXCHANGE` | RE1–RE6 (+ ALL) | Same as order-scoped + **reason** optional | **L1:** policy in reply + **open ticket** (or portal later) | +| `WARRANTY` | WA1–WA5 (+ ALL) | Verified identity + proof-of-purchase / order | **L1:** intake + **ticket**; human adjudicates | +| `WRONG_ITEM` | WI1–WI6 (+ ALL) | Same as order-scoped + **issue type**; email may attach **photos** | **L1:** ack + policy + **ticket** with structured fields | +| `COMPLAINT` | CP1–CP6 (+ ALL) | **Strict** same as other “fiber” flows | **L1:** empathetic ack + **ticket** + severity; human owns | + +**Removed vague module:** `DELIVERY_PROBLEM` is **removed** as a single umbrella. Use **`NOT_RECEIVED_MARKED_DELIVERED`** and **`SHIPMENT_STUCK_OR_DELAYED`** so intents and playbooks stay **specific**. + +--- + +## 4. Design principles (knowledge modules) + +### 4.1 Modular enablement + +- Each **use-case** = one **sub-module**. Vendor **enables** modules they want. +- In the **product UI**, each enabled module should expose **three areas** aligned with **§2.4**: **(1) Policy & messaging** (vendor questionnaire), **(2) Verification & prerequisites** (read-only copy of platform rules + any future vendor toggles), **(3) L1 & ticketing** (read-only summary or advanced: what automation does vs when tickets open) — exact layout is product design. +- **Type 1** complete: enabled when **required** vendor questions (`shared_field_id`) for that module are filled (where the module has a questionnaire). +- **Transactional / ticket** modules: additionally require **technical readiness** (e.g. order data in DB, ticket pipeline) — product-defined. +- **`CATEGORY_METADETAILS`:** **no** vendor questionnaire — only a **single store-level toggle** (**on** / **off**). **On** = agent may use **tool calls** to read **category metadetails** for **all** synced categories/products; **off** = those tools are not invoked for customer answers. **No per-category** enable/disable in v1. + +### 4.2 Single source of truth (shared answers) + +- Vendor questions map to **`shared_field_id`**. Same id = one value; edit propagates everywhere to avoid contradictions. + +### 4.3 Duplication in the UI + +- Same question may appear in multiple modules for **context**; storage is still keyed by `shared_field_id`. + +### 4.4 `ALL` (global) + +- Baseline questions apply to every store regardless of modules. + +### 4.5 Document boundaries + +- This doc specifies **business rules** and **conceptual field identity** for alignment across modules. **Schema, UI copy, exact OTP TTL**, and chunking strategy are implementation details elsewhere — and **physical schema / API names need not match** the `shared_field_id` or module codes in this file (see **Read this first (integration)** at the top). + +--- + +## 5. Shared field ID reference (cross-module truth) + +*The keys in the first column are **spec labels** for “one conceptual answer, many modules.” In your app they might be DB columns, JSON keys, or CMS field handles with **different** names — that is **expected** when integrating (see **Read this first**).* + +| `shared_field_id` | Typical use | +|-------------------|-------------| +| `sf_brand_display_name` | Brand as customers know it | +| `sf_store_one_liner` | What you sell / for whom | +| `sf_official_languages` | Languages you support in writing | +| `sf_support_channels` | Email, phone, chat, hours | +| `sf_support_first_response_sla` | Expected time to first human/bot reply | +| `sf_tone_and_words_to_avoid` | Voice + banned terms | +| `sf_order_id_help` | How customers find order # / confirmation | +| `sf_first_message_checklist` | What to include when contacting support | +| `sf_policy_link_terms` | Terms of sale URL | +| `sf_policy_link_privacy` | Privacy policy URL | +| `sf_policy_link_returns` | Return policy URL (if separate) | +| `sf_privacy_support_paragraph` | Short: how data is used in support | +| `sf_shipping_regions_served` | Where you ship | +| `sf_shipping_regions_excluded` | Where you do not ship | +| `sf_processing_time_before_ship` | Dispatch / handling time | +| `sf_carriers_and_tracking_policy` | Carriers + when tracking is provided | +| `sf_shipping_methods_and_transit` | Methods + realistic transit ranges | +| `sf_shipping_cost_model` | Free/paid/flat/dynamic | +| `sf_dispatch_cutoff` | Same-day rules if any | +| `sf_address_correction_policy` | Wrong address, failed delivery fees | +| `sf_customs_duties_policy` | International duties disclaimer | +| `sf_delivered_not_received_policy` | “Delivered but I don’t have it” | +| `sf_major_delay_communication` | Strikes, weather, backlog messaging | +| `sf_stuck_shipment_customer_steps` | What customer should do while shipment appears stalled | +| `sf_cancellation_window` | When cancel is allowed | +| `sf_cancellation_how_to_request` | How customer asks to cancel | +| `sf_cancellation_preorder_digital` | Special rules if applicable | +| `sf_order_change_allowed_until` | Until when edits are possible (for KB / human context) | +| `sf_order_change_how_to_request` | How to request change (human path) | +| `sf_order_change_not_possible_next_step` | After cutoff / shipped | +| `sf_return_window` | Days / trigger event | +| `sf_return_condition_requirements` | Tags, unworn, packaging | +| `sf_return_shipping_payer` | Who pays return shipping | +| `sf_exchange_policy` | Swap vs credit vs refund | +| `sf_how_to_start_return` | Portal/email/steps | +| `sf_non_returnable_categories` | Final sale, hygiene, etc. | +| `sf_refund_methods` | Original payment vs store credit | +| `sf_refund_timeline_after_approval` | Time to money back | +| `sf_partial_refund_rules` | Open box, missing parts | +| `sf_store_credit_rules` | Expiry, combinable | +| `sf_payment_methods_accepted` | Cards, wallets, regional limits | +| `sf_promo_discount_rules` | Stacking, exclusions | +| `sf_tax_display_policy` | What’s included at checkout | +| `sf_price_adjustment_policy` | Post-purchase price match if any | +| `sf_failed_payment_guidance` | What customer should retry | +| `sf_wrong_item_report_deadline` | Time limit to report | +| `sf_wrong_item_evidence` | Photos, packing slip, etc. | +| `sf_wrong_item_remedies` | Reship, refund, return-first (human executes) | +| `sf_damage_carrier_vs_warehouse` | How you explain difference | +| `sf_warranty_duration_scope` | Length + what’s covered | +| `sf_warranty_voiders` | Misuse, unauthorized repair | +| `sf_warranty_claim_process` | Proof, repair, replace | +| `sf_sizing_fit_guidance` | Fit notes + size chart link | +| `sf_compatibility_notes` | Devices, voltage, region | +| `sf_materials_care_limitations` | Care, allergens, limitations | +| `sf_back_in_stock_policy` | Waitlist / notify / none | +| `sf_preorder_policy` | Charge timing, ship-by, cancel | +| `sf_discontinued_product_policy` | Alternatives / final sale | +| `sf_physical_locations_hours` | Addresses + hours + holidays | +| `sf_pickup_rules` | Hold time, ID, process | +| `sf_instore_vs_online_inventory` | Same stock or not | +| `sf_human_escalation_when` | Clear “ask a human” path | +| `sf_complaint_acknowledgement_sla` | How fast you acknowledge | +| `sf_complaint_supervisor_escalation` | When a lead reviews | +| `sf_abusive_contact_policy` | Neutral boundary statement | +| `sf_feedback_csat_process` | Survey link / when sent | +| `sf_order_summary_tax_invoice_note` | Invoices, VAT, limitations | +| `sf_double_charge_intake` | What customer should send for duplicate charge claims | + +--- + +## 6. `ALL` — Global / store-wide questions + +| # | Question (vendor-facing) | Why it matters | `shared_field_id` | +|---|-------------------------|----------------|-------------------| +| G1 | What is your **brand or store name** as customers should see it? | Consistent identity. | `sf_brand_display_name` | +| G2 | In **one or two sentences**, what do you sell and who is it for? | Grounds answers. | `sf_store_one_liner` | +| G3 | Which **languages** do you officially support in email/support? | Sets expectations. | `sf_official_languages` | +| G4 | What are your **official support channels** (email, phone, chat) and **hours**? | Human + ticket paths. | `sf_support_channels` | +| G5 | Typical **first response time** for human support? | Trust / SLA. | `sf_support_first_response_sla` | +| G6 | **Tone** and **words or promises** to avoid. | Brand-safe automation. | `sf_tone_and_words_to_avoid` | +| G7 | How customers find **order number** / confirmation. | All order-related flows. | `sf_order_id_help` | +| G8 | What customers should **always include** when contacting support. | Faster resolution. | `sf_first_message_checklist` | +| G9 | Link to **terms of sale**. | Legal grounding. | `sf_policy_link_terms` | +| G10 | Link to **privacy policy**. | Trust. | `sf_policy_link_privacy` | +| G11 | Link to **return / refund policy** if separate. | Canonical policy URL. | `sf_policy_link_returns` | +| G12 | **Short paragraph**: how data is used in support. | Privacy framing. | `sf_privacy_support_paragraph` | + +--- + +## 7. Per-module vendor question sets + +Each subsection follows **§2.4** (three types): + +| Type | Role in this section | +|------|----------------------| +| **Type 1** | **Policy & messaging** — vendor questionnaire rows in the table (`shared_field_id`). Completing required Type 1 fields gates **Knowledge** for that module. | +| **Type 2** | **Verification & prerequisites** — who may run the flow and which **slots** must be filled (**§1–§2**); summarized here per module. | +| **Type 3** | **L1 automation & ticketing** — tool reads/writes, reply shape, and **when tickets open**; humans own ticket resolution. | + +**§3.1** is the **at-a-glance** matrix for all modules; **§7** is the **detailed** breakdown. + +### 7.1 `ORDER_STATUS` — Order status + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| OS1 | **Normal processing time** before shipment? | `sf_processing_time_before_ship` | +| OS2 | **Proactive status updates** (if any)? | *(optional `sf_order_status_proactive_updates`)* | +| OS3 | If **delayed before shipment**, what do you tell the customer? | `sf_major_delay_communication` | +| OS4 | **Ship to** / **do not ship to**? | `sf_shipping_regions_served`, `sf_shipping_regions_excluded` | +| OS5 | **Order # help** + checklist | `sf_order_id_help`, `sf_first_message_checklist` | + +#### Type 2 — Verification & prerequisites + +- **Email:** `From` = **order email**; **multi-order disambiguation** (**§1.1**). +- **Widget:** **email + OTP** + **session TTL** (**§1.2**). +- **Slots:** **`order_id`** (or pick from candidate list). + +#### Type 3 — L1 automation & ticketing + +- **L1:** Read **order / fulfillment** from DB after Type 2 passes; compose reply with **Type 1** messaging. +- **Ticket:** Data missing, anomaly, or **outside playbook** → **create ticket** (no full auto-resolution). + +--- + +### 7.2 `INVENTORY_STOCK` — Inventory / stock + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| IV1–IV5 | *(unchanged — back-in-stock, pre-order, discontinued, in-store vs online, disclaimer)* | `sf_back_in_stock_policy`, `sf_preorder_policy`, `sf_discontinued_product_policy`, `sf_instore_vs_online_inventory`, optional disclaimer | + +#### Type 2 — Verification & prerequisites + +- **Slots:** **`product_name` / SKU** (and disambiguation if many matches). +- **Widget:** May allow **anonymous** stock checks, or require **OTP session** if tied to “my order” — **product decision**. + +#### Type 3 — L1 automation & ticketing + +- **L1:** **Inventory tool** read from DB; blend **Type 1** policy snippets where relevant. +- **Ticket / clarify:** Heavy disambiguation or conflicting data → ask clarifying questions or **ticket**. + +--- + +### 7.3 `FAQ` — General FAQ (includes **refund policy**, not refund status) + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| FQ1–FQ8 | Shipping, returns, **refunds policy summary**, payment, promo, tax, contact, policy links | *(as in prior doc)* | + +#### Type 2 — Verification & prerequisites + +- **None** for **anonymous** FAQ-only path on the widget (**§1.2**). + +#### Type 3 — L1 automation & ticketing + +- **L1:** RAG / structured FAQ from **Type 1** (+ **`ALL`** where useful). +- **Escalation:** Out of scope → suggest **email** or **ticket** per vendor channels (**`sf_support_channels`**). + +--- + +### 7.4 `ORDER_CANCEL` — Order cancellation + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| CX1–CX5 | Window, how to request, next step if impossible, pre-order, refund timing | `sf_cancellation_*`, `sf_refund_*`, `sf_order_change_not_possible_next_step` | + +#### Type 2 — Verification & prerequisites + +- Same **order-scoped** verification as **`ORDER_STATUS`** (**§1**, **§2**). + +#### Type 3 — L1 automation & ticketing + +- **L1:** **Eligibility + cancel** when rules/DB allow; otherwise explain from **Type 1**. +- **Ticket:** Ambiguous, edge case, or cancel not permitted by rules → **open ticket** if policy requires human judgment. + +--- + +### 7.5 `HUMAN_ESCALATION` + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| HE1–HE4 | When to ask human, channels, SLA, checklist | `sf_human_escalation_when`, `sf_support_*`, `sf_first_message_checklist` | + +#### Type 2 — Verification & prerequisites + +- **Minimal** — optional context slots only; no order read required unless combined with another intent. + +#### Type 3 — L1 automation & ticketing + +- **L1:** Ack + routing copy from **Type 1** / **`ALL`**. +- **Ticket:** **Create ticket** when vendor policy or product rules say so. + +--- + +### 7.6 `COMPLAINT` — Ticket-led, strict verification + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| CP1–CP6 | Ack, SLA, supervisor, remedies summary, abuse policy, channels | `sf_complaint_*`, `sf_wrong_item_remedies`, `sf_refund_methods`, `sf_support_channels` | + +#### Type 2 — Verification & prerequisites + +- **Strict** same as other **fiber** flows: **email alignment** or **widget OTP session**; attach **order** when relevant. + +#### Type 3 — L1 automation & ticketing + +- **L1:** Empathetic ack + **Type 1** snippets + severity / structured intake. +- **Ticket:** **Always create ticket**; **human** resolves. + +--- + +### 7.7 `ORDER_SUMMARY` + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| SM1–SM4 | Receipt naming, tax invoice note, checklist, order # help | optional receipt naming, `sf_order_summary_tax_invoice_note`, `sf_first_message_checklist`, `sf_order_id_help` | + +#### Type 2 — Verification & prerequisites + +- Same as **`ORDER_STATUS`** (order-scoped). + +#### Type 3 — L1 automation & ticketing + +- **L1:** Read **line items / totals** from DB; compose recap + **Type 1** (tax/invoice disclaimers). +- **Ticket:** Tax/invoice edge cases per vendor policy → **human**. + +--- + +### 7.8 `REFUND_STATUS` — Transactional (distinct from FAQ refund policy) + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| RF1–RF6 | Methods, timeline, after return received, partial rules, store credit, delay messaging | `sf_refund_*`, optional `sf_refund_after_return_received`, `sf_refund_delay_exception` | + +#### Type 2 — Verification & prerequisites + +- **Verified identity** + **order / return** reference (slots as your schema defines). + +#### Type 3 — L1 automation & ticketing + +- **L1:** Read **refund/return pipeline state** from DB when modeled; answer with **Type 1** timelines where applicable. +- **Ticket:** State not in DB or ambiguous → ack + **ticket** after verification. + +--- + +### 7.9 `SHIPPING_TRACKING` + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| ST1–ST6 | Carriers, methods, cost, cutoff, regions, customs | *(as before)* | + +#### Type 2 — Verification & prerequisites + +- Same as **`ORDER_STATUS`** (**verified order**). + +#### Type 3 — L1 automation & ticketing + +- **L1:** Read **tracking fields** for that order; reply with snapshot + **Type 1** carrier/transit copy. +- **Ticket:** Carrier **claim / dispute** → **open ticket** for human. + +--- + +### 7.10 `WRONG_ITEM` — Ticket-led + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| WI1–WI6 | Deadline, evidence, remedies, damage explanation, return shipping, links | `sf_wrong_item_*`, `sf_return_shipping_payer`, `sf_policy_link_returns`, `sf_how_to_start_return` | + +#### Type 2 — Verification & prerequisites + +- **Order-scoped** verification + **`issue_type`** slot; **email** may include **photo** attachments. + +#### Type 3 — L1 automation & ticketing + +- **L1:** Ack + **Type 1** policy + structured fields for the ticket body. +- **Ticket:** **Create ticket**; human executes reship/refund/return. + +--- + +### 7.11 `RETURN_EXCHANGE` — Ticket-led initiation + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| RE1–RE6 | Window, condition, non-returnable, shipping, exchange, refund timing | `sf_return_*`, `sf_exchange_policy`, `sf_how_to_start_return`, `sf_refund_*` | + +#### Type 2 — Verification & prerequisites + +- **Order-scoped** verification + optional **`reason`** / return intent slot. + +#### Type 3 — L1 automation & ticketing + +- **L1:** Policy in reply from **Type 1** + **open ticket** (or portal link if product adds it). +- **Ticket:** **Human / ops** fulfills RMA. + +--- + +### 7.12 `NOT_RECEIVED_MARKED_DELIVERED` — Ticket-led (replaces vague “delivery problem”) + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| NR1 | **“Delivered” but not received** — customer steps, wait time, claim | `sf_delivered_not_received_policy` | +| NR2 | **Carrier** / claim expectations | `sf_carriers_and_tracking_policy` | + +#### Type 2 — Verification & prerequisites + +- **Order-scoped** verification (same pattern as **`ORDER_STATUS`**). + +#### Type 3 — L1 automation & ticketing + +- **L1:** Last **tracking snapshot** + **Type 1** snippet → then **always open ticket** for investigation. +- **Ticket:** **Human** handles carrier / claim (**§0.1**). + +--- + +### 7.13 `SHIPMENT_STUCK_OR_DELAYED` — Ticket-led + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| SD1 | When shipment seems **stuck** or **far past** expected range, what should customer do first? | `sf_stuck_shipment_customer_steps` | +| SD2 | How you communicate **major delays** | `sf_major_delay_communication` | +| SD3 | **Carriers** reference | `sf_carriers_and_tracking_policy` | + +#### Type 2 — Verification & prerequisites + +- **Order-scoped** verification. + +#### Type 3 — L1 automation & ticketing + +- **L1:** **Last scan dates** + **Type 1** → **open ticket** for human carrier/ops follow-up. + +--- + +### 7.14 `PAYMENT_PROBLEM` — Ticket-led (prerequisites vary) + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| PP1–PP6 | Payment methods, failed payment, **double charge intake**, promo, tax, price adjust | `sf_payment_methods_accepted`, `sf_failed_payment_guidance`, `sf_double_charge_intake`, `sf_promo_discount_rules`, `sf_tax_display_policy`, `sf_price_adjustment_policy` | + +#### Type 2 — Verification & prerequisites + +- **Order-scoped** issues: **verified identity + order id**. +- **Pre-purchase / checkout** failures: may only have **email + narrative** → ticket with available slots. + +#### Type 3 — L1 automation & ticketing + +- **L1:** Structured intake + **ticket**; **no** automated money movement. +- **Ticket:** **Human** + finance tools. + +--- + +### 7.15 `ORDER_CHANGE` — Ticket-led only + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| OC1–OC4 | Until when changes are *sometimes* possible, how to request, impossible case, cancel alternative | `sf_order_change_*`, `sf_cancellation_*` | + +#### Type 2 — Verification & prerequisites + +- **Order-scoped** verification + capture **requested change** (address/items/qty — slots). + +#### Type 3 — L1 automation & ticketing + +- **L1 v1:** **No** auto-mutate order — **ack + open ticket** only. +- **Ticket:** **Human** decides feasible vs offer cancel path from **Type 1**. + +--- + +### 7.16 `WARRANTY` — Ticket-led + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| WA1–WA5 | Scope, voiders, process, timeline optional, escalation | `sf_warranty_*`, `sf_complaint_supervisor_escalation` | + +#### Type 2 — Verification & prerequisites + +- **Verified identity** + **proof-of-purchase / order** reference. + +#### Type 3 — L1 automation & ticketing + +- **L1:** Intake + **Type 1** + **ticket**. +- **Ticket:** **Human** adjudicates. + +--- + +### 7.17 `PRODUCT_FIT` + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| PF1–PF4 | Sizing, compatibility, materials, human edge cases | `sf_sizing_fit_guidance`, `sf_compatibility_notes`, `sf_materials_care_limitations`, `sf_human_escalation_when` | + +#### Type 2 — Verification & prerequisites + +- **Optional** **product** slot (name/SKU) for specificity; no order required for pure fit FAQ. + +#### Type 3 — L1 automation & ticketing + +- **L1:** KB + optional **`CATEGORY_METADETAILS`** tools when enabled (**§7.20**). +- **Ticket / human:** Edge cases per **`sf_human_escalation_when`**. + +--- + +### 7.18 `STORE_LOCAL` + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| SL1–SL4 | Locations, pickup, inventory nuance, local returns | `sf_physical_locations_hours`, `sf_pickup_rules`, `sf_instore_vs_online_inventory`, `sf_how_to_start_return` | + +#### Type 2 — Verification & prerequisites + +- **None** for typical **Knowledge** replies. + +#### Type 3 — L1 automation & ticketing + +- **L1:** **KB-only** from **Type 1**; no order tools required. + +--- + +### 7.19 `FEEDBACK` + +#### Type 1 — Policy & messaging (vendor questionnaire) + +| # | Question | `shared_field_id` | +|---|----------|-------------------| +| FB1–FB3 | CSAT process, negative follow-up optional, tone | `sf_feedback_csat_process`, optional negative follow-up, `sf_tone_and_words_to_avoid` | + +#### Type 2 — Verification & prerequisites + +- Optional link to **prior ticket** / context if product supports it. + +#### Type 3 — L1 automation & ticketing + +- **L1:** CSAT link / one-shot ask per **Type 1**. +- **Human:** Negative follow-up path if configured. + +--- + +### 7.20 `CATEGORY_METADETAILS` — Category metadetails (store-wide, DB via tools) + +This module is **special**: there is **no Type 1 questionnaire** — only a **store-level enable/disable** (see below). Policy copy for *when* to prefer PDP vs human still lives in **`PRODUCT_FIT`** / **`FAQ`**. + +#### Type 1 — Policy & messaging (vendor questionnaire) + +- **None.** **Vendor configuration:** **one toggle only** — **Enabled** or **Disabled** for the store. **No** per-category or per-product granularity in v1. + +#### Type 2 — Verification & prerequisites + +- Vendor **enables** the feature; sync pipeline persists **category metadetails** in DB. +- **Slots:** resolve **product → category** (e.g. name/SKU → category) before tool calls; tools return **fields**, not guesses. + +#### Type 3 — L1 automation & ticketing + +**When enabled** + +- **L1** may invoke **tool calls** to read **category metadetails** (and related synced fields — exact schema in engineering doc). +- Coverage: **all products** in synced categories (e.g. sizing, materials, care, compatibility at **category** level). + +**When disabled** + +- Do **not** call category-metadetails tools for customer-facing answers. +- Fall back to **`PRODUCT_FIT` / FAQ**, PDP links, or escalation. + +**Ticketing** + +- If data missing, disabled, or intent unclear → **ticket** or clarify per **`ALL`** / **`PRODUCT_FIT`**. + +--- + +## 8. L1 agentic implementation checklist (engineering) + +### 8.1 Core services + +- [ ] **Intent router** → module + pillar (K / T / X / catalog-metadata) — use **your** module/enum names if they differ from this doc. +- [ ] **Tool-call layer** (or RPC): explicit functions that **query the database** for each concern — **orders**, **line items**, **fulfillment/tracking**, **inventory**, **refund/return state**, **products**, **categories**, **category metadetails** — using **real table/field names from your schema** (not necessarily any placeholder names in this plan). The LLM/agent must only assert facts returned by tools (plus vendor KB where allowed). +- [ ] **Slot extractor** (per module manifest): `order_id`, `product_name`, `issue_subtype`, etc. +- [ ] **Email adapter:** `from_email` + thread id; enforce **same-email** rule for T/X. +- [ ] **Widget adapter:** session + **OTP** service + TTL (e.g. 1h). +- [ ] **DB readers** (exposed as tools) for order, fulfillment, tracking, inventory, refund state (as available in your schema). +- [ ] **Category metadetails tools:** e.g. resolve `product_id`/`sku` → `category_id` → **fetch metadetails record**; respect vendor **`CATEGORY_METADETAILS` enabled** flag before calling. +- [ ] **Writers** only where allowed (e.g. cancel); **ticket creator** for all **X** modules. + +### 8.2 Manifests (per module) + +- [ ] Map each module to **§2.4**: **Type 1** = which `shared_field_id` keys feed prompts; **Type 2** = `verification_gate` + `required_slots`; **Type 3** = `l1_actions[]`, ticket triggers, tool allowlist. +- [ ] `required_slots`, `verification_gate`, `pillar`, `l1_actions[]`, `ticket_template_id` (for X). +- [ ] **Disambiguation** rule when multiple orders match email. + +### 8.3 Knowledge + +- [ ] Vendor `shared_field_id` storage + sync. +- [ ] RAG/chunking with `module_code` metadata for **K** paths. +- [ ] Store-level flag: **`category_metadetails_enabled`** (boolean) — gates category-metadetails **tool calls** only; no per-category flags in v1. + +### 8.4 Safety + +- [ ] **No existence leak:** wrong email must not confirm others’ orders. +- [ ] **Rate limits** on OTP and lookups. +- [ ] **Guardrails** (existing) before LLM. + +### 8.5 Explicit non-goals (this phase) + +- [ ] No Shopify OAuth login for customers. +- [ ] No magic links. +- [ ] No full automated ticket resolution workflow — humans own tickets. + +--- + +## 9. Optional `shared_field_id` backlog + +- `sf_order_status_proactive_updates` +- `sf_inventory_disclaimer` +- `sf_receipt_naming` +- `sf_refund_after_return_received` +- `sf_refund_delay_exception` +- `sf_negative_feedback_escalation` + +--- + +*End of document.* diff --git a/package-lock.json b/package-lock.json index 4202f91..44d7b7f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10173,7 +10173,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "optional": true, "os": [ diff --git a/prisma/migrations/20260412120000_store_module_policies/migration.sql b/prisma/migrations/20260412120000_store_module_policies/migration.sql new file mode 100644 index 0000000..0fe0329 --- /dev/null +++ b/prisma/migrations/20260412120000_store_module_policies/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Store" ADD COLUMN IF NOT EXISTS "modulePolicies" JSONB; diff --git a/prisma/migrations/20260412140000_store_shared_field_answers/migration.sql b/prisma/migrations/20260412140000_store_shared_field_answers/migration.sql new file mode 100644 index 0000000..77bcf6d --- /dev/null +++ b/prisma/migrations/20260412140000_store_shared_field_answers/migration.sql @@ -0,0 +1 @@ +ALTER TABLE "Store" ADD COLUMN IF NOT EXISTS "sharedFieldAnswers" JSONB; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index cf393b1..a1e8174 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -124,7 +124,11 @@ model Store { categoryMetadetailsEnabled Boolean @default(false) /** Per-module AI enablement, e.g. { "FAQ": true, "ORDER_STATUS": true }. */ aiModules Json? - + /** FAQ-style policy text per L1 module for embeddings, e.g. { "ORDER_STATUS": "..." }. */ + modulePolicies Json? + /** Vendor Type-1 answers keyed by shared_field_id (sf_*); one value per key store-wide (§4.2). */ + sharedFieldAnswers Json? + // Relations connections Connection[] cachedProducts CachedProduct[] diff --git a/scripts/seed-templates.ts b/scripts/seed-templates.ts deleted file mode 100644 index 0b924a5..0000000 --- a/scripts/seed-templates.ts +++ /dev/null @@ -1,291 +0,0 @@ -#!/usr/bin/env tsx -/** - * Seed script for predefined JUNO email templates. - * - * Usage: - * tsx scripts/seed-templates.ts - * - * Behaviour: - * - Finds a target store: - * - If SEED_STORE_ID is set, uses that storeId - * - Otherwise, uses the first active store it can find - * - Inserts a small library of templates for common intents/moods - * - Idempotent: skips creating a template if one with the same - * (storeId, intent, mood, name) already exists. - */ - -import { prisma } from "../src/lib/prisma" -import { logger } from "../src/lib/logger" -import { randomUUID } from "crypto" - -type SeedTemplate = { - name: string - intent: string - mood: string - description: string - body: string -} - -const SEED_TEMPLATES: SeedTemplate[] = [ - // Shipping delay - angry customer - { - name: "Shipping delay – angry customer", - intent: "shipping_delay", - mood: "angry", - description: - "Apology and clear acknowledgement for an angry customer asking where their order is.", - body: `Hi {{customer_name}}, - -I'm really sorry for the frustration here. I can see why you'd be upset, and I appreciate you taking the time to reach out. - -We've reviewed your message and we're looking into the status of your order right away. {{ticket_summary}} - -As soon as we have an updated timeline from our shipping partner, we'll follow up with you by email. If you have any extra details you think might help, you can reply directly to this message. - -Thank you for your patience while we sort this out. - -Best regards, -{{store_name}} Support Team`, - }, - // Shipping delay - confused / neutral customer - { - name: "Shipping delay – confused customer", - intent: "shipping_delay", - mood: "confused", - description: - "Reassuring response for a customer who is unsure where their order is.", - body: `Hi {{customer_name}}, - -Thanks for reaching out about your order – I’m glad you checked in. - -We’ve received your message and we’re reviewing your order details now. {{ticket_summary}} - -We’re confirming the latest tracking information with our shipping partner and will follow up with an update as soon as we have it. In the meantime, if you notice any changes or receive a new tracking email, feel free to reply here so we can keep everything in one place. - -Best regards, -{{store_name}} Support Team`, - }, - // Refund / return request – neutral - { - name: "Refund or return – neutral", - intent: "refund_request", - mood: "neutral", - description: - "Professional acknowledgement of a refund/return request with next steps.", - body: `Hi {{customer_name}}, - -Thank you for contacting us about your refund/return request. - -We’ve reviewed your message and we’re now looking into the order details so we can process this correctly. {{ticket_summary}} - -In some cases we may need a bit more information (for example photos of the item or confirmation of the order number). If that’s needed, we’ll reply back with a short checklist so you know exactly what to send. - -Once everything is confirmed, we’ll process your refund/return and email you a final confirmation. - -Best regards, -{{store_name}} Support Team`, - }, - // Refund / return request – appreciative / happy - { - name: "Refund or return – polite customer", - intent: "refund_request", - mood: "happy", - description: - "Warm, appreciative reply when the customer is polite and reasonable about a refund/return.", - body: `Hi {{customer_name}}, - -Thank you for your thoughtful message – we really appreciate how clearly you explained the situation. - -We’ve started reviewing your request and the details of your order. {{ticket_summary}} - -If we need anything else from you (for example photos or a bit more context), we’ll let you know right away. Otherwise, we’ll process your refund/return and follow up with a confirmation email as soon as it’s completed. - -Thank you again for shopping with {{store_name}}. - -Warm regards, -{{store_name}} Support Team`, - }, - // Product question / general support – neutral - { - name: "Product question – we’re checking", - intent: "question", - mood: "neutral", - description: - "Neutral response letting the customer know their product question is being reviewed.", - body: `Hi {{customer_name}}, - -Thanks for reaching out with your question – we’re glad you asked. - -We’ve received your message and we’re checking the details so we can give you a clear answer. {{ticket_summary}} - -As soon as we’ve confirmed everything with the relevant team, we’ll follow up with a detailed reply. If you have any extra information or screenshots that might help, you can reply to this email and they’ll be added to your ticket. - -Best regards, -{{store_name}} Support Team`, - }, - // Product question – confused customer - { - name: "Product question – confused customer", - intent: "question", - mood: "confused", - description: - "Reassuring answer for a customer who is confused about how something works.", - body: `Hi {{customer_name}}, - -Thank you for reaching out and for explaining what’s been confusing – you’re not alone, and we’re here to help. - -We’ve received your message and we’re reviewing the details so we can walk you through this step by step. {{ticket_summary}} - -Once we’ve double-checked everything, we’ll follow up with a clear explanation and, if helpful, some simple steps you can follow. If you have any screenshots or additional details, feel free to reply to this email so we can see exactly what you’re seeing. - -Warm regards, -{{store_name}} Support Team`, - }, - // Account / billing issue – frustrated customer - { - name: "Account or billing – concerned customer", - intent: "account_billing", - mood: "angry", - description: - "Empathetic response when a customer is upset about a billing or account issue.", - body: `Hi {{customer_name}}, - -I’m really sorry for the worry and frustration this billing issue has caused – I completely understand how important this is. - -We’ve received your message and we’re reviewing your account and recent charges as a priority. {{ticket_summary}} - -If we need any additional details to resolve this, we’ll let you know exactly what we need and why. Otherwise, we’ll follow up as soon as we have a full explanation and next steps for you. - -Thank you for your patience while we get to the bottom of this. - -Best regards, -{{store_name}} Support Team`, - }, - // Account / billing issue – neutral - { - name: "Account or billing – investigation", - intent: "account_billing", - mood: "neutral", - description: - "Calm, neutral response acknowledging an account or billing question and starting an investigation.", - body: `Hi {{customer_name}}, - -Thanks for getting in touch about your account/billing question. - -We’ve received your message and we’re reviewing your account details and recent activity. {{ticket_summary}} - -If we need any additional information from you, we’ll reply with a short list so it’s easy to provide. Once we’ve completed our review, we’ll follow up with a clear explanation of what we found and, if needed, any adjustments we’ll make. - -Best regards, -{{store_name}} Support Team`, - }, - // Thank-you / positive feedback – happy customer - { - name: "Thank-you – happy customer", - intent: "general", - mood: "happy", - description: - "Warm reply to a customer who sends positive feedback or a thank-you message.", - body: `Hi {{customer_name}}, - -Thank you so much for your kind message – it truly made our day here at {{store_name}}. - -We really appreciate you taking the time to share your experience. {{ticket_summary}} - -If there’s ever anything else we can help with, just reply to this email and we’ll be glad to assist. - -Warm regards, -{{store_name}} Support Team`, - }, -] - -async function getTargetStoreId(): Promise { - const explicitId = process.env.SEED_STORE_ID - if (explicitId) { - const store = await prisma.store.findUnique({ - where: { id: explicitId }, - select: { id: true }, - }) - if (!store) { - throw new Error(`SEED_STORE_ID is set, but no store found with id=${explicitId}`) - } - return store.id - } - - const store = await prisma.store.findFirst({ - where: { isActive: true }, - select: { id: true }, - }) - - if (!store) { - throw new Error( - "No active store found. Set SEED_STORE_ID to a valid store id before running this script.", - ) - } - - return store.id -} - -async function seedTemplates() { - try { - logger.info("Starting template seeding...") - - const storeId = await getTargetStoreId() - logger.info(`Seeding templates for storeId=${storeId}`) - - for (const tpl of SEED_TEMPLATES) { - // Check if a template with the same (storeId, intent, mood, name) already exists - const existing = await prisma.$queryRaw< - { id: string }[] - >` - SELECT "id" - FROM "Template" - WHERE "storeId" = ${storeId} - AND "intent" = ${tpl.intent} - AND "mood" = ${tpl.mood} - AND "name" = ${tpl.name} - LIMIT 1 - ` - - if (existing.length > 0) { - logger.info( - `Skipping existing template: "${tpl.name}" (intent=${tpl.intent}, mood=${tpl.mood})`, - ) - continue - } - - const id = randomUUID() - - await prisma.$executeRaw` - INSERT INTO "Template" - ("id", "storeId", "name", "intent", "mood", "description", "body", "variables", "isHtml", "createdAt", "updatedAt") - VALUES - (${id}, ${storeId}, ${tpl.name}, ${tpl.intent}, ${tpl.mood}, ${tpl.description}, ${tpl.body}, NULL, FALSE, NOW(), NOW()) - ` - - logger.info( - `Inserted template: "${tpl.name}" (intent=${tpl.intent}, mood=${tpl.mood})`, - ) - } - - logger.info("Template seeding completed.") - } catch (error) { - logger.error("Template seeding failed:", error) - process.exit(1) - } finally { - await prisma.$disconnect() - } -} - -if (require.main === module) { - seedTemplates() - .then(() => { - logger.info("Seed script finished successfully.") - process.exit(0) - }) - .catch((error) => { - logger.error("Seed script encountered an error:", error) - process.exit(1) - }) -} - diff --git a/src/app/api/juno-engine/templates/[templateId]/route.ts b/src/app/api/juno-engine/templates/[templateId]/route.ts deleted file mode 100644 index e915c93..0000000 --- a/src/app/api/juno-engine/templates/[templateId]/route.ts +++ /dev/null @@ -1,241 +0,0 @@ -import { NextResponse } from "next/server" -import { auth } from "@/auth" -import { isVendorPortalRole } from "@/lib/auth-roles" -import { prisma } from "@/lib/prisma" -import { logger } from "@/lib/logger" -import { junoTemplatePatchSchema } from "@/lib/api-schemas/vendorMutations" -import { validationErrorResponse } from "@/lib/api-schemas/validate" - -type TemplateRow = { - id: string - storeId: string - name: string - intent: string - mood: string - description: string | null - body: string - variables: unknown | null - isHtml: boolean - updatedAt: Date -} - -async function getActiveStoreId(userId: string) { - const user = await prisma.user.findUnique({ - where: { id: userId }, - include: { - stores: { - where: { isActive: true }, - take: 1, - select: { id: true }, - }, - }, - }) - - return user?.stores[0]?.id ?? null -} - -export async function GET( - _req: Request, - { params }: { params: Promise<{ templateId: string }> }, -) { - try { - const session = await auth() - if (!session?.user?.id) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) - } - - if (!isVendorPortalRole(session.user.role)) { - return NextResponse.json({ message: "Forbidden" }, { status: 403 }) - } - - const { templateId } = await params - - const storeId = await getActiveStoreId(session.user.id) - if (!storeId) { - return NextResponse.json( - { message: "No active store found" }, - { status: 400 }, - ) - } - - const rows = await prisma.$queryRaw` - SELECT - "id", - "storeId", - "name", - "intent", - "mood", - "description", - "body", - "variables", - "isHtml", - "updatedAt" - FROM "Template" - WHERE "id" = ${templateId} AND "storeId" = ${storeId} - LIMIT 1 - ` - - const row = rows[0] - - if (!row) { - return NextResponse.json({ message: "Template not found" }, { status: 404 }) - } - - const template = { - ...row, - variables: row.variables ?? undefined, - updatedAt: row.updatedAt.toISOString(), - } - - return NextResponse.json({ template }) - } catch (error) { - logger.error("Failed to load template", error) - return NextResponse.json( - { message: "Failed to load template" }, - { status: 500 }, - ) - } -} - -export async function PUT( - req: Request, - { params }: { params: Promise<{ templateId: string }> }, -) { - try { - const session = await auth() - if (!session?.user?.id) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) - } - - if (!isVendorPortalRole(session.user.role)) { - return NextResponse.json({ message: "Forbidden" }, { status: 403 }) - } - - const { templateId } = await params - - const storeId = await getActiveStoreId(session.user.id) - if (!storeId) { - return NextResponse.json( - { message: "No active store found" }, - { status: 400 }, - ) - } - - const existingRows = await prisma.$queryRaw[]>` - SELECT "id" - FROM "Template" - WHERE "id" = ${templateId} AND "storeId" = ${storeId} - LIMIT 1 - ` - const existing = existingRows[0] - if (!existing) { - return NextResponse.json({ message: "Template not found" }, { status: 404 }) - } - - const raw = await req.json().catch(() => ({})) - const parsed = junoTemplatePatchSchema.safeParse(raw) - if (!parsed.success) { - return validationErrorResponse(parsed.error) - } - const { - name, - intent, - mood, - description, - body: templateBody, - variables, - isHtml, - } = parsed.data - - const rows = await prisma.$queryRaw` - UPDATE "Template" - SET - "name" = COALESCE(${name}, "name"), - "intent" = COALESCE(${intent}, "intent"), - "mood" = COALESCE(${mood}, "mood"), - "description" = COALESCE(${description ?? null}, "description"), - "body" = COALESCE(${templateBody}, "body"), - "variables" = COALESCE(${variables as Record | null}, "variables"), - "isHtml" = COALESCE(${isHtml}, "isHtml"), - "updatedAt" = NOW() - WHERE "id" = ${existing.id} - RETURNING - "id", - "storeId", - "name", - "intent", - "mood", - "description", - "body", - "variables", - "isHtml", - "updatedAt" - ` - - const row = rows[0] - const updated = { - ...row, - variables: row.variables ?? undefined, - updatedAt: row.updatedAt.toISOString(), - } - - return NextResponse.json({ template: updated }) - } catch (error) { - logger.error("Failed to update template", error) - return NextResponse.json( - { message: "Failed to update template" }, - { status: 500 }, - ) - } -} - -export async function DELETE( - _req: Request, - { params }: { params: Promise<{ templateId: string }> }, -) { - try { - const session = await auth() - if (!session?.user?.id) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) - } - - if (!isVendorPortalRole(session.user.role)) { - return NextResponse.json({ message: "Forbidden" }, { status: 403 }) - } - - const { templateId } = await params - - const storeId = await getActiveStoreId(session.user.id) - if (!storeId) { - return NextResponse.json( - { message: "No active store found" }, - { status: 400 }, - ) - } - - const existingRows = await prisma.$queryRaw[]>` - SELECT "id" - FROM "Template" - WHERE "id" = ${templateId} AND "storeId" = ${storeId} - LIMIT 1 - ` - const existing = existingRows[0] - if (!existing) { - return NextResponse.json({ message: "Template not found" }, { status: 404 }) - } - - await prisma.$executeRaw` - DELETE FROM "Template" - WHERE "id" = ${existing.id} - ` - - return NextResponse.json({ success: true }) - } catch (error) { - logger.error("Failed to delete template", error) - return NextResponse.json( - { message: "Failed to delete template" }, - { status: 500 }, - ) - } -} - diff --git a/src/app/api/juno-engine/templates/route.ts b/src/app/api/juno-engine/templates/route.ts deleted file mode 100644 index e422858..0000000 --- a/src/app/api/juno-engine/templates/route.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { NextResponse } from "next/server" -import { auth } from "@/auth" -import { isVendorPortalRole } from "@/lib/auth-roles" -import { prisma } from "@/lib/prisma" -import { logger } from "@/lib/logger" -import { randomUUID } from "crypto" -import { junoTemplateCreateSchema } from "@/lib/api-schemas/vendorMutations" -import { validationErrorResponse } from "@/lib/api-schemas/validate" - -type TemplateRow = { - id: string - storeId: string - name: string - intent: string - mood: string - description: string | null - body: string - variables: unknown | null - isHtml: boolean - updatedAt: Date -} - -export async function GET(req: Request) { - try { - const session = await auth() - if (!session?.user?.id) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) - } - - if (!isVendorPortalRole(session.user.role)) { - return NextResponse.json({ message: "Forbidden" }, { status: 403 }) - } - - const url = new URL(req.url) - const intent = url.searchParams.get("intent") || undefined - const mood = url.searchParams.get("mood") || undefined - - const user = await prisma.user.findUnique({ - where: { id: session.user.id }, - include: { - stores: { - where: { isActive: true }, - take: 1, - select: { id: true }, - }, - }, - }) - - const activeStore = user?.stores[0] - if (!activeStore) { - return NextResponse.json({ templates: [] }) - } - - // Use raw SQL so we don't depend on generated Prisma delegates - const rows = await prisma.$queryRaw` - SELECT - "id", - "storeId", - "name", - "intent", - "mood", - "description", - "body", - "variables", - "isHtml", - "updatedAt" - FROM "Template" - WHERE "storeId" = ${activeStore.id} - ORDER BY "updatedAt" DESC - ` - - const filtered = - intent || mood - ? rows.filter((tpl: TemplateRow) => { - if (intent && tpl.intent !== intent) return false - if (mood && tpl.mood !== mood) return false - return true - }) - : rows - - const templates = filtered.map((tpl: TemplateRow) => ({ - ...tpl, - variables: tpl.variables ?? undefined, - updatedAt: tpl.updatedAt.toISOString(), - })) - - return NextResponse.json({ templates }) - } catch (error) { - logger.error("Failed to load templates", error) - return NextResponse.json( - { message: "Failed to load templates", templates: [] }, - { status: 500 }, - ) - } -} - -export async function POST(req: Request) { - try { - const session = await auth() - if (!session?.user?.id) { - return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) - } - - if (!isVendorPortalRole(session.user.role)) { - return NextResponse.json({ message: "Forbidden" }, { status: 403 }) - } - - const user = await prisma.user.findUnique({ - where: { id: session.user.id }, - include: { - stores: { - where: { isActive: true }, - take: 1, - select: { id: true }, - }, - }, - }) - - const activeStore = user?.stores[0] - if (!activeStore) { - return NextResponse.json( - { message: "No active store found" }, - { status: 400 }, - ) - } - - const raw = await req.json().catch(() => ({})) - const parsed = junoTemplateCreateSchema.safeParse(raw) - if (!parsed.success) { - return validationErrorResponse(parsed.error) - } - const { name, intent, mood, description, body: templateBody, variables, isHtml } = - parsed.data - - const id = randomUUID() - - const rows = await prisma.$queryRaw` - INSERT INTO "Template" - ("id", "storeId", "name", "intent", "mood", "description", "body", "variables", "isHtml", "createdAt", "updatedAt") - VALUES - (${id}, ${activeStore.id}, ${name}, ${intent}, ${mood}, ${description ?? null}, ${templateBody}, ${variables as Record | null}, ${isHtml ?? true}, NOW(), NOW()) - RETURNING - "id", - "storeId", - "name", - "intent", - "mood", - "description", - "body", - "variables", - "isHtml", - "updatedAt" - ` - - const inserted = rows[0] - const template = { - ...inserted, - variables: inserted.variables ?? undefined, - updatedAt: inserted.updatedAt.toISOString(), - } - - return NextResponse.json({ template }, { status: 201 }) - } catch (error) { - logger.error("Failed to create template", error) - return NextResponse.json( - { message: "Failed to create template" }, - { status: 500 }, - ) - } -} - diff --git a/src/app/api/storefront-chat/messages/route.ts b/src/app/api/storefront-chat/messages/route.ts index 169fc3e..13330ea 100644 --- a/src/app/api/storefront-chat/messages/route.ts +++ b/src/app/api/storefront-chat/messages/route.ts @@ -124,8 +124,9 @@ export async function POST(req: Request) { senderType: m.senderType, content: m.content, })), - categoryMetadetailsEnabled: store.categoryMetadetailsEnabled, aiModulesJson: store.aiModules, + sharedFieldAnswersJson: store.sharedFieldAnswers, + categoryMetadetailsEnabled: store.categoryMetadetailsEnabled, widgetVerifiedEmail: sessionOk ? conversation.assertedEmail : null, widgetSessionExpiresAt: sessionOk ? conversation.sessionExpiresAt : null, boundShopifyOrderId: conversation.boundShopifyOrderId, diff --git a/src/app/api/stores/[storeId]/knowledge-template/route.ts b/src/app/api/stores/[storeId]/knowledge-template/route.ts index 1f4a3ae..541204a 100644 --- a/src/app/api/stores/[storeId]/knowledge-template/route.ts +++ b/src/app/api/stores/[storeId]/knowledge-template/route.ts @@ -4,12 +4,11 @@ import { prisma } from "@/lib/prisma" import { logger } from "@/lib/logger" import { knowledgeTemplatePutSchema, - normalizeTemplateAnswers, + mergeTemplateAnswers, parseKnowledgeTemplateJson, - buildKnowledgeTemplateStored, type KnowledgeTemplateStored, } from "@/lib/kb/knowledgeTemplate" -import { replaceStructuredKnowledgeChunks } from "@/lib/kb/structuredTemplateChunks" +import { replaceStructuredKnowledgeChunksForStore } from "@/lib/kb/structuredTemplateChunks" import { deleteDocumentKbForStore, deleteFaqKbForStore, @@ -172,35 +171,10 @@ export async function PUT( } const prev = parseKnowledgeTemplateJson(store.knowledgeTemplate) - const mergedAnswers = { - ...(prev?.legacyAnswers ?? {}), - ...normalizeTemplateAnswers(body.template.answers), - } - const mergedShared: Record = {} - for (const [k, v] of Object.entries(prev?.sharedFields ?? {})) { - if (typeof v === "string" && v.trim()) { - mergedShared[k] = v.trim() - } - } - if (body.template.sharedFields !== undefined) { - for (const [k, v] of Object.entries(body.template.sharedFields)) { - if (typeof v === "string" && v.trim()) { - mergedShared[k] = v.trim() - } else { - delete mergedShared[k] - } - } - } - const misc = - body.template.misc !== undefined - ? body.template.misc.trim() || undefined - : prev?.misc - - const stored: KnowledgeTemplateStored = buildKnowledgeTemplateStored({ - answers: mergedAnswers as Record, - sharedFields: mergedShared as Record, - misc, - }) + const stored: KnowledgeTemplateStored = mergeTemplateAnswers( + prev, + body.template.answers as Record | undefined, + ) const nextMode: KnowledgeBaseMode = store.knowledgeBaseMode === "UNSET" ? "STRUCTURED" : store.knowledgeBaseMode @@ -213,7 +187,7 @@ export async function PUT( }, }) - await replaceStructuredKnowledgeChunks(store.id, stored) + await replaceStructuredKnowledgeChunksForStore(store.id) } const refreshed = await prisma.store.findFirst({ diff --git a/src/app/api/stores/[storeId]/route.ts b/src/app/api/stores/[storeId]/route.ts index 0968ad5..c5932f5 100644 --- a/src/app/api/stores/[storeId]/route.ts +++ b/src/app/api/stores/[storeId]/route.ts @@ -3,6 +3,13 @@ import { auth } from "@/auth" import { prisma } from "@/lib/prisma" import { Prisma } from "@prisma/client" import { storeSelectPatchStorefrontAi } from "@/lib/prisma/storeSelects" +import { modulePoliciesBodySchema, parseModulePoliciesJson } from "@/lib/kb/modulePolicies" +import { + parseSharedFieldAnswersJson, + sharedFieldAnswersBodySchema, + sharedFieldAnswersPayload, +} from "@/lib/kb/sharedFieldAnswers" +import { replaceStructuredKnowledgeChunksForStore } from "@/lib/kb/structuredTemplateChunks" import { getPublishedThemeId, getThemeAsset, @@ -521,6 +528,55 @@ async function syncStorefrontTheme(params: { console.log("[STOREFRONT_CHAT_THEME_SYNC_OK]", { storeId: params.storeId, enabled: params.enabled, themeId }) } +/** + * GET /api/stores/[storeId] + * Read storefront + L1 AI settings for the authenticated vendor. + */ +export async function GET( + _request: Request, + { params }: { params: Promise<{ storeId: string }> }, +) { + try { + const session = await auth() + if (!session?.user?.id) { + return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) + } + + const { storeId } = await params + if (!storeId) { + return NextResponse.json({ message: "Store ID is required" }, { status: 400 }) + } + + const store = await prisma.store.findFirst({ + where: { id: storeId, userId: session.user.id }, + select: { + id: true, + businessName: true, + categoryMetadetailsEnabled: true, + aiModules: true, + modulePolicies: true, + sharedFieldAnswers: true, + }, + }) + + if (!store) { + return NextResponse.json({ message: "Store not found" }, { status: 404 }) + } + + return NextResponse.json({ + id: store.id, + businessName: store.businessName, + categoryMetadetailsEnabled: store.categoryMetadetailsEnabled, + aiModules: store.aiModules, + modulePolicies: parseModulePoliciesJson(store.modulePolicies), + sharedFieldAnswers: parseSharedFieldAnswersJson(store.sharedFieldAnswers), + }) + } catch (error) { + console.error("Error reading store settings:", error) + return NextResponse.json({ message: "Failed to read store" }, { status: 500 }) + } +} + export async function PATCH( request: Request, { params }: { params: Promise<{ storeId: string }> } @@ -582,6 +638,39 @@ export async function PATCH( updatePayload.aiModules = data.aiModules as Prisma.InputJsonValue } + let reembedStructured = false + if (data.modulePolicies !== undefined) { + const parsed = modulePoliciesBodySchema.safeParse(data.modulePolicies) + if (!parsed.success) { + return NextResponse.json( + { message: "Invalid modulePolicies", issues: parsed.error.flatten() }, + { status: 400 }, + ) + } + const normalized = parseModulePoliciesJson(parsed.data) + updatePayload.modulePolicies = + Object.keys(normalized).length === 0 + ? Prisma.JsonNull + : (normalized as Prisma.InputJsonValue) + reembedStructured = true + } + + if (data.sharedFieldAnswers !== undefined) { + const parsed = sharedFieldAnswersBodySchema.safeParse(data.sharedFieldAnswers) + if (!parsed.success) { + return NextResponse.json( + { message: "Invalid sharedFieldAnswers", issues: parsed.error.flatten() }, + { status: 400 }, + ) + } + const normalized = sharedFieldAnswersPayload(parsed.data) + updatePayload.sharedFieldAnswers = + Object.keys(normalized).length === 0 + ? Prisma.JsonNull + : (normalized as Prisma.InputJsonValue) + reembedStructured = true + } + const updatedStore = await prisma.store.update({ where: { id: storeId }, data: updatePayload, @@ -609,6 +698,14 @@ export async function PATCH( } } + if (reembedStructured) { + try { + await replaceStructuredKnowledgeChunksForStore(storeId) + } catch (e) { + console.error("[MODULE_POLICIES_REEMBED_ERR]", e) + } + } + return NextResponse.json({ success: true, storefrontChatEnabled: updatedStore.storefrontChatEnabled, @@ -619,6 +716,8 @@ export async function PATCH( storefrontChatLogoUrl: updatedStore.storefrontChatLogoUrl, categoryMetadetailsEnabled: updatedStore.categoryMetadetailsEnabled, aiModules: updatedStore.aiModules, + modulePolicies: parseModulePoliciesJson(updatedStore.modulePolicies), + sharedFieldAnswers: parseSharedFieldAnswersJson(updatedStore.sharedFieldAnswers), }) } catch (error) { console.error("Error updating store settings:", error) diff --git a/src/app/dashboard/help-support/page.tsx b/src/app/dashboard/help-support/page.tsx index bb22783..36a02f2 100644 --- a/src/app/dashboard/help-support/page.tsx +++ b/src/app/dashboard/help-support/page.tsx @@ -58,7 +58,7 @@ const faqs: FAQItem[] = [ { id: "5", question: "What is JUNO Engine?", - answer: "JUNO Engine is our communication and support infrastructure. It includes ticket management, email flagging, Gmail integration, and email templates to help you manage customer communications efficiently.", + answer: "JUNO Engine is our communication and support infrastructure. It includes ticket views from email activity, email flagging, Gmail integration, and L1 AI replies driven by your knowledge base and module settings (JUNO Engine → Modules).", category: "general" }, { diff --git a/src/app/dashboard/juno-engine/email-templates/page.tsx b/src/app/dashboard/juno-engine/email-templates/page.tsx deleted file mode 100644 index 2da8263..0000000 --- a/src/app/dashboard/juno-engine/email-templates/page.tsx +++ /dev/null @@ -1,612 +0,0 @@ -"use client" - -import { useState, useEffect, useMemo } from "react" -import { createPortal } from "react-dom" -import { motion, AnimatePresence } from "framer-motion" -import { FileText, Search, Plus, Loader2, Edit, Trash2 } from "lucide-react" -import { renderTemplate } from "@/lib/templates/simpleRenderer" -import { useTheme } from "@/components/ThemeProvider" - -type TemplateIntent = - | "general" - | "shipping_delay" - | "refund_request" - | "question" - | "account_billing" - | "off_topic" -type TemplateMood = "neutral" | "angry" | "confused" | "happy" - -interface Template { - id: string - storeId: string - name: string - intent: TemplateIntent | string - mood: TemplateMood | string - description: string | null - body: string - variables?: unknown - isHtml: boolean - updatedAt: string -} - -const INTENT_OPTIONS: { value: TemplateIntent; label: string }[] = [ - { value: "general", label: "General" }, - { value: "shipping_delay", label: "Shipping delay" }, - { value: "refund_request", label: "Refund request" }, - { value: "question", label: "General question" }, - { value: "account_billing", label: "Account / billing" }, - { value: "off_topic", label: "Off-topic (not store-related)" }, -] - -const MOOD_OPTIONS: { value: TemplateMood; label: string }[] = [ - { value: "neutral", label: "Neutral" }, - { value: "angry", label: "Frustrated / Angry" }, - { value: "confused", label: "Confused" }, - { value: "happy", label: "Happy" }, -] - -const exampleContext = { - customer_name: "Alex", - customer_email: "alex@example.com", - store_name: "JUNOVendor", - store_email: "support@junovendor.com", - subject: "Where is my order?", - ticket_summary: "Customer is asking about a shipping delay on order #1234.", - order_summary: - "Order #1001 (2025-03-01) | financial: paid | fulfillment: fulfilled | total: 49.99 USD | items: Tee ×1", -} - -function isAiGeneratedTemplate(variables: unknown): boolean { - if (!variables || typeof variables !== "object") return false - return (variables as Record).source === "ai-generated" -} - -export default function EmailTemplatesPage() { - const [searchTerm, setSearchTerm] = useState("") - const [templates, setTemplates] = useState([]) - const [loading, setLoading] = useState(false) - const [error, setError] = useState(null) - - const [editing, setEditing] = useState