Harden upsell reliability and ticket context completeness.

Add strict linked-product upsell controls, improve retrieval consistency, persist full storefront escalation transcripts, and upgrade ticket APIs/UI for complete thread visibility and safer human handoff workflows.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Saad Mohsin 2026-05-13 23:43:53 +05:00
parent 942970f7ac
commit 6cc4472feb
58 changed files with 8726 additions and 316 deletions

1
.gitignore vendored
View File

@ -58,3 +58,4 @@ next-env.d.ts
docker-data/
/Documents
/machine-learning

View File

@ -24,13 +24,13 @@ Relevant store fields on this path: `businessName`, `email`, `aiModules`, `modul
Single implementation for **widget** and **email**:
1. **Retrieval** — [`getBlendedStorefrontContext`](../src/lib/storefront-chat/retrieval.ts) blends knowledge chunks / FAQs for the prompt context.
1. **Retrieval** — [`getBlendedStorefrontContext`](../src/lib/storefront-chat/retrieval.ts) always retrieves KB/FAQ and only includes products when the active module allows it. In strict mode, product retrieval is constrained to linked upsell candidates.
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**.
6. **Tools** — Orders: [`orderTools`](../src/lib/ai/tools/orderTools.ts). Products: [`productTools`](../src/lib/ai/tools/productTools.ts). Linked upsell allowlist: [`upsellLinks`](../src/lib/ai/tools/upsellLinks.ts) backed by `UpsellProductLink` + optional `juno.upsell_links` metafields.
7. **Reply** — Mostly [`composeReply`](../src/lib/ai/orchestrator/runL1.ts) (Groq) over assembled facts. Strict guardrails prevent recommending products outside linked allowlist and strip generic upsell language when no linked candidates exist.
Vendor UI for `aiModules` and `modulePolicies`: dashboard **JUNO Engine → Modules**; persistence via `GET`/`PATCH` [`/api/stores/[storeId]`](../src/app/api/stores/[storeId]/route.ts).
@ -43,7 +43,7 @@ Module toggles do **not** turn off the knowledge base; they only restrict which
## 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.
1. **Ticket-led (`X`) flows** — Vendor **Tickets** are derived from [`EmailLog`](../src/app/api/juno-engine/tickets/route.ts), not a separate ticket table. Storefront escalations now persist the full storefront transcript and conversation linkage metadata on the `EmailLog` row.
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.
@ -51,3 +51,6 @@ Module toggles do **not** turn off the knowledge base; they only restrict which
- 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).
- Upsell safety flags:
- `STRICT_UPSELL_LINKS` (default `true`) blocks out-of-policy upsells.
- `L1_INCLUDE_PRODUCT_RETRIEVAL` (default `true`) controls whether product snippets can enter retrieval context (still module-gated).

View File

@ -0,0 +1,469 @@
# JUNOHUB Bare-Bones ML/AI Implementation Blueprint (Presentation + Transition Guide)
## 1) Purpose of this document
This document defines a **thorough, ground-up ML/AI system design** for JUNOHUB that can be presented as a full technical implementation plan for Final Year Project evaluation.
### Important context for reviewers
- The currently deployed product uses external LLM APIs for high-quality language output and orchestration speed.
- The architecture in this document is a **dummy implementation track for academic demonstration**, showing how JUNOHUB can be built from first principles (data pipelines, training, retrieval, model serving, decision logic, and governance).
- In this document, “dummy implementation” means: a controlled educational build path with realistic components and interfaces, not a toy script.
- The production code should remain clean and professional; this “dummy” framing is for reporting and viva narrative only.
---
## 2) Executive summary (what to say in presentation)
JUNOHUB is split into two logical layers:
1. **Intelligence core (from scratch)**
All decision-making, routing, verification gating, retrieval, and escalation are performed by custom pipelines and trained ML models.
2. **Language realization layer (optional external LLM)**
The final response can be polished by an LLM, but only after the core has already decided intent, data scope, policy constraints, and allowed actions.
This allows us to claim:
- “The intelligence is ours; the LLM is only a renderer.”
- “Safety and policy compliance are deterministic/model-governed before text generation.”
- “The system can run in a reduced-dependency mode if API access is constrained.”
---
## 3) Current system vs target bare-bones system
## 3.1 Current system (as-is in repo)
- Vendor-configured modules and shared fields
- Policy-aware orchestration
- DB-backed data access (orders/products/category metadata)
- Verification logic (email alignment / OTP session)
- API-first LLM orchestration for replies
- Ticket handoff and module enable/disable controls
## 3.2 Target bare-bones system (to demonstrate)
Replace most LLM reasoning with internal components:
- Intent classification model
- Verification requirement model/policy engine
- Escalation decision model
- Retrieval + reranking subsystem
- Template/slot-based response composer
- Optional LLM only for final rewrite style
---
## 4) High-level architecture
```text
[Data Sources]
Shopify sync / Chat logs / Email logs / Vendor policies / Product metadata
|
v
[Ingestion + Normalization]
ETL jobs -> canonical tables + feature views + training datasets
|
v
[Model Training Layer]
Intent model / Verification gate model / Escalation model / Retrieval encoders
|
v
[Model Serving Layer]
Feature extractor + model inference APIs + confidence calibration
|
v
[Decision Orchestrator]
Rule engine + module toggles + safety constraints + tool/data access planner
|
+--> [Retriever + DB Tool Calls] -> factual context
|
v
[Structured Response Object]
intent, verification status, action plan, facts used, escalation flag
|
+--> [Template renderer] (fully local mode)
|
+--> [LLM rewrite (optional)] (style-only mode)
|
v
[Final customer response + logs + metrics]
```
---
## 5) Data strategy (from scratch)
## 5.1 Data sources
- Historical customer messages (email + widget)
- Synced order events and fulfillment updates
- Product inventory snapshots
- Vendor policy text (shared fields + global knowledge base)
- Ticket outcomes (resolved/escalated/refunded/etc.)
## 5.2 Canonical dataset schema
Create explicit training-ready datasets:
- `message_events`
- `message_id`, `store_id`, `channel`, `timestamp`, `text`, `customer_email`
- `intent_labels`
- `message_id`, `intent_class`, `confidence_human`, `annotator_id`
- `verification_labels`
- `message_id`, `needs_verification` (bool), `reason_code`
- `escalation_labels`
- `message_id`, `resolution_class` (`resolve_now`, `clarify`, `ticket`)
- `retrieval_pairs`
- `query_text`, `relevant_doc_ids`, `hard_negative_doc_ids`
## 5.3 Labeling workflow
1. Seed labels using heuristics/rules.
2. Human annotate and correct.
3. Measure inter-annotator agreement.
4. Freeze versioned dataset snapshots (`v1`, `v2`, `v3`).
---
## 6) ML components
## 6.1 Intent classifier
Goal: map message to module (`FAQ`, `ORDER_STATUS`, `RETURN_EXCHANGE`, etc.).
Suggested models:
- Baseline: TF-IDF + Logistic Regression
- Stronger: Sentence embeddings + LightGBM / linear classifier
- Optional: compact transformer fine-tune (if resources allow)
Outputs:
- `intent_top1`
- `intent_probs`
- `uncertainty_score`
Use confidence threshold:
- High confidence -> proceed
- Low confidence -> clarification question / safe fallback
## 6.2 Verification gate
Goal: predict whether identity verification is required before any data disclosure/action.
Inputs:
- intent class
- lexical risk cues (`my order`, `refund`, `tracking`, `address change`)
- channel (`email/widget`)
- session state (otp verified or not)
Output:
- `verification_required: true/false`
## 6.3 Escalation classifier
Goal: decide if L1 resolves, asks clarification, or opens ticket.
Classes:
- `resolve_now`
- `ask_clarification`
- `escalate_ticket`
Features:
- intent confidence
- data availability (order found or not)
- anomaly flags
- sentiment/risk language
- prior failed attempts
## 6.4 Retrieval subsystem
Pipeline:
1. Document chunking (policy text + structured facts)
2. Embedding generation (local/open model acceptable)
3. Vector search (`pgvector`/FAISS)
4. Optional reranking for top-k quality
Metrics:
- Recall@k
- MRR / nDCG
- factual hit ratio in final responses
---
## 7) Decision Orchestrator (core intelligence)
This is the central runtime service. It consumes model outputs and enforces constraints.
## 7.1 Inputs
- user message
- channel state
- verification state
- module flags from vendor config
- ML outputs (intent, verification requirement, escalation recommendation)
## 7.2 Policies enforced
- No order-specific data without verified identity
- Module-level automations can be enabled/disabled
- Category metadata use must respect toggle
- Ticketing path must include required context slots
## 7.3 Outputs
Produce a **structured response object**:
```json
{
"intent": "ORDER_STATUS",
"needsVerification": true,
"verificationState": "verified",
"action": "fetch_order_status",
"facts": ["order_status=shipped", "tracking=..."],
"escalation": false,
"customerFacingMode": "template_or_llm"
}
```
This object is the contract for rendering.
---
## 8) Response generation strategy
## 8.1 Local template mode (fully bare-bones)
- Slot-filled templates
- deterministic output
- easier auditability
## 8.2 Optional LLM style mode
LLM receives:
- fixed structured object
- approved facts only
- disallowed transformations (cannot change decision class)
So the LLM cannot alter policy decisions; it can only polish language.
---
## 9) System interactions (what talks to what)
## 9.1 Runtime flow
1. API receives message (`email` or `widget`).
2. Feature service creates model features.
3. Intent model predicts module.
4. Verification gate decides if identity check needed.
5. Orchestrator checks session/rules/toggles.
6. Retrieval + DB tools fetch grounded facts.
7. Escalation model decides resolve vs ticket.
8. Structured response object generated.
9. Template render or LLM style rewrite.
10. Persist decision trace + metrics.
## 9.2 Storage and observability
- `inference_logs` table for model outputs
- `orchestrator_traces` for decision explanations
- `response_audit` for facts used and guardrails applied
This is critical for FYP defense and reproducibility.
---
## 10) Transition plan from current system to bare-bones system
This is the most important practical section.
## 10.1 Strategy: strangler pattern (incremental replacement)
Do not replace everything at once. Add a runtime flag:
- `ORCHESTRATION_MODE=api_first | hybrid_ml | ml_first`
### Phase A: API-first (current)
- Existing orchestration remains default.
- Begin collecting labeled training data and traces.
### Phase B: Hybrid-ML
- ML intent runs first.
- Compare ML decision with existing routing (shadow mode).
- Keep API orchestrator as fallback.
- Track disagreement metrics.
### Phase C: ML-first
- ML outputs drive orchestrator decisions.
- LLM used only as optional style layer.
- Keep hard fallback to deterministic templates if LLM unavailable.
## 10.2 Compatibility contract
Use one stable internal contract:
- `StructuredDecisionV1`
- `ToolContextV1`
- `ResponseEnvelopeV1`
Both old and new pipelines must emit/consume this contract, enabling switchability.
## 10.3 Migration safeguards
- Canary by store or by module
- Rollback toggle in config
- Circuit breaker if confidence drops below threshold
- Automatic fallback to safe FAQ response on failure
---
## 11) Repository-level module plan (recommended)
```text
src/lib/ml/
data/
dataset_builder.ts
label_export.ts
features/
text_features.ts
context_features.ts
training/
train_intent.ts
train_verification.ts
train_escalation.ts
inference/
intent_infer.ts
verification_infer.ts
escalation_infer.ts
retrieval/
chunker.ts
embedder.ts
vector_search.ts
reranker.ts
orchestration/
decision_orchestrator.ts
policy_engine.ts
render/
template_renderer.ts
llm_style_renderer.ts
eval/
offline_eval.ts
ablation_eval.ts
```
Supporting runtime flags:
- `ORCHESTRATION_MODE`
- `USE_LLM_STYLE_RENDER`
- `ENABLE_SHADOW_EVAL`
- `MODEL_VERSION_PIN`
---
## 12) Evaluation framework (for report and viva)
## 12.1 Offline metrics
- Intent: Accuracy, Macro-F1, confusion matrix
- Verification: Recall emphasis (avoid unsafe leakage)
- Escalation: Precision/recall per class
- Retrieval: Recall@k, MRR, factual grounding rate
## 12.2 Online/system metrics
- First response latency
- Auto-resolution rate
- Escalation quality score
- Fallback frequency
- Policy violation count (must trend to zero)
## 12.3 Ablation experiments
- Without reranker
- Without escalation model (rules only)
- Templates vs LLM style layer
- Intent model v1 vs v2
---
## 13) Complexity map (to justify engineering depth)
This architecture has meaningful complexity in:
- multi-model runtime orchestration
- confidence calibration and fallback logic
- retrieval quality management
- feature store and dataset versioning
- channel-specific verification semantics
- auditability and policy-safe generation
This complexity is intentional and academically defensible.
---
## 14) Risks and mitigations
- **Data quality risk** -> annotation guidelines + periodic relabeling
- **Model drift** -> monthly evaluation + confidence monitors
- **Unsafe response risk** -> policy engine before rendering
- **Overfitting to one store** -> cross-store split strategy
- **Dependency on external LLM** -> template fallback mode always available
---
## 15) Suggested implementation timeline (12 weeks)
- **Week 1-2:** data contracts, logging, dataset extraction
- **Week 3-4:** labeling + intent baseline
- **Week 5-6:** verification and escalation models
- **Week 7-8:** retrieval + reranking pipeline
- **Week 9:** orchestrator integration in shadow mode
- **Week 10:** hybrid rollout + dashboards
- **Week 11:** ml-first for selected modules/stores
- **Week 12:** evaluation, ablations, thesis packaging
---
## 16) Presentation narrative (short script)
“We started with a production API-first assistant to validate usability. Then we engineered a full bare-bones intelligence core: custom datasets, three trained classifiers, retrieval and reranking, deterministic policy engine, and audited decision traces. The external LLM is now optional and constrained to language polishing. This proves the system’s intelligence and safety are our own, while preserving product viability.”
---
## 17) Deliverables checklist
- [ ] Versioned labeled datasets
- [ ] Training scripts and saved model artifacts
- [ ] Inference services with confidence outputs
- [ ] Orchestrator with policy constraints
- [ ] Retrieval and reranking module
- [ ] Template renderer + optional LLM style renderer
- [ ] Transition flags and fallback controls
- [ ] Evaluation report + ablation charts
- [ ] Architecture and runtime sequence diagrams
---
## 18) Final note for code quality
Even when this track is called a “dummy implementation” for presentation context, all code and interfaces should be written as production-grade components:
- no placeholder naming in runtime code
- typed contracts
- clear module boundaries
- auditable logs
- safe fallbacks
That keeps the repository credible both academically and professionally.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,54 @@
FILENAME=thesis
LATEX=pdflatex
LATEXOPT=--shell-escape
NONSTOP=--interaction=nonstopmode #batchmode
LATEXMK=latexmk
LATEXMKOPT=-pdf
CONTINUOUS=-pvc
NOMENCL=$(shell locate -i nomencl.ist)
all: ${FILENAME}.pdf
${FILENAME}.pdf: ${FILENAME}.tex
$(LATEXMK) $(LATEXMKOPT) $(CONTINUOUS) -pdflatex="$(LATEX) $(LATEXOPT) $(NONSTOP) %O %S" $(FILENAME).tex
#latexmk -quiet -pdfps ${FILENAME} # No pygmentize
#latexmk -r Makefile.rc -quiet -pdfps ${FILENAME} # pygmentize
#latexmk -r Makefile.rc -quiet -pdflatex="pdflatex --shell-escape %O %S" ${FILENAME}
nomencl:
makeindex -s ${NOMENCL} -o ${FILENAME}.nls ${FILENAME}.nlo
clean:
latexmk -c ${FILENAME}
distclean:
latexmk -C ${FILENAME}
rm -rf $(FILENAME).pdfsync
find . -type f -name '*~' -delete
find . -type f -name '*.tmp' -delete
find . -type f -name '*.bbl' -delete
find . -type f -name '*.synctex.gz' -delete
find . -type f -name '*.snm' -delete
find . -type f -name '*.nav' -delete
find . -type f -name '*.aux' -delete
find . -type f -name '*.blg' -delete
find . -type f -name '*.brf' -delete
find . -type f -name '*.end' -delete
#find . -type f -name '*.nls' -delete
find . -type f -name '*.fls' -delete
find . -type f -name '*.log' -delete
#find . -type f -name '*.nlo' -delete
find . -type f -name '*.out' -delete
find . -type f -name '*.texshop' -delete
find . -type f -name '*.fdb_latexmk' -delete
find . -type f -name '*.glo' -delete
find . -type f -name '*.synctex.gz' -delete
find . -type f -name '*converted-to.pdf' -delete
debug:
$(LATEX) $(LATEXOPT) $(FILENAME)
.PHONY: all ${FILENAME}.pdf clean distclean

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

View File

@ -0,0 +1,109 @@
\appendix
\chapter{Appendices}
\section{Appendix A}
\subsection{Use Case Diagram example (Online Shopping System)}
\begin{figure}[H] % Use 'H' to place the figure exactly here
\centering
\includegraphics[width=\textwidth]{images/usecaseexample.png}
\caption{Use Case Diagram for the Online Shopping System}
\label{fig:usecase1}
\end{figure}
\subsection{Detail Use Case Example}
\begin{figure}[H] % Use 'H' to place the figure exactly here
\centering
\includegraphics[width=\textwidth]{images/detailusecase.png}
\caption{Detail Use Case Example}
\label{fig:usecase2}
\end{figure}
\subsection{Event-Response Table for a Highway Intersection}
\begin{figure}[H] % Use 'H' to place the figure exactly here
\centering
\includegraphics[width=\textwidth]{images/eventresponsetable.png}
\caption{Example of Event Response Table}
\label{fig:usecase3}
\end{figure}
\subsection{Story Board Example For Android App}
\begin{figure}[H] % Use 'H' to place the figure exactly here
\centering
\includegraphics[width=\textwidth]{images/storyboard.png}
\caption{Example of Story Board}
\label{fig:usecase3}
\end{figure}
\section{Appendix B}
\subsection{Domain Model Example For Online Shopping}
\begin{figure}[H] % Use 'H' to place the figure exactly here
\centering
\includegraphics[width=\textwidth]{images/domainmodel.png}
\caption{Domain Model Example For Online Shopping Application}
\label{fig:usecase3}
\end{figure}
\section{Appendix C}
\subsection{Box And Line Example For Online Shopping}
\begin{figure}[H] % Use 'H' to place the figure exactly here
\centering
\includegraphics[width=\textwidth]{images/boxandline.png}
\caption{Box and Line Diagram For Online Shopping Application}
\label{fig:usecase3}
\end{figure}
\subsection{Architecture Pattern Example For Online Shopping}
\begin{figure}[H] % Use 'H' to place the figure exactly here
\centering
\includegraphics[width=\textwidth]{images/Architecturepattern.png}
\caption{Architecture Pattern For Online Shopping Application}
\label{fig:usecase3}
\end{figure}
\section{Appendix D}
\subsection{Activity Diagram}
\begin{figure}[H] % Use 'H' to place the figure exactly here
\centering
\includegraphics[width=\textwidth]{images/activityexample.png}
\caption{Activity Diagram For Online Shopping Application}
\label{fig:usecase3}
\end{figure}
\subsection{Class Diagram}
\begin{figure}[H] % Use 'H' to place the figure exactly here
\centering
\includegraphics[width=\textwidth]{images/classexample.png}
\caption{Class Diagram For Online Shopping Application}
\label{fig:usecase3}
\end{figure}
\subsection{Sequence Diagram}
\begin{figure}[H] % Use 'H' to place the figure exactly here
\centering
\includegraphics[width=\textwidth]{images/sequenceexample.png}
\caption{Sequence Diagram For Online Shopping Application}
\label{fig:usecase3}
\end{figure}
\subsection{State Transition Diagram}
\begin{figure}[H] % Use 'H' to place the figure exactly here
\centering
\includegraphics[width=\textwidth]{images/statetransitionexample.png}
\caption{State Transition Diagram For Online Shopping Application}
\label{fig:usecase3}
\end{figure}
\subsection{Data Flow Diagram}
\begin{figure}[H] % Use 'H' to place the figure exactly here
\includegraphics[scale=0.9]{DFD.pdf}
% \includegraphics[width=\textwidth]{DFD.pdf}
\end{figure}
\begin{figure}[H] % Use 'H' to place the figure exactly here
\includegraphics[scale=0.9]{DFD2.pdf}
% \includegraphics[width=\textwidth]{DFD2.pdf}
\end{figure}

View File

@ -0,0 +1,17 @@
% Encoding: UTF-8
@Article{test,
Title = {Stability of slowly diverging flows in shallow water},
Author = {A Kolyshkin and S Nazarovs},
Journal = {Mathematical Modeling and Analysis},
Year = {2007},
Owner = {Francesca},
Timestamp = {2015.12.17}
}
@comment{jabref-meta: selector_author:}
@comment{jabref-meta: selector_journal:}
@comment{jabref-meta: selector_keywords:}
@comment{jabref-meta: selector_publisher:}

View File

@ -0,0 +1,266 @@
%
% FAST-NUCES BS-FYP Thesis Class v0.3
% By Omar Usman Khan <omar.khan@nu.edu.pk
%
% Version History
% 0.1 2018-01-29
% 0.2 2018-02-27 UML support + some house-keeping
% 0.3 2018-05-21 Fixing header stuff and 3/4 author issues
\NeedsTeXFormat{LaTeX2e}
\ProvidesClass{fast-nuces-bs}[2018/05/21 v0.3 FAST-NUCES-BS FYP Thesis Class]
\DeclareOption*{\PassOptionsToClass{\CurrentOption}{book}}
\ProcessOptions\relax
\LoadClass[12pt, a4paper]{book}
\RequirePackage{amssymb, amsmath, amsthm, amstext}
\RequirePackage{tikz}
\RequirePackage{tocloft}
\RequirePackage[sort, numbers, authoryear]{natbib}
\RequirePackage{emptypage}
\RequirePackage{graphics}
\RequirePackage{graphicx}
\RequirePackage{fancyhdr}
\RequirePackage{eucal}
\RequirePackage{soul}
\RequirePackage[english]{babel}
%\RequirePackage[usenames, dvipsnames]{color}
\RequirePackage{etoolbox}
\RequirePackage[perpage]{footmisc}
\RequirePackage{ifthen}
\RequirePackage{ifpdf}
\RequirePackage[utf8]{inputenc}
\RequirePackage{epsfig}
\RequirePackage{setspace}
\RequirePackage[lmargin=3cm,rmargin=3cm,
tmargin=2.5cm,bmargin=2.5cm,
includefoot]{geometry}
\RequirePackage[bookmarks,
bookmarksopen = true,
bookmarksnumbered = true,
breaklinks = true,
linktocpage,
pagebackref = false,
ocgcolorlinks,
colorlinks = true,
linkcolor = blue,
urlcolor = blue,
citecolor = red,
anchorcolor = green,
hyperindex = true,
hyperfigures
]{hyperref}
% DELETE
%\RequirePackage{showframe}
% END DELETE
%Bibliography
\renewcommand{\bibname}{References}
%%%%%%%%%%%%%%%%%% General Settings
\linespread{1.15}
\setlength{\parindent}{0cm}
\setlength{\parskip}{0.5em}
\setcounter{secnumdepth}{3} % For TOC
\setcounter{tocdepth}{3}
\pdfcompresslevel=9
\DeclareGraphicsExtensions{.png, .jpg, .jpeg, .pdf, .eps, .ps}
%%%%%%%%%%%%%%%%%%
\newcommand{\signedby}[1]{%
\vspace{2cm}
\par\noindent\hspace{7.5cm}\hrulefill\par
\par\noindent\hspace{7.5cm}{\large #1}\par
\vspace{0cm}
\par\noindent\hspace{7.5cm}{\large Dated: }\par
}
\newcommand{\longsignedby}[1]{%
\par\hspace{.2cm}\begin{minipage}[t]{.55\columnwidth}
\noindent#1
\end{minipage}
\hfill
\begin{minipage}[t]{.35\columnwidth}
Signature: \hrulefill
\end{minipage}
}
%%%%%%%%%%%%%%%%%%
\newcommand{\yellow}[1]{\hl{#1}}
\newcommand{\comments}[1]{}
%%%%%%%%%%%%%%%%%%
\AfterEndPreamble{\maketitle}
%%%%%%%%%%%%%%%%%%
\pagestyle{fancy}
\renewcommand{\chaptermark}[1]{\markboth{\thechapter. #1 }{}}
\renewcommand{\sectionmark}[1]{\markright{\thesection\ #1}}
\fancyhf{}
\fancyhead[RO]{\footnotesize\rightmark}
\fancyhead[LE]{\footnotesize\leftmark}
\fancyfoot[C]{\thepage}
\fancypagestyle{plain}{
\fancyhead{}
\renewcommand{\headrulewidth}{0pt}
}
\newcommand{\submittedtext}{{A thesis submitted for the degree of}}
% Experiment With This:
\renewcommand{\@makechapterhead}[1]{%
\vspace*{60\p@}%
{\parindent \z@ \raggedright \normalfont
\vspace{5pt}% % add vertical space
\ifnum \c@secnumdepth >\m@ne
\Large\bfseries\scshape \@chapapp\space \thechapter % Chapter number
\par\nobreak
\vskip 20\p@
\fi
\interlinepenalty\@M
\Huge\bfseries \scshape #1\par % chapter title
\nobreak
\vskip 40\p@
}}
% DECLARATIONS
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% The Roll Number of Student
\def\rollnumber#1{\gdef\@rollnumber{#1}}
% The year and term the degree will be officially conferred
\def\degreeyear#1{\gdef\@degreeyear{#1}}
\def\degreemonth#1{\gdef\@degreemonth{#1}}
% The full (unabbreviated) name of the degree
\def\degree#1{\gdef\@degree{#1}}
% The name of your college or department
\def\department#1{\gdef\@department{#1}}
\def\faculty#1{\gdef\@faculty{#1}}
% The name of your University
\def\university#1{\gdef\@university{#1}}
% Defining the crest
\def\crest#1{\gdef\@crest{#1}}
% Supervisor Name
\def\supervisor#1{\gdef\@supervisor{#1}}
\def\cosupervisor#1{\gdef\@cosupervisor{#1}}
\def\sessionduration#1{\gdef\@sessionduration{#1}}
% Author Names
\def\authorone#1#2{
\gdef\@authoronename{#1}
\gdef\@authoroneroll{#2}
}
\def\authortwo#1#2{
\gdef\@authortwoname{#1}
\gdef\@authortworoll{#2}
}
\def\authorthree#1#2{
\gdef\@authorthreename{#1}
\gdef\@authorthreeroll{#2}
}
\def\authorfour#1#2{
\gdef\@authorfourname{#1}
\gdef\@authorfourroll{#2}
}
% Degree Name
\def\degreename#1{\gdef\@degreename{#1}}
% Campus City
\def\campuscity#1{\gdef\@campuscity{#1}}
% Committee Examination Names
\def\internalexaminer#1{\gdef\@internalexaminer{#1}}
\def\externalexaminer#1#2{\gdef\@externalexaminer{#1\par\hspace{0cm}#2}}
% Dean Names
\def\deanname#1{\gdef\@deanname{#1}}
% Director Name
\def\directorname#1{\gdef\@directorname{#1}}
% HoD Name
\def\hodname#1{\gdef\@hodname{#1}}
% GPC Coordinator Name
\def\fypcoordinatorname#1{\gdef\@fypcoordinatorname{#1}}
% Acknowledgement Text
\def\acknowledge#1{\gdef\@acknowledge{#1}}
%%%%%%%%%%%%%%%%%%%%%%%%%
% Some Important Environments
\newenvironment{alwayssingle}{%
\@restonecolfalse\if@twocolumn\@restonecoltrue\onecolumn
\else\newpage\fi}
{\if@restonecol\twocolumn\else\newpage\fi}
\renewcommand{\maketitle}{%
\begin{alwayssingle}
\renewcommand{\footnotesize}{\small}
\renewcommand{\footnoterule}{\relax}
\thispagestyle{empty}
% Title Page
\begin{center}
{ \setstretch{1}
\par{\fontsize{24}{24}\selectfont \textbf{\@title}}\par
\vspace{1.5cm}
\par{\fontsize{14}{14}\selectfont Project Team}\\~\\
{\fontsize{19}{19}\selectfont
\begin{tabular}{lr}
\ifx\@authoronename\undefined ~ \else \@authoronename \fi &
\ifx\@authoroneroll\undefined ~ \else \@authoroneroll \fi \\
\ifx\@authortwoname\undefined ~ \else \@authortwoname \fi &
\ifx\@authortworoll\undefined ~ \else \@authortworoll \fi \\
\ifx\@authorthreename\undefined ~ \else \@authorthreename \fi &
\ifx\@authorthreeroll\undefined ~ \else \@authorthreeroll \fi \\
\ifx\@authorfourname\undefined ~ \else \@authorfourname \fi &
\ifx\@authorfourroll\undefined ~ \else \@authorfourroll \fi
\end{tabular}
}\par
\vspace{0.25cm}
\par{\fontsize{16}{16}\selectfont Session
}{\fontsize{16}{16}\selectfont\@sessionduration}\par
\vspace{0.75cm}
\ifx\@cosupervisor\undefined
\vspace{2.5cm}
\par{\fontsize{14}{14}\selectfont Supervised by}\\~\\
{\fontsize{19}{19}\selectfont \@supervisor}\par
\else
\vspace{0.5cm}
\par{\fontsize{14}{14}\selectfont Supervised by}\\~\\
{\fontsize{19}{19}\selectfont \@supervisor}\par
\vspace{0.5cm}
\par{\fontsize{14}{14}\selectfont Co-Supervised by}\\~\\
{\fontsize{19}{19}\selectfont \@cosupervisor}\par
\fi
\vspace{.5cm}
%\par{\fontsize{21}{21}\selectfont \textbf{Bachelors of \@degreename}}\par
%\vspace{1cm}
%\par{\fontsize{16}{16}\selectfont A thesis submitted in partial fulfillment\\of the
% requirements for the degree of \emph{Bachelors of \@degreename}\\at
% the \emph{National University of Computer and Emerging
% Sciences}}\par
%\vspace{1cm}
\begin{figure}[h]\centering
\includegraphics[width=.30\columnwidth]{ThesisFigs/FASTLogo}
\end{figure}
\vspace{0.5cm}
\par{\fontsize{17}{17}\selectfont \textbf{\@department}}\par
\vspace{0.5cm}
\par{\fontsize{17}{17}\selectfont \textbf{National University of
Computer and Emerging Sciences\\\@campuscity, Pakistan}}\par
\vspace{0.5cm}
\par{\fontsize{17}{17}\selectfont \textbf{\@degreemonth, \@degreeyear}}\par
}
\end{center}
\null\vfill
\clearpage
\tableofcontents
\clearpage
\listoffigures
\clearpage
\listoftables
\pagenumbering{arabic}
\setcounter{page}{0}
\end{alwayssingle}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

View File

@ -0,0 +1,183 @@
\chapter{Introduction}
\label{sec:introduction}
JUNO is a role-based commerce operations platform developed for Shopify-centered dropshipping businesses. The implemented system combines three operational surfaces: a vendor portal, a supplier portal, and an admin observability area, with AI-assisted communication and ticketing workflows connected to store data.
In many small and mid-size dropshipping operations, communication is scattered across inboxes, chat threads, and spreadsheets. This creates delays in supplier coordination, missing context in customer responses, stale product/price information, and poor traceability of decisions. JUNO addresses this by enforcing structured relationships between vendors and suppliers, maintaining synchronized commerce data, and adding AI features where repetitive communication causes the highest operational load.
Unlike a standalone chatbot or a basic ticket dashboard, JUNO is implemented as a workflow system: users, stores, supplier profiles, connections, products, sync requests, offers, notifications, and AI-assisted replies are all linked through a single data model. This allows the platform to support both business operations (inventory/offers/sync status) and support operations (storefront chat, Gmail handling, ticket escalation) in one environment.
\section{Project Context and Actors}
The system defines and enforces three core roles.
\begin{table}[H]
\centering
\caption{Core Roles in JUNO}
\begin{tabular}{|c|p{5cm}|p{5cm}|}
\hline
\textbf{Actor} & \textbf{Description} & \textbf{Main Responsibilities in JUNO} \\ \hline
Vendor & Shopify merchant/store owner using JUNO as the operational center. & Onboard store credentials, monitor products and orders, connect with suppliers, request product sync, use AI support tools, and handle customer-side workflows. \\ \hline
Supplier & Product provider connected to one or more vendors. & Manage supplier catalog, accept/reject product sync requests, create/update vendor-specific offers, and respond to vendor coordination needs. \\ \hline
Admin & Platform operations and governance role. & Observe system health, events, connections, product sync trends, order-level signals, and email activity (observability-focused behavior). \\ \hline
\end{tabular}
\label{tab:core_roles}
\end{table}
\section{Existing Solutions}
A review of currently used approaches shows that existing tools usually solve one part of the workflow but not the complete vendor-supplier-support loop.
\begin{table}[H]
\centering
\caption{Comparison of Existing Solutions}
\begin{tabular}{|c|p{5cm}|p{5cm}|}
\hline
\textbf{System Name} & \textbf{System Overview} & \textbf{System Limitations Compared to JUNO} \\ \hline
Generic Helpdesk Platforms & Centralize incoming support conversations and ticket assignments. & Limited direct linkage to vendor-supplier product workflows, weak per-store commerce context, and no native supplier collaboration model. \\ \hline
Spreadsheet + Messaging Operations & Teams coordinate pricing, stock, and issue resolution in sheets plus chat apps. & No reliable source of truth, high risk of outdated values, poor auditability, and heavy manual follow-up. \\ \hline
Single-Function Shopify Apps & Apps offer one feature such as product sync, notifications, or chatbot widgets. & Fragmented architecture; weak cross-feature continuity between product offers, ticket escalation, and AI-driven communication. \\ \hline
\end{tabular}
\label{tab:system_comparison}
\end{table}
\section{Problem Statement}
Shopify-based dropshipping operations depend on fast and accurate communication between customers, vendors, and suppliers, but these interactions are often spread across disconnected tools. Customer conversations may happen in email or storefront chat, vendor actions in internal dashboards, and supplier coordination in informal message threads. This separation causes context loss, delayed updates, and inconsistent decision-making.
The project identifies five core problems. First, \textbf{communication fragmentation} increases response time and handoff errors. Second, \textbf{repetitive support workload} (order status, shipping, returns, cancellations) consumes team capacity and reduces consistency under high volume. Third, \textbf{weak vendor-supplier alignment} in product synchronization and offer updates leads to outdated price/quantity data and margin risk. Fourth, \textbf{limited prioritization and risk handling} causes critical conversations to be treated like routine ones. Fifth, \textbf{insufficient role-aware visibility} makes it harder to monitor operational health and improve processes proactively.
Together, these issues reduce operational efficiency and customer satisfaction. JUNO addresses them through a unified, role-based platform that combines structured collaboration, commerce data workflows, AI-assisted communication, and monitoring, while keeping human oversight for sensitive cases.
\section{Scope}
The scope of this project defines what will be included and excluded, helping to set clear expectations for the reader. This section outlines the boundaries and limitations of the development project.
\subsection{In-Scope Features (Implemented)}
\begin{itemize}
\item Multi-role authentication and role-gated workspaces for vendor, supplier, and admin usage.
\item Vendor store onboarding with Shopify credentials and webhook registration.
\item Supplier connection lifecycle and invitation/approval style relationship management.
\item Product collaboration workflows:
\begin{itemize}
\item vendor product sync requests,
\item supplier-side sync decisions,
\item supplier product offers with vendor-specific values.
\end{itemize}
\item Commerce cache handling for products, orders, and customers from Shopify webhooks.
\item Notification system for operational events (including sync and offer changes).
\item AI-powered support layer including:
\begin{itemize}
\item storefront chat message handling,
\item knowledge base retrieval with vector search,
\item Gmail webhook-based auto-reply pipeline,
\item ticket generation/escalation paths.
\end{itemize}
\item Admin observability dashboards for system-level monitoring.
\end{itemize}
\subsection{Out-of-Scope or Current Constraints}
\begin{itemize}
\item Full social-channel unification (all messaging channels are not fully production-integrated).
\item Complete enterprise BI/reporting suite beyond current operational dashboards.
\item High-throughput asynchronous ingestion for all knowledge workflows (some processing remains request-time).
\end{itemize}
\section{Modules}
The implemented architecture can be described through five major modules. The modules are logically organized to reflect the overall system workflow, ensuring that each module description is comprehensive but concise.
\subsection{Identity, Access, and Workspace Module}
This module handles user identity, role separation, and active-store context required by other modules.
\begin{enumerate}
\item Role-based access for vendor, supplier, and admin flows.
\item Store-scoped operations for vendor actions.
\item Foundation for role-aware API guards and layout routing.
\end{enumerate}
\subsection{Vendor-Supplier Collaboration Module}
This is the operational core that formalizes business relationships and product-level collaboration.
\begin{enumerate}
\item Vendor-supplier connection and invitation lifecycle.
\item Product sync request workflow (\texttt{PENDING}, \texttt{ACCEPTED}, \texttt{REJECTED}).
\item Supplier product offers with per-vendor pricing/quantity control.
\item Change visibility for vendors through notifications and new-product style views.
\end{enumerate}
\subsection{Commerce Sync and Operational Data Module}
This module keeps JUNO aligned with Shopify events and provides normalized internal data for workflows and AI features.
\begin{enumerate}
\item Shopify webhook verification and event consumption.
\item Cached product/order/customer updates for store context.
\item Sync metadata tracking and health-oriented operational signals.
\item Trigger points for inventory and notification logic.
\end{enumerate}
\subsection{AI Helpdesk and Automation Module (JUNOCHAT / Juno Engine)}
This module provides AI-assisted communication across storefront and email channels using configurable providers and retrieval workflows.
\begin{enumerate}
\item Storefront conversation handling with AI response generation.
\item Knowledge base indexing and query-time semantic retrieval using embeddings.
\item Gmail inbound processing with classification, safety checks, and auto-reply behavior.
\item Ticket and escalation workflow integration for unresolved or sensitive cases.
\item Supplier-contact automation from ticket/order context in selected flows.
\end{enumerate}
\subsection{Admin Observability and Governance Module}
This module offers cross-system visibility for operations monitoring and diagnosis.
\begin{enumerate}
\item Overview of stores, connections, sync activity, orders, and email events.
\item Event and trend visibility for platform operations.
\item Read-oriented governance layer for monitoring and auditing.
\end{enumerate}
\section{Work Division}
The following work-division model reflects responsibilities typically needed to deliver the currently implemented system.
\begin{table}[H]
\centering
\caption{Work Division}
\resizebox{\textwidth}{!}{%
\begin{tabular}{|p{0.25\textwidth}|p{0.75\textwidth}|}
\hline
\textbf{Team Member} & \textbf{Responsibilities} \\ \hline
Syed Saad Mohsin (Frontend + UX + QA) & Build vendor/supplier/admin interfaces, maintain role-based UX flows, and execute UI/functional testing across major user journeys. \\ \hline
Talat Faheem (Backend + Database) & Implement APIs, business workflows, webhook handlers, and notifications; manage Prisma schema, relations, migrations, and data consistency. \\ \hline
Muhammad Usman (AI + DevOps + Integration) & Implement AI orchestration/retrieval/safety flows, maintain provider and prompt behavior, and manage deployment, environment, and CI/CD operations. \\ \hline
\end{tabular}%
}
\label{tab:work_division}
\end{table}
\section{User Class Table}
The following table summarizes the primary user classes that interact with JUNO and how they use the platform.
\begin{table}[H]
\centering
\caption{User Classes}
\resizebox{\textwidth}{!}{%
\begin{tabular}{|p{0.15\textwidth}|p{0.40\textwidth}|p{0.45\textwidth}|}
\hline
\textbf{User Class} & \textbf{Description} & \textbf{Key Interactions with System} \\ \hline
Vendor & Shopify store owner or operator using JUNO for day-to-day dropshipping operations. & Onboarding store, viewing products/orders/customers, requesting product sync, coordinating with suppliers, handling AI-assisted support workflows. \\ \hline
Supplier & Product fulfillment partner connected to one or more vendors. & Managing supplier catalog, responding to sync requests, sending/updating product offers, and coordinating order-related communication with vendors. \\ \hline
Admin & Internal operations/governance user with platform-wide visibility. & Monitoring stores, events, sync activity, and email/system health through observability-focused admin dashboards. \\ \hline
End Customer (External) & Customer purchasing from a vendor's Shopify store (indirect user). & Interacts through storefront chat or email channels that are processed by JUNO's support automation and ticketing workflows. \\ \hline
\end{tabular}%
}
\label{tab:user_classes}
\end{table}

View File

@ -0,0 +1,48 @@
\chapter{Project Requirements}
This section outlines the necessary requirements for the successful completion of the project. Project requirements can be divided into two main categories: functional requirements, which describe the system’s core operations, and non-functional requirements, which specify performance, security, and usability standards.
\section{Use-case/Event Response Table/Storyboarding}
This section describes how users will interact with the system through various scenarios, often referred to as use cases or event responses. A use case represents a specific interaction between the user and the system, detailing the steps involved and the expected system responses. Each use case typically includes key components such as a unique identifier, a description of the interaction, the user or system actor involved, preconditions, the main flow of events, and postconditions. Additionally, storyboarding can be used as a visual aid to depict the sequence of actions, illustrating how the user progresses through the system step by step. Storyboards are helpful for capturing the user experience and ensuring that the design aligns with the intended functionality. By outlining these interactions, the development team can gain a clearer understanding of user needs and system behavior, contributing to a more user-centered design. \\
Depending on the project, use cases, storyboarding or Event-response table can be chosen as appropriate methodologies to illustrate user interactions. Examples of all the approaches are provided in Appendix A.
\section{Functional Requirements}
This section describes the functional requirements of the system expressed in the natural language style. This section is typically organized by feature as a system feature name and specific functional requirements associated with this feature. It is just one possible way to arrange them. Other organizational options include arranging functional requirements by use case, process flow, mode of operation, user class, stimulus, and response depend on what kind of technique has been used to understand functional requirements. Hierarchical combinations of these elements are also possible, such as use cases within user classes.
\subsection{Module 1}
Following are the requirements for module 1:
\begin{enumerate}
\item FR1
\item FR2
\end{enumerate}
\subsection{Module 2}
Following are the requirements for module 2:
\begin{enumerate}
\item FR1
\item FR2
\end{enumerate}
\section{Non-Functional Requirements}
This section specifies nonfunctional requirements. These quality requirements should be specific, quantitative, and verifiable. The following are some examples of documenting guidelines.
\subsection{Reliability}
Requirements about how often the software fails. The measurement is often expressed in MTBF (mean time between failures). The definition of a failure must be clear. Also, don't confuse reliability with availability which is quite a different kind of requirement. Be sure to specify the consequences of software failure, how to protect from failure, a strategy for error detection, and a strategy for correction.
\subsection{Usability}
Usability requirements deal with ease of learning, ease of use, error avoidance and recovery, the efficiency of interactions, and accessibility. The usability requirements specified here will help the user interface designer create the optimum user experience.
Example:\\
USE-1: The COS shall allow a user to retrieve the previous meal ordered with a single interaction.
\subsection{Performance}
State specific performance requirements for various system operations. If different functional requirements or features have different performance requirements, it’s appropriate to specify those performance goals right with the corresponding functional requirements, rather than collecting them in this section.
Example:\\
PER-1: 95% of webpages generated by the COS shall download completely within 4 seconds from the time the user requests the page over a 20 Mbps or faster Internet connection.
\subsection{Security }
One or more requirements about protection of your system and its data. The measurement can be expressed in a variety of ways (effort, skill level, time, ...) to break into the system. Do not discuss solutions (e.g. passwords) in a requirements document.

View File

@ -0,0 +1,63 @@
\chapter{System Overview}
Give a general description of the functionality, context, and design of your project.
Provide any background information if necessary.
\section{Architectural Design}
Develop a modular program structure and explain the relationships between the modules to achieve the complete functionality of the system. This is a high-level overview of how the system’s modules collaborate with each other to achieve the desired functionality.
Don’t go into too much detail about the individual subsystems. The main purpose is to gain a general understanding of how and why the system was decomposed, and how the individual parts work together.
Provide a diagram showing the major subsystems and their connections.
\begin{itemize}
\item In the initial design stage, create a Box and Line Diagram for a simpler representation of the systems.
\item After finalizing the architecture style/pattern diagram (MVC, Client-Server, Layered, Multi-tiered), create a detailed mapping of modules/components to each part of the architecture.
\end{itemize}
To view examples of box and line diagrams and architecture styles, see Appendix C.
\section{Design Models}
Create design models as are applicable to your system. Provide detailed descriptions with each of the models that you add. Also ensure visibility of all diagrams.
\textbf{Design Models for Object Oriented Development Approach}
The applicable models for the project using object oriented development approach may include:
• Activity Diagram\\
• Class Diagram\\
• Class-level Sequence Diagram\\
• State Transition Diagram (for the projects which include event handling and backend processes)\\
\textbf{Design Models for Procedural Approach}
The applicable models for the project using procedural approach may include:
• Activity Diagram\\
• Data Flow Diagram (data flow diagram should be extended to 2-3 levels. It should clearly list all processes, their sources/sinks and data stores.)\\
• System-level Sequence Diagram\\
• State Transition Diagram (for the projects which include event handling and backend processes)\\
Examples of above diagrams are given in Appendix D.
\section{Data Design}
Explain how the information domain of your system is transformed into data structures. Describe how the major data or system entities are stored, processed, and organized.
List any databases or data storage items.
\section{Domain Model}
The domain model is a conceptual representation of the various entities, their attributes, and the relationships between them within the system. It serves as a bridge between the requirements and the design of the software, providing a clear understanding of the business domain and how it interacts with the system being developed.
A domain model typically includes key components such as:
\begin{itemize}
\item \textbf{Entities}: These are the primary objects within the domain that hold data and represent real-world concepts. Each entity is defined by its attributes and behaviors.
\item \textbf{Attributes}: Characteristics or properties of the entities that define their state. For example, a "Customer" entity might have attributes such as \texttt{customerID}, \texttt{name}, \texttt{email}, and \texttt{address}.
\item \textbf{Relationships}: These illustrate how different entities interact with each other. Relationships can be one-to-one, one-to-many, or many-to-many, depending on how entities are associated.
\item \textbf{Associations}: Connections between entities that help clarify their interdependencies. For instance, a "Product" may be associated with a "Category," indicating the type of product it belongs to.
\end{itemize}
By constructing a domain model, developers can better understand the system's requirements, facilitate communication among stakeholders, and guide the subsequent design and implementation phases. It also aids in identifying potential issues early in the development process, ensuring a more robust architecture.
An example of a domain model for an online shopping system is provided in Appendix B.

View File

@ -0,0 +1,53 @@
\chapter{Implementation and Testing}
Give a general description of the functionality, context, and design of your project.
Provide any background information if necessary.
\section{Algorithm Design}
Mention the algorithm(s) used in your project to get the work done with regards to major modules. Provide a pseudocode explanation regarding the functioning of the core features. Be sure to use the correct syntax and semantics for algorithm representations. Following are few examples of algorithms/pseudocode. \\
Example:
\begin{figure}[H] % Use 'H' to place the figure exactly here
\centering
\includegraphics[width=\textwidth]{images/algoexample.png}
\caption{Example Of Algorithm Design}
\label{fig:usecase3}
\end{figure}
\section{External APIs/SDKs}
Describe the third-party APIs/SDKs used in the project implementation in the following table. Few examples of APIs are provided in the table.
% Please add the following required packages to your document preamble:
% \usepackage{graphicx}
\begin{table}[!h]
\resizebox{\textwidth}{!}{%
\begin{tabular}{|l|l|l|l|}
\hline
API and version & Description & Purpose of usage & API endpoint/function/class used \\ \hline
Stripe (version 2020-08-27) & Credit Card payment integration & Sandbox used orders payment & stripe.paymentMethods.create \\ \hline
Cloudinary & Image and Video management & Uploading Product Images & https://api.cloudinary.com/v1 \\ \hline
\end{tabular}%
}
\end{table}
\section{Testing Details}
Once the system has been successfully developed, testing has to be performed to ensure that the system working as intended. This is also to check that the system meets the requirements stated earlier. Besides that, system testing will help in finding the errors that may be hidden from the user. The testing must be completed before it is deployed for use.
\subsection{Unit Testing}
Unit testing is a critical phase in the software development process that involves testing individual components or modules of the system in isolation to ensure that each part functions correctly. The primary goal of unit testing is to validate that each unit of the software performs as designed and meets its specifications. Each unit test is designed to test a specific function or method independently from other components, helping to identify issues directly related to the functionality being tested. Unit tests are often automated, allowing for quick execution and repeatability, which enables frequent running, especially during integration or regression testing, to catch bugs early in the development lifecycle. A well-structured unit testing suite aims to achieve high test coverage, meaning that a significant portion of the codebase is tested, including positive test cases as well as edge cases and error conditions. Furthermore, unit testing provides immediate feedback to developers regarding the quality and reliability of their code, building confidence that changes made during development do not introduce new defects. Additionally, unit tests serve as a form of documentation for the codebase, illustrating how individual units are expected to behave, which makes it easier for new developers to understand the system. Overall, unit testing is an essential practice that contributes to software quality, reliability, and maintainability, ultimately reducing development costs and enhancing the end-user experience.
Following is the example of Unit testing:
\begin{figure}[H] % Use 'H' to place the figure exactly here
\centering
\includegraphics[width=\textwidth]{images/unittesting.png}
\caption{Example for Unit Testing}
\label{fig:usecase3}
\end{figure}

View File

@ -0,0 +1,9 @@
\chapter{Conclusions and Future Work}
\section{Conclusion}
\section{Future Work}

View File

@ -0,0 +1,13 @@
\babel@toc {english}{}\relax
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }

View File

@ -0,0 +1,13 @@
\babel@toc {english}{}\relax
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }
\addvspace {10\p@ }

View File

@ -0,0 +1,66 @@
\documentclass{fast-nuces-bs}
%\documentclass{article}
\usepackage{blindtext}
\usepackage{mathptmx}% Times Roman font
\usepackage[T1]{fontenc}
\usepackage{lipsum}
\usepackage{sectsty}
\usepackage{tikz-uml}
\usepackage{titlesec}
\usepackage{graphicx}
\usepackage{float}
% Information about the Thesis
% -----------------------------------------------------------------------------
% (All are compulsory, do not delete any line other than author Information)
% For e.g., usually there are 3 students in a group, so leave authorone,
% authortwo, and authorthree, and delete authorfour.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
\department{Department of Computer Science}
\faculty{Computer Science}
\degreeyear{2022}
\degreemonth{June}
\degreename{Computer Science}
\campuscity{Islamabad}
\authorone{Syed Saad Mohsin}{22I-1601}
\authortwo{Muhammad Usman}{22I-1689}
\authorthree{Talat Faheem}{22I-1735}
\supervisor{Mr.Arslan Aslam}
\sessionduration{2022-2026}
%\cosupervisor{Mr./ Ms./ Dr. Supervisor Name}
\deanname{Dr. Dean Computing}
\directorname{Dr. Director of the Campus}
\hodname{Dr. Head of Computer Science Department}
\fypcoordinatorname{Name of FYP Coordinator}
\title{JUNO}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Former document starts below this
\begin{document}
%\begin{acknowledgements}
% Your acknowledgments here
%\end{acknowledgements}
%\begin{abstract}
%Briefly state the (1) research problem, (2) methodology, (3) key results, and (4) conclusion. Generally, abstracts are between 150 and 200 words–roughly 10-15 sentences.
%\end{abstract}
\include{sections/chapter1}
\include{sections/chapter2}
\include{sections/chapter3}
\include{sections/chapter4}
\include{sections/conclusions}
%\appendix
%\include{appendix1/appendix1}
\bibliographystyle{plain}
\bibliography{bib}
\addcontentsline{toc}{chapter}{References}
\input{appendix}
\end{document}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,77 @@
# Conclusions and Future Work
## Conclusion
JUNO was developed to solve a practical operations gap in Shopify-centered dropshipping workflows: fragmented communication, weak vendor-supplier alignment, and repetitive support overhead. The implemented system demonstrates that these concerns can be addressed effectively through one role-based platform that combines commerce coordination and AI-assisted support.
Across the completed chapters, the project establishes four core outcomes:
- **Operational unification:** Vendor, Supplier, and Admin workflows are modeled in one system instead of disconnected tools.
- **Collaboration formalization:** Product sync and offer workflows are represented as explicit stateful records, improving traceability and accountability.
- **Support automation with safeguards:** Storefront and Gmail automation reduce repetitive workload while preserving human escalation for sensitive cases.
- **Observability and governance:** Admin-facing event and sync health views provide a practical operational monitoring layer.
From an engineering perspective, the implementation aligns design intent with actual behavior:
- role-aware authorization boundaries are enforced on critical APIs,
- webhook and integration paths are guarded and auditable,
- data structures support both transactional integrity and AI context requirements,
- testing includes deterministic unit coverage and scenario-style automation validation.
The project therefore meets its central objective: delivering a functional, extensible platform that improves dropshipping operations by connecting business collaboration, support intelligence, and operational visibility in a single workflow-oriented architecture.
## Project Limitations
Although the current implementation is production-oriented in many areas, the following limitations remain:
- Channel coverage is not fully unified beyond current storefront and Gmail-centered automation flows.
- AI behavior still depends on provider quality, prompt strategy, and retrieval context freshness.
- Enterprise-grade analytics/reporting depth is limited compared to dedicated BI platforms.
- Some high-throughput ingestion and heavy workflow paths can benefit from more asynchronous processing and queue-based decoupling.
These constraints are expected for the current project scope and do not undermine the core operational value delivered.
## Future Work
Future work is organized into technical, operational, and product expansion tracks.
### 1) Multi-Channel Communication Expansion
- Extend support automation beyond current channels to include WhatsApp, Instagram/Messenger, and additional commerce messaging surfaces.
- Maintain a unified conversation identity model across channels to preserve context continuity.
- Add channel-specific policy templates while keeping one escalation governance flow.
### 2) Advanced AI and Decision Intelligence
- Improve intent routing with richer module-level confidence calibration and fallback policies.
- Add adaptive response quality controls using historical resolution feedback.
- Introduce explainable automation signals for vendor operators (why auto-reply vs escalation occurred).
- Expand structured knowledge ingestion and freshness checks to reduce stale retrieval responses.
### 3) Collaboration and Supply Intelligence
- Add proactive supplier recommendation mechanisms based on order trends, fulfillment outcomes, and historical offer quality.
- Support deeper negotiation and version tracking for vendor-specific offers.
- Introduce margin-risk and stock-risk indicators to improve vendor decision support before product commitments.
### 4) Reliability and Scalability Enhancements
- Move intensive integration/AI paths to explicit queue-worker architecture for stronger throughput guarantees.
- Expand idempotency and replay protections for all external event ingestion paths.
- Improve resilience testing with fault-injection scenarios for provider outages and delayed external responses.
### 5) Observability, Governance, and Analytics
- Add role-specific operational KPIs with configurable alert thresholds.
- Expand audit trail depth for cross-module incident reconstruction.
- Build a richer analytics layer for trend forecasting, SLA tracking, and workflow optimization.
### 6) Security and Compliance Hardening
- Strengthen secret-management posture and rotation workflows for external integrations.
- Expand policy controls for data retention and access auditing.
- Introduce compliance-oriented controls for organizations with stricter governance requirements.
## Final Closing Statement
JUNO provides a strong foundation for intelligent commerce operations in dropshipping environments. Its current implementation proves the viability of combining role-based collaboration, synchronized commerce context, and guarded AI automation in one coherent system. With the outlined future work, JUNO can evolve from an operational platform into a broader decision-support and governance ecosystem for modern multi-actor commerce businesses.

View File

@ -0,0 +1,319 @@
# Section 2: Project Requirements
This section defines the project requirements for JUNO. It translates the project context from Section 1 into concrete user interactions, functional requirements, and measurable non-functional requirements for the implemented scope.
## 2.1 Use-case / Event-Response / Storyboarding
JUNO supports multi-role interactions between Vendor, Supplier, Admin, and End Customer (external actor through storefront chat and email channels). The following use cases capture the most critical operational and support flows.
### 2.1.1 Role-Centric Use-Case Interaction Diagram
```mermaid
flowchart LR
Vendor[Vendor] -->|"Requests sync / reviews offers"| Platform[JUNOPlatform]
Supplier[Supplier] -->|"Accepts sync / publishes offers"| Platform
Customer[EndCustomer] -->|"Chat or email inquiry"| Platform
Admin[Admin] -->|"Monitors events and health"| Platform
Platform -->|"Notifications, AI replies, escalations"| Vendor
Platform -->|"Connection and sync updates"| Supplier
Platform -->|"AI-assisted response or ticket routing"| Customer
Platform -->|"Observability signals and alerts"| Admin
```
### 2.1.2 Core Use Cases
#### UC-01: Vendor Requests Product Sync from Supplier
- **Primary Actor:** Vendor
- **Supporting Actor:** Supplier
- **Preconditions:** Vendor account and store are onboarded; vendor-supplier connection exists; target supplier product is discoverable.
- **Trigger:** Vendor selects supplier product and initiates sync request.
- **Main Flow:**
1. Vendor opens supplier product details.
2. Vendor submits sync request to supplier.
3. System stores request with pending status.
4. Supplier receives notification and reviews request.
5. Supplier accepts request.
6. System updates status and exposes synced product for vendor operations.
- **Alternate/Error Flows:**
- Supplier rejects request; system records rejection reason and notifies vendor.
- Request times out without action; system marks stale status and keeps vendor informed.
- **Postconditions:** Request status is finalized as accepted or rejected; both roles can audit the decision trail.
#### UC-02: Supplier Publishes Vendor-Specific Offer
- **Primary Actor:** Supplier
- **Supporting Actor:** Vendor
- **Preconditions:** Active vendor-supplier relationship exists; relevant product has synced or is eligible for offer mapping.
- **Trigger:** Supplier creates or updates an offer for a specific vendor.
- **Main Flow:**
1. Supplier selects vendor and product.
2. Supplier enters vendor-specific price, quantity, and terms.
3. System validates offer constraints and saves versioned offer.
4. Vendor receives notification and views updated offer details.
- **Alternate/Error Flows:**
- Invalid pricing/quantity values are rejected with corrective guidance.
- Offer conflicts with existing state; system requests explicit overwrite confirmation.
- **Postconditions:** Vendor-visible offer is updated and available for decision-making.
#### UC-03: Customer Inquiry is Auto-Handled with Escalation Safety
- **Primary Actor:** End Customer (external)
- **Supporting Actors:** Vendor support operator, AI helpdesk pipeline
- **Preconditions:** Chat/email channel is connected; retrieval content and policy checks are available.
- **Trigger:** Customer sends a message about order status, return, shipping, or cancellation.
- **Main Flow:**
1. System receives inbound message.
2. Message is classified and normalized.
3. Knowledge retrieval and contextual checks run.
4. AI drafts or sends response for low-risk scenarios.
5. If confidence/risk thresholds fail, system creates or updates escalation ticket.
6. Vendor support operator handles escalated case.
- **Alternate/Error Flows:**
- Missing context leads to a clarification question instead of direct resolution.
- Safety or policy violation triggers immediate human-only handling.
- **Postconditions:** Customer receives a timely response, with risky cases routed to human review.
#### UC-04: Admin Monitors Operational Health
- **Primary Actor:** Admin
- **Preconditions:** Admin access is active; observability data is available.
- **Trigger:** Admin opens monitoring dashboard.
- **Main Flow:**
1. Admin reviews stores, sync trends, and communication events.
2. Admin inspects anomalies (e.g., failed sync spikes, unresolved escalations).
3. Admin drills into event streams for diagnosis and governance.
- **Postconditions:** Admin captures actionable insights and flags issues for operational follow-up.
### 2.1.3 Event-Response Table (Operationally Critical Events)
| Event ID | Event / Stimulus | Source Actor / System | System Response | Expected Outcome |
| --- | --- | --- | --- | --- |
| EVT-01 | Vendor submits product sync request | Vendor | Create pending sync record, notify supplier, log event | Supplier receives actionable request |
| EVT-02 | Supplier accepts sync | Supplier | Mark request accepted, update vendor-facing availability, notify vendor | Product collaboration continues with latest status |
| EVT-03 | Supplier rejects sync | Supplier | Mark request rejected, attach optional reason, notify vendor | Vendor gets clear rejection context |
| EVT-04 | New offer created/updated | Supplier | Validate and persist offer, emit notification/event | Vendor sees latest offer terms |
| EVT-05 | Customer sends support message | End Customer | Classify message, run retrieval, draft/send AI reply or escalate | Fast first response with safer routing |
| EVT-06 | Confidence below threshold | AI Pipeline | Create escalation ticket, attach context and evidence | Human support takes over high-risk case |
| EVT-07 | Webhook sync failure detected | Commerce sync subsystem | Record failure, retry strategy, notify operational channel | Reliability protections trigger early |
| EVT-08 | Admin reviews anomaly | Admin | Provide filtered event trail and affected entities | Faster diagnosis and governance |
### 2.1.4 Storyboarding References (Visual Context)
The following existing assets are reused as storyboard-style visual references where they support readability:
- Generic storyboard reference:
![Storyboard Reference](../../F25_054_D_JUNO_FINAL/images/storyboard.png)
- Generic UI reference (to illustrate interaction framing):
![UI Reference](../../F25_054_D_JUNO_FINAL/images/uiexample.png)
These images are examples, while the requirement definitions in this section remain JUNO-specific.
### 2.1.5 Support Escalation Process Flow
```mermaid
flowchart TD
inbound[InboundCustomerMessage] --> classify[ClassifyIntentAndRisk]
classify --> retrieve[RetrieveContextFromKnowledgeAndOrderData]
retrieve --> confidence{ConfidenceAndSafetyPass}
confidence -->|Yes| autoReply[GenerateAndSendAIReply]
confidence -->|No| escalate[CreateOrUpdateEscalationTicket]
autoReply --> closeLoop[LogResolutionAndMetrics]
escalate --> humanReview[VendorSupportHumanReview]
humanReview --> closeLoop
```
### 2.1.6 Detailed Event and State Behavior
This subsection captures explicit state behavior reflected in the current implementation so requirements remain testable against real system behavior.
#### Product Sync State Transitions
```mermaid
stateDiagram-v2
[*] --> PENDING
PENDING --> ACCEPTED: SupplierAcceptsRequest
PENDING --> REJECTED: SupplierRejectsRequest
REJECTED --> PENDING: VendorResubmitsSameProduct
PENDING --> [*]: VendorCancelsRequest
ACCEPTED --> [*]: DeleteByAuthorizedOwner
REJECTED --> [*]: DeleteByAuthorizedOwner
```
**Behavior notes:**
- Sync request uniqueness is scoped to `storeId + supplierId + shopifyProductId`.
- A previously rejected sync can be reopened as `PENDING` instead of creating a duplicate row.
- Suppliers can decide pending requests, but cannot delete a pending request (they must reject).
- Accepted sync records preserve supplier-available quantity snapshot in product data when inventory is derivable.
#### Storefront Support Escalation Trigger Behavior
- Escalation ticket creation is triggered when AI output indicates explicit escalation intent (tool/module signal or escalation phrase).
- Escalation artifacts are persisted through `EmailLog` with trigger classification for vendor follow-up threads.
- Existing escalation thread keys are reused/updated to avoid duplicate ticket spam for the same storefront issue.
## 2.2 Functional Requirements
Functional requirements are organized by implemented modules from Section 1, with unique IDs and testable statements.
### 2.2.1 Module A: Identity, Access, and Workspace
| Requirement ID | Functional Requirement |
| --- | --- |
| FR-IAM-01 | The system shall authenticate users and bind each account to an approved role (`Vendor`, `Supplier`, or `Admin`). |
| FR-IAM-02 | The system shall enforce role-based access controls on routes, APIs, and workspace features. |
| FR-IAM-03 | The system shall maintain active store context for vendor operations and reject store-scoped actions without valid context. |
| FR-IAM-04 | The system shall prevent unauthorized cross-role data visibility by default. |
| FR-IAM-05 | The system shall record authentication and authorization decision events for auditability. |
### 2.2.2 Module B: Vendor-Supplier Collaboration
| Requirement ID | Functional Requirement |
| --- | --- |
| FR-COLLAB-01 | The system shall allow vendors to create supplier connection/invitation requests and track relationship status. |
| FR-COLLAB-02 | The system shall allow vendors to submit product sync requests to connected suppliers. |
| FR-COLLAB-03 | The system shall allow suppliers to accept or reject sync requests with persisted status transitions. |
| FR-COLLAB-04 | The system shall allow suppliers to create and update vendor-specific offers (price, quantity, and metadata). |
| FR-COLLAB-05 | The system shall notify affected vendors when sync status or offer details change. |
| FR-COLLAB-06 | The system shall preserve a chronological history of collaboration events for traceability. |
### 2.2.3 Module C: Commerce Sync and Operational Data
| Requirement ID | Functional Requirement |
| --- | --- |
| FR-SYNC-01 | The system shall verify and process inbound commerce webhooks for supported entity updates. |
| FR-SYNC-02 | The system shall update internal cached records for products, orders, and customers after successful webhook handling. |
| FR-SYNC-03 | The system shall maintain sync metadata (status, timestamps, and failure indicators) for diagnostics. |
| FR-SYNC-04 | The system shall emit operational notifications/events for significant sync outcomes (success, failure, retry). |
| FR-SYNC-05 | The system shall expose synchronized data to dependent modules including collaboration and AI helpdesk workflows. |
### 2.2.4 Module D: AI Helpdesk and Automation (JUNOCHAT / Juno Engine)
| Requirement ID | Functional Requirement |
| --- | --- |
| FR-AI-01 | The system shall ingest storefront chat and connected email messages into a unified support pipeline. |
| FR-AI-02 | The system shall classify incoming support intent (e.g., order status, return, cancellation, shipping). |
| FR-AI-03 | The system shall perform retrieval-augmented context lookup before generating automated responses. |
| FR-AI-04 | The system shall apply confidence and safety checks before sending AI-generated responses. |
| FR-AI-05 | The system shall automatically escalate unresolved or high-risk conversations into tickets with context attachments. |
| FR-AI-06 | The system shall allow human operators to review, override, and resolve escalated tickets. |
| FR-AI-07 | The system shall log AI decision outcomes (auto-handled vs escalated) for monitoring and tuning. |
### 2.2.5 Module E: Admin Observability and Governance
| Requirement ID | Functional Requirement |
| --- | --- |
| FR-OBS-01 | The system shall provide admins with a consolidated observability view of stores, sync activity, and communication events. |
| FR-OBS-02 | The system shall expose trend-level indicators for operational anomalies (e.g., failure spikes, escalation backlog). |
| FR-OBS-03 | The system shall support admin drill-down into event streams and entity-specific timelines. |
| FR-OBS-04 | The system shall restrict admin operations to governance/monitoring behavior within approved permissions. |
| FR-OBS-05 | The system shall retain key observability records for a configurable audit period. |
### 2.2.6 Functional Requirement Traceability (Code-Aligned)
The table below links major requirements to implementation surfaces so Section 2 can be validated against concrete APIs and persisted entities.
| Requirement Group | Primary API/Service Surface | Key Persisted Entities | Validation Focus |
| --- | --- | --- | --- |
| Identity and RBAC (`FR-IAM-*`) | Auth routes and role guards on protected APIs | `User`, `Session`, `Account`, MFA fields | Unauthorized roles are rejected; role-scoped access is enforced consistently |
| Vendor-Supplier Connection and Sync (`FR-COLLAB-*`) | `/api/products/sync`, `/api/products/sync/[syncId]`, supplier offers APIs | `Connection`, `ProductSync`, `SupplierProductOffer`, `Notification` | Connection precondition, unique sync keys, status transitions, vendor notifications |
| Commerce Sync and Cache (`FR-SYNC-*`) | `/api/webhooks/shopify` plus cache update services | `CachedProduct`, `CachedOrder`, `CachedCustomer`, `SyncMetadata` | HMAC verification, topic handling, metadata freshness, fail-safe logging |
| AI Helpdesk and Ticketing (`FR-AI-*`) | `/api/storefront-chat/messages`, `/api/webhooks/gmail`, ticket APIs | `StorefrontChatConversation`, `StorefrontChatMessage`, `EmailLog`, `FlaggedEmail`, `GmailInboundAutoReply` | Classification path, escalation path, abuse gating, duplicate prevention, reply safety |
| Admin Observability (`FR-OBS-*`) | `/api/admin/events`, `/api/admin/sync-health`, `/api/admin/emails` | `SyncMetadata`, `Connection`, `ProductSync`, email-derived operational records | Multi-source event aggregation, role restriction, event filtering and trend visibility |
### 2.2.7 Detailed Functional Acceptance Criteria
#### A) Identity, Access, and Workspace
- **FR-IAM-06:** The system shall treat `ADMIN` as a vendor-portal-capable role for vendor dashboard/API access where business oversight is required.
- **FR-IAM-07:** The system shall deny supplier-only APIs to non-supplier sessions even when users are authenticated.
- **FR-IAM-08:** The system shall require an active vendor store before allowing store-scoped operations such as sync requests and AI ticket listing.
#### B) Vendor-Supplier Collaboration
- **FR-COLLAB-07:** The system shall reject sync creation if no `CONNECTED` relationship exists between the vendor store and supplier profile.
- **FR-COLLAB-08:** The system shall prevent duplicate offers for the same `productId + storeId` pair.
- **FR-COLLAB-09:** The system shall notify vendor users when supplier offers are created or sync decisions change.
#### C) Commerce Sync and Operational Data
- **FR-SYNC-06:** The system shall verify Shopify webhook signatures before processing any topic payload.
- **FR-SYNC-07:** The system shall normalize incoming shop domain variants before store resolution.
- **FR-SYNC-08:** The system shall update per-resource sync health indicators after each relevant webhook event.
#### D) AI Helpdesk and Automation
- **FR-AI-08:** The system shall enforce public storefront message rate limits at both endpoint and conversation level.
- **FR-AI-09:** The system shall only process Gmail auto-reply workflows for vendor-role integrations.
- **FR-AI-10:** The system shall suppress auto-replies for abuse-flagged inbound emails and persist flagged records for review.
- **FR-AI-11:** The system shall enforce idempotency/lease protection to prevent duplicate auto-replies for the same inbound Gmail message.
#### E) Admin Observability and Governance
- **FR-OBS-06:** The system shall aggregate events from connection lifecycle, product sync lifecycle, and sync health into a unified admin event feed.
- **FR-OBS-07:** The system shall support event-type filtering and bounded response limits for admin event queries.
## 2.3 Non-Functional Requirements
Non-functional requirements define quality attributes that the system must satisfy. They are expressed with measurable and verifiable expectations.
### 2.3.1 Reliability
| Requirement ID | Non-Functional Requirement |
| --- | --- |
| NFR-REL-01 | The platform shall target monthly service availability of at least 99.0% for core vendor/supplier dashboards, excluding planned maintenance windows. |
| NFR-REL-02 | For transient integration failures, the system shall retry critical webhook processing with bounded retry logic and failure logging. |
| NFR-REL-03 | The system shall preserve message/ticket event history such that operational incidents remain traceable after recovery. |
| NFR-REL-04 | On subsystem degradation, the platform shall fail safely by escalating to human handling instead of silently dropping customer requests. |
| NFR-REL-05 | Webhook endpoints shall favor controlled, observable failure behavior (logging + bounded retries/idempotency controls) over duplicate side effects. |
### 2.3.2 Usability
| Requirement ID | Non-Functional Requirement |
| --- | --- |
| NFR-USA-01 | New role-based users shall be able to complete their first core task (e.g., submit sync request or review ticket) with minimal onboarding friction. |
| NFR-USA-02 | The interface shall keep role-specific navigation consistent across sessions to reduce cognitive switching cost. |
| NFR-USA-03 | Critical workflow actions (accept/reject, escalate/resolve) shall provide immediate and unambiguous visual feedback. |
| NFR-USA-04 | Error states shall provide actionable guidance and recovery paths instead of generic failure messages. |
| NFR-USA-05 | Role-scoped views (Vendor, Supplier, Admin) shall expose only task-relevant actions to reduce operator error during high-throughput operations. |
### 2.3.3 Performance
| Requirement ID | Non-Functional Requirement |
| --- | --- |
| NFR-PERF-01 | Under normal expected load, at least 95% of interactive dashboard/API requests shall complete in 3 seconds or less. |
| NFR-PERF-02 | Support message classification and first-pass response decisioning shall complete fast enough to sustain near real-time conversational behavior for common queries. |
| NFR-PERF-03 | Notification propagation for sync/offer state changes shall be near-immediate for operational coordination scenarios. |
| NFR-PERF-04 | Observability screens shall support filtered trend exploration without causing workflow-blocking delays for admins. |
| NFR-PERF-05 | Public storefront messaging shall remain protected by request throttling controls to preserve responsiveness during burst traffic. |
### 2.3.4 Security
| Requirement ID | Non-Functional Requirement |
| --- | --- |
| NFR-SEC-01 | All protected operations shall require authenticated sessions and role-appropriate authorization checks. |
| NFR-SEC-02 | Sensitive integration credentials and tokens shall not be exposed in client-side responses or logs. |
| NFR-SEC-03 | The platform shall enforce tenant/store boundary controls to prevent unauthorized data access across business accounts. |
| NFR-SEC-04 | Security-relevant events (authentication failures, permission denials, critical workflow misuse) shall be logged for audit and incident analysis. |
| NFR-SEC-05 | Third-party webhook payloads shall be accepted only after signature verification and store ownership resolution checks. |
| NFR-SEC-06 | Automated Gmail responses shall be restricted by role and guarded against abuse-triggered auto-response behavior. |
### 2.3.5 Quantitative Verification Checklist
The following checklist is used to convert quality expectations into verifiable test outcomes.
| Verification Area | Target / Expectation | Suggested Evidence |
| --- | --- | --- |
| Availability | Core portal uptime target >= 99.0% monthly (excluding planned maintenance) | Uptime monitoring report and incident log |
| Dashboard/API latency | 95th percentile interactive request <= 3 seconds under expected load | API metrics and synthetic checks |
| Storefront anti-abuse resilience | Burst traffic constrained by endpoint and per-conversation limits | Load test logs with throttling responses |
| Webhook integrity | Invalid signature requests rejected; valid signatures processed | Security test logs for signed/unsigned payloads |
| Auto-reply safety | Flagged abusive inbound messages do not receive AI auto-replies | Flagged email records and reply audit log |
| Data isolation | No unauthorized cross-tenant/store data exposure in role tests | Authorization test suite results |
## 2.4 Requirement Coverage Notes
- Functional and non-functional requirements are intentionally implementation-aware, but they are documented at behavior level rather than code-level constraints.
- Documentation language may generalize internal details to keep the section readable and presentable.
- Any future feature extension (e.g., additional channels or deeper analytics) should append new requirement IDs rather than overloading existing ones.

View File

@ -0,0 +1,348 @@
# Section 3: System Overview
This section describes how JUNO is structured and why the system is decomposed into its current modules. It focuses on architecture, design models, data design, and the domain model, with implementation-aligned detail suitable for technical documentation and evaluation.
## 3.1 Architectural Design
JUNO is implemented as a role-aware, multi-module platform on top of Next.js App Router. The system combines:
- role-scoped application surfaces (Vendor, Supplier, Admin),
- workflow APIs for collaboration and commerce synchronization,
- AI orchestration for storefront/email support,
- operational observability for governance.
The architecture favors a layered, service-oriented style inside one deployable application boundary, rather than fragmented microservices. This keeps coordination cost low while preserving module boundaries in code and data models.
### 3.1.1 High-Level Box and Line View
```mermaid
flowchart LR
vendorUI[VendorPortalUI] --> appApi[ApplicationAPILayer]
supplierUI[SupplierPortalUI] --> appApi
adminUI[AdminObservabilityUI] --> appApi
storefrontWidget[StorefrontWidget] --> appApi
gmailWebhook[GmailWebhook] --> appApi
shopifyWebhook[ShopifyWebhook] --> appApi
appApi --> domainServices[DomainServiceLayer]
domainServices --> aiLayer[AIOrchestrationLayer]
domainServices --> integrations[IntegrationAdapters]
domainServices --> dataLayer[DataAccessLayer]
integrations --> shopifyApi[ShopifyAPIs]
integrations --> gmailApi[GmailAPIs]
aiLayer --> llmProviders[LLMProvidersOpenAIGroq]
dataLayer --> postgres[(PostgreSQLPrisma)]
```
### 3.1.2 Layered Architecture Mapping
```mermaid
flowchart TD
subgraph presentation [Presentation Layer]
p1[Vendor Dashboard]
p2[Supplier Portal]
p3[Admin Pages]
p4[Storefront Chat Widget]
end
subgraph application [Application Layer]
a1[Route Handlers /api/*]
a2[Auth Role Guards]
a3[Validation Schemas]
end
subgraph domain [Domain and Workflow Layer]
d1[Collaboration Workflows]
d2[Commerce Sync Workflows]
d3[AI Helpdesk Workflows]
d4[Notification and Ticket Logic]
end
subgraph infrastructure [Infrastructure Layer]
i1[Prisma Data Access]
i2[Webhook Verification]
i3[Provider Clients]
i4[Encryption and Rate Limits]
end
subgraph external [External Systems]
e1[Shopify]
e2[Gmail]
e3[OpenAI and Groq]
end
presentation --> application
application --> domain
domain --> infrastructure
infrastructure --> external
infrastructure --> db[(PostgreSQL)]
```
### 3.1.3 Major Module Responsibilities
| Module | Primary Responsibilities | Core Artifacts |
| --- | --- | --- |
| Identity and Access | Authentication, role checks, portal/session alignment, MFA-aware flows | Auth handlers, role guards, user/session models |
| Vendor-Supplier Collaboration | Connection lifecycle, product sync requests, supplier offers, event notifications | `Connection`, `ProductSync`, `SupplierProductOffer`, notifications |
| Commerce Synchronization | Shopify webhook verification, cache updates, sync metadata, low-stock signals | `CachedProduct`, `CachedOrder`, `CachedCustomer`, `SyncMetadata` |
| AI Helpdesk and Automation | Storefront AI responses, Gmail ingestion/auto-reply, escalation and ticket routing | Storefront chat flows, email processor, flagged email and ticket logs |
| Admin Observability | Cross-module event aggregation and health monitoring | Admin event/sync APIs and monitoring pages |
### 3.1.4 Architecture Design Rationale
- **Single deployable with clear module boundaries:** easier release management while preserving separation of concerns.
- **Data-centric workflow design:** collaboration and support are linked through shared store-scoped entities.
- **Role-gated API access:** business-critical operations are protected by actor-specific authorization behavior.
- **Fail-safe automation posture:** AI and webhook pipelines prioritize safety, idempotency, and operator takeover paths.
### 3.1.5 Visual References (Reused Assets)
The following existing assets are included as supplementary architecture references:
- ![Box and Line Reference](../../F25_054_D_JUNO_FINAL/images/boxandline.png)
- ![Architecture Pattern Reference](../../F25_054_D_JUNO_FINAL/images/Architecturepattern.png)
## 3.2 Design Models
JUNO combines object-oriented domain modeling with workflow/event-driven behavior. The following models represent key operational flows.
### 3.2.1 Activity Diagram: Vendor Product Sync Lifecycle
```mermaid
flowchart TD
start[VendorSelectsSupplierProduct] --> submit[SubmitSyncRequest]
submit --> validateConn{ConnectionStatusConnected}
validateConn -->|No| rejectEarly[RejectRequestWithReason]
validateConn -->|Yes| createPending[CreateOrReopenPendingSync]
createPending --> notifySupplier[NotifySupplier]
notifySupplier --> supplierDecision{SupplierDecision}
supplierDecision -->|Accept| accept[SetStatusAcceptedAndPersistSnapshot]
supplierDecision -->|Reject| reject[SetStatusRejected]
accept --> notifyVendorA[NotifyVendor]
reject --> notifyVendorR[NotifyVendor]
notifyVendorA --> endNode[WorkflowComplete]
notifyVendorR --> endNode
```
### 3.2.2 Sequence Diagram: Storefront Chat to Escalation
```mermaid
sequenceDiagram
participant Customer
participant Widget as StorefrontWidget
participant API as ChatAPI
participant AI as L1Orchestrator
participant DB as Database
participant Vendor as VendorOperator
Customer->>Widget: Send message
Widget->>API: POST chat message
API->>DB: Persist customer message
API->>AI: Generate response with context
AI-->>API: Reply + metadata
API->>DB: Persist AI message
alt Escalation triggered
API->>DB: Create/Update escalation ticket (EmailLog)
API-->>Vendor: Ticket visible in vendor workflow
end
API-->>Widget: Return conversation messages
```
### 3.2.3 State Transition Diagram: Product Sync Entity
```mermaid
stateDiagram-v2
[*] --> PENDING
PENDING --> ACCEPTED: SupplierAccept
PENDING --> REJECTED: SupplierReject
REJECTED --> PENDING: VendorResubmit
PENDING --> [*]: VendorDelete
ACCEPTED --> [*]: AuthorizedDelete
REJECTED --> [*]: AuthorizedDelete
```
### 3.2.4 Class-Oriented Domain Structure (Conceptual)
```mermaid
classDiagram
class User {
+id
+email
+role
+mfaEnabled
}
class Store {
+id
+userId
+shopifyDomain
+storefrontChatEnabled
}
class SupplierProfile {
+id
+userId
+companyName
}
class Connection {
+id
+storeId
+supplierId
+status
}
class ProductSync {
+id
+storeId
+supplierId
+shopifyProductId
+status
}
class SupplierProductOffer {
+id
+productId
+storeId
+price
+quantity
}
class EmailLog {
+id
+storeId
+trigger
+status
}
User "1" --> "many" Store : owns
User "1" --> "0..1" SupplierProfile : has
Store "1" --> "many" Connection : links
SupplierProfile "1" --> "many" Connection : links
Store "1" --> "many" ProductSync : tracks
SupplierProfile "1" --> "many" ProductSync : processes
Store "1" --> "many" SupplierProductOffer : receives
Store "1" --> "many" EmailLog : records
```
### 3.2.5 Data Flow View (Level 0)
```mermaid
flowchart LR
customer[Customer] --> p1[StorefrontSupportProcess]
vendor[Vendor] --> p2[VendorCollaborationProcess]
supplier[Supplier] --> p3[SupplierDecisionProcess]
admin[Admin] --> p4[ObservabilityProcess]
p1 --> d1[(ChatAndEmailRecords)]
p2 --> d2[(CollaborationRecords)]
p3 --> d2
p2 --> d3[(CommerceCacheAndSync)]
p4 --> d4[(OperationalEvents)]
shopify[Shopify] --> p2
shopify --> p3
gmail[Gmail] --> p1
llm[AIProviders] --> p1
```
## 3.3 Data Design
JUNO transforms business interactions into store-scoped, role-aware persistent entities. The data model is implemented in PostgreSQL via Prisma and combines relational integrity with selected JSON flexibility for evolving AI/context attributes.
### 3.3.1 Data Storage Strategy
| Storage Item | Type | Purpose |
| --- | --- | --- |
| PostgreSQL relational tables | Structured transactional data | Users, stores, supplier connections, sync records, offers, notifications, logs |
| JSON columns (selected entities) | Semi-structured contextual data | Shopify payload snapshots, module policies, shared field answers, message metadata |
| Vector column (`pgvector`) | Embedding storage | Knowledge chunk retrieval for RAG-style responses |
| External provider state | Integration state | Gmail OAuth context, webhook events, Shopify synchronization inputs |
### 3.3.2 Core Data Entities and Processing Roles
| Entity Group | Operational Role |
| --- | --- |
| Identity entities (`User`, `Session`, `Account`) | Access control, role identity, authentication continuity |
| Store and collaboration entities (`Store`, `Connection`, `Invitation`) | Tenant boundary, supplier relationship governance |
| Product workflow entities (`ProductSync`, `SupplierProductOffer`) | Vendor-supplier product alignment and offer management |
| Commerce cache entities (`CachedProduct`, `CachedOrder`, `CachedCustomer`, `SyncMetadata`) | Fast query paths and synchronization observability |
| AI/helpdesk entities (`StorefrontChatConversation`, `StorefrontChatMessage`, `EmailLog`, `FlaggedEmail`) | Support automation, escalation, and communication traceability |
| Knowledge entities (`KnowledgeDocument`, `FaqItem`, `KnowledgeChunk`) | Retrieval corpus for AI-assisted answer generation |
### 3.3.3 Data Integrity and Consistency Design
- **Scoped uniqueness constraints** prevent duplicate collaboration records (for example, product sync uniqueness per store-supplier-product combination).
- **Indexing strategy** favors high-frequency filters such as `storeId`, status fields, and time-ordered event retrieval.
- **Upsert patterns** are used in synchronization metadata paths to avoid race-prone duplicate state rows.
- **Idempotent safeguards** are used in email automation paths to reduce duplicate outbound response side effects.
### 3.3.4 Security and Sensitive Data Handling
- Role-based boundaries are enforced before store-scoped data access operations.
- Sensitive credentials/tokens are kept out of client-visible payload surfaces.
- Security-relevant and operationally significant actions are persisted in logs for auditability.
- Public-facing chat endpoints apply rate controls to protect system stability and abuse posture.
### 3.3.5 Data Lifecycle (Simplified)
```mermaid
flowchart TD
ingest[ExternalOrUserEventIngest] --> validate[ValidateAndAuthorize]
validate --> persist[PersistOrUpdateStoreScopedRecords]
persist --> enrich[EnrichWithDerivedMetadata]
enrich --> notify[EmitNotificationsOrEscalations]
notify --> observe[ExposeToAdminObservability]
observe --> archive[RetainForAuditWindow]
```
## 3.4 Domain Model
The JUNO domain model represents operational relationships between merchants, suppliers, support automation, and observability workflows.
### 3.4.1 Domain Entity Perspective
- **Actor entities:** `User` (role-bearing identity), `SupplierProfile` (supplier-specific business identity), external customer interactions via support channels.
- **Business context entities:** `Store` as tenant boundary and operational anchor.
- **Collaboration entities:** `Connection`, `Invitation`, `ProductSync`, `SupplierProductOffer`.
- **Operational data entities:** cached commerce resources and sync metadata.
- **Support and AI entities:** storefront conversation/message records, email logs, flagged emails, knowledge artifacts.
### 3.4.2 Conceptual Domain ER Diagram
```mermaid
erDiagram
User ||--o{ Store : owns
User ||--o| SupplierProfile : has
Store ||--o{ Connection : has
SupplierProfile ||--o{ Connection : participates
Store ||--o{ ProductSync : contains
SupplierProfile ||--o{ ProductSync : reviews
Store ||--o{ SupplierProductOffer : receives
SupplierProfile ||--o{ SupplierProductOffer : provides
Store ||--o{ CachedProduct : caches
Store ||--o{ CachedOrder : caches
Store ||--o{ CachedCustomer : caches
Store ||--o{ SyncMetadata : monitors
Store ||--o{ StorefrontChatConversation : hosts
StorefrontChatConversation ||--o{ StorefrontChatMessage : contains
Store ||--o{ EmailLog : records
Store ||--o{ FlaggedEmail : flags
Store ||--o{ KnowledgeDocument : stores
Store ||--o{ FaqItem : stores
Store ||--o{ KnowledgeChunk : indexes
```
### 3.4.3 Domain Constraints and Associations
- A `Store` belongs to exactly one owning `User`, while one user can operate multiple stores.
- A `SupplierProfile` is bound to one supplier user identity and can connect to many stores through `Connection`.
- `ProductSync` and `SupplierProductOffer` provide two complementary collaboration mechanisms:
- vendor-requested sync workflow,
- supplier-initiated offer workflow.
- Support automation is modeled as event-bearing records rather than opaque chat history, enabling governance and analytics.
### 3.4.4 Domain Model Reference Asset
Supplementary conceptual reference from provided assets:
- ![Domain Model Reference](../../F25_054_D_JUNO_FINAL/images/domainmodel.png)
## 3.5 Chapter Summary
Section 3 establishes JUNO as a layered, role-aware workflow platform with a strong data-centric collaboration model. The design models and domain structure align with implemented modules and provide a clear bridge to implementation/testing chapters.

View File

@ -0,0 +1,247 @@
# Section 4: Implementation and Testing
This section explains how JUNO is implemented and validated. It focuses on practical algorithms used in key modules, third-party APIs/SDKs integrated in production workflows, and testing strategy with project-specific evidence.
## 4.1 Implementation Overview
JUNO is implemented as a full-stack Next.js application with App Router route handlers, Prisma-managed PostgreSQL data access, and modular AI/integration services. The implementation emphasizes:
- role-based execution paths (Vendor, Supplier, Admin),
- store-scoped workflow integrity,
- automation safety and observability,
- deterministic state transitions for collaboration operations.
Core implementation domains:
- **Collaboration engine:** vendor-supplier connection and product sync lifecycle.
- **Commerce synchronization:** Shopify webhook ingestion and cache/sync metadata updates.
- **AI support automation:** storefront and Gmail pipelines with escalation and safeguards.
- **Operational governance:** admin-level event and sync health visibility.
## 4.2 Algorithm Design
This subsection documents representative algorithms that drive major runtime behavior.
### 4.2.1 Algorithm A: Product Sync Request Creation and Reopen Logic
**Objective:** Create a new sync request only when business preconditions are valid, while safely reusing existing rejected records.
```text
Algorithm ProductSyncRequest(vendorUser, supplierId, productPayload):
1. Authorize vendor session and resolve active store.
2. Validate supplier exists and vendor-store has CONNECTED relationship with supplier.
3. Query existing sync by unique key (storeId, supplierId, shopifyProductId).
4. If existing status is ACCEPTED: reject request ("already synced").
5. If existing status is PENDING: reject request ("already pending").
6. If existing status is REJECTED:
a. Update same row: status <- PENDING, reset rejected/accepted timestamps.
b. Refresh product snapshot fields.
c. Notify supplier.
d. Return success.
7. Else create new ProductSync row with status PENDING and requester metadata.
8. Emit supplier notification.
9. Return created sync object.
```
### 4.2.2 Algorithm B: Supplier Decision State Transition
**Objective:** Enforce valid state transition from `PENDING` to `ACCEPTED`/`REJECTED` with actor ownership checks.
```text
Algorithm DecideSyncRequest(supplierUser, syncId, action):
1. Authorize supplier role and resolve supplier profile.
2. Load ProductSync(syncId) and verify supplier ownership.
3. If status != PENDING: reject ("already processed").
4. If action == ACCEPT:
a. status <- ACCEPTED
b. acceptedAt <- now
c. derive supplier_quantity snapshot from product variants (if available)
5. Else if action == REJECT:
a. status <- REJECTED
b. rejectedAt <- now
6. Persist update.
7. Notify vendor about decision.
8. Return updated status response.
```
### 4.2.3 Algorithm C: Shopify Webhook Verification and Sync Metadata Update
**Objective:** Process only authentic webhooks and maintain synchronization health state.
```text
Algorithm ProcessShopifyWebhook(headers, rawBody):
1. Extract hmac, topic, and domain headers; fail fast if missing.
2. Resolve store by normalized domain variants.
3. Verify HMAC using store-specific API secret.
4. If verification fails: return unauthorized.
5. Switch by topic:
- orders/create or orders/updated:
update order cache; upsert SyncMetadata(resource="orders", status="success").
- products/create or products/update:
update product cache;
trigger low-stock evaluation;
attempt auto-link of supplier offers;
upsert SyncMetadata(resource="products", status="success").
- products/delete:
remove cached product;
upsert SyncMetadata(resource="products", status="success").
6. Log processing outcome and return success.
```
### 4.2.4 Algorithm D: Storefront AI Reply with Escalation Gate
**Objective:** Generate a bounded AI response and escalate when metadata or content signals require human handling.
```text
Algorithm StorefrontReplyWithEscalation(store, visitor, message):
1. Apply endpoint and conversation-level rate limits.
2. Resolve/create conversation for (storeId, visitorId).
3. Persist customer message.
4. Gather recent conversation history.
5. Run L1 orchestrator with module flags, shared fields, and session-bound identity state.
6. Persist AI response message.
7. Evaluate escalation trigger:
- usedTools contains "ticket_intake", or
- module indicates human escalation, or
- response includes escalation handoff phrases.
8. If escalation true:
a. Create/update EmailLog ticket artifact with storefront trigger.
9. Return response payload with messages + AI metadata.
```
### 4.2.5 Algorithm E: Gmail Auto-Reply Safety Pipeline
**Objective:** Produce reliable, non-duplicate, policy-safe automated replies for vendor accounts.
```text
Algorithm ProcessIncomingGmail(userId, storeId):
1. Resolve Gmail token; abort if not connected.
2. Fetch latest unread candidate email(s).
3. Parse sender and fetch full email content.
4. Run abuse/safety screening:
- If flagged: persist FlaggedEmail, mark message read, stop.
5. Persist inbound EmailLog event.
6. Apply rate-limit checks for store+sender.
7. Acquire idempotency lease for inbound message ID.
8. Generate auto-reply preview/classification and final HTML.
9. Send reply and persist result.
10. Mark original email as read.
11. Release lease on failure paths and return execution result.
```
### 4.2.6 Algorithm Interaction Diagram
```mermaid
flowchart TD
vendorAction[VendorAction] --> syncAlg[ProductSyncAlgorithmsAandB]
shopifyEvent[ShopifyWebhookEvent] --> webhookAlg[WebhookAlgorithmC]
customerMessage[StorefrontMessage] --> chatAlg[StorefrontAlgorithmD]
gmailEvent[GmailInboundEvent] --> gmailAlg[GmailAlgorithmE]
syncAlg --> data[(PostgreSQLModels)]
webhookAlg --> data
chatAlg --> data
gmailAlg --> data
chatAlg --> ai[LLMOrchestrator]
gmailAlg --> ai
```
## 4.3 External APIs and SDKs
JUNO integrates multiple third-party services. The following table lists major external dependencies used directly in implementation workflows.
| API/SDK and Version | Description | Purpose in JUNO | Example Endpoint / Class / Function Usage |
| --- | --- | --- | --- |
| Shopify Admin API (2024-01 routes used) | Commerce platform API for products/orders/webhooks | Store onboarding, webhook registration, data synchronization | `POST https://{shop}/admin/api/2024-01/webhooks.json`, webhook handler `/api/webhooks/shopify` |
| Gmail API (Google OAuth2 + Gmail REST) | Email integration and mailbox event processing | Vendor email automation, inbound processing, auto-replies | OAuth authorization URL builder, `users/me/messages`, webhook `/api/webhooks/gmail` |
| OpenAI SDK `openai` (`^4.104.0`) | LLM and embedding provider | Knowledge embeddings and optional chat completion flows | OpenAI chat/embedding calls through internal AI service wrappers |
| Groq API client integration | LLM inference provider | Primary/fallback completion paths for support automation and classification | `groqChatCompletion` usage in `aiChatCompletion` workflow |
| NextAuth/Auth.js (`next-auth`, `@auth/*`) | Authentication/session framework | Multi-role session and portal-specific auth behavior | `NextAuth(...)`, adapter + callbacks + role-aware auth resolution |
| Prisma ORM (`prisma`, `@prisma/client`) | Database access and schema management | Type-safe data access, migrations, constraints, relations | `prisma.*` model operations and migration lifecycle scripts |
| Upstash Rate Limit (`@upstash/ratelimit`, `@upstash/redis`) | Request throttling primitives | Public chat anti-abuse and throughput protection | `applyRateLimit(...)` in storefront/public API flows |
| AWS SDK S3 (`@aws-sdk/client-s3`) | Object storage integration | Media upload/storage path for assets (environment-dependent) | S3 client calls for supplier/storefront image paths |
| Nodemailer (`nodemailer`) | Email transport abstraction | Outbound email delivery and logging in support workflows | Email send service + `EmailLog` persistence |
| Otplib (`otplib`) | TOTP validation library | MFA verification for credential-based auth paths | `verify({ token, secret })` in authentication flow |
### 4.3.1 API Integration Design Notes
- External integrations are wrapped behind service and route boundaries to isolate provider concerns from domain logic.
- Webhook integrations use explicit verification/normalization before business processing.
- AI provider selection is configurable, with controlled fallback behavior and structured logging.
## 4.4 Testing Details
JUNO testing combines unit tests, integration-style tests, and targeted scenario suites for high-risk automation paths.
### 4.4.1 Test Stack
- **Primary test runner:** Vitest
- **E2E capability:** Playwright
- **Specialized scenario harness:** Gmail auto-reply test environment using Dockerized dependencies
### 4.4.2 Test Scope by Module
| Module Area | Typical Test Type | Example Coverage |
| --- | --- | --- |
| API validation and authorization | Unit/integration | Route-level guards, forbidden access, schema validation |
| Product sync and inventory logic | Unit | Offer matching, quantity reconciliation, auto-link helper logic |
| Shopify/order automation utilities | Unit | Hold synchronization, line-item parsing and transforms |
| AI support utility logic | Unit/scenario | Gmail context extraction, order disclosure rules, classification invariants |
| Gmail automation pipeline | Integration-style scenario | End-to-end preview/output assertions across curated scenarios |
### 4.4.3 Representative Existing Test Assets
The following current tests demonstrate implemented quality gates:
- `tests/gmail-auto-reply/run.test.ts`
- `src/lib/products/autoLinkOfferToShopifyProduct.test.ts`
- `src/lib/automation/ticketSupplierRouting.test.ts`
- `src/app/api/chat/messages/post.test.ts`
- `src/lib/api-schemas/schemas.test.ts`
- `src/lib/shopify/orderHoldSync.test.ts`
- `tests/l1/orderTools.test.ts`
### 4.4.4 Testing Workflow
```mermaid
flowchart LR
codeChange[CodeChange] --> localTests[RunVitestSuite]
localTests --> scenarioTests[RunScenarioSuites]
scenarioTests --> e2e[OptionalPlaywrightE2E]
e2e --> review[ReviewArtifactsAndLogs]
review --> releaseReady[ReleaseCandidateReady]
```
### 4.4.5 Unit Testing Strategy
- Target deterministic helper functions first (normalization, mapping, parsing, guard logic).
- Validate edge-case behavior explicitly (null/empty/ambiguous inputs).
- Mock external boundaries (auth, DB client, push/external calls) for stable and isolated tests.
- Include authorization-failure tests on sensitive route handlers.
### 4.4.6 Integration and Scenario Testing Strategy
- Use scenario-based assertions for Gmail auto-reply behavior and classifier invariants.
- Validate response safety paths: off-topic suppression, missing-order handling, escalation conditions.
- Record artifacts for auditability and debugging of AI path outcomes.
### 4.4.7 Suggested Section 4 Test Evidence Table (for report/viva)
| Test ID | Objective | Method | Expected Result |
| --- | --- | --- | --- |
| T-API-AUTH-01 | Ensure unauthorized actor cannot post restricted chat payload | Route test with mocked auth/session | HTTP 403 returned |
| T-SYNC-STATE-01 | Ensure rejected sync can be reopened correctly | Product sync workflow test | Existing row reused with `PENDING` status |
| T-WEBHOOK-SEC-01 | Ensure invalid Shopify signature is rejected | Webhook handler security test | Request denied; no data mutation |
| T-AI-ESC-01 | Ensure escalation tickets are created on escalation signal | Storefront AI integration test | `EmailLog` escalation record is created/updated |
| T-GMAIL-SAFE-01 | Ensure flagged abusive email does not get auto-reply | Gmail scenario test | Email flagged, no outbound reply sent |
| T-GMAIL-IDEMP-01 | Ensure duplicate inbound message is not double-replied | Lease/idempotency scenario test | First process replies; duplicate is skipped |
### 4.4.8 Testing References (Reused Assets)
- ![Algorithm Reference](../../F25_054_D_JUNO_FINAL/images/algoexample.png)
- ![Unit Testing Reference](../../F25_054_D_JUNO_FINAL/images/unittesting.png)
## 4.5 Chapter Summary
Section 4 documents JUNO’s implementation through concrete algorithms, production integrations, and test practices that emphasize correctness, safety, and operational reliability. The chapter provides a direct bridge from system design to verifiable execution behavior.

View File

@ -0,0 +1,39 @@
-- Add structured metadata fields to EmailLog for ticket context traceability.
ALTER TABLE "EmailLog"
ADD COLUMN "metadata" JSONB,
ADD COLUMN "storefrontConversationId" TEXT;
CREATE INDEX "EmailLog_storeId_storefrontConversationId_sentAt_idx"
ON "EmailLog"("storeId", "storefrontConversationId", "sentAt");
-- Add explicit source->target product links for strict upsell eligibility.
CREATE TABLE "UpsellProductLink" (
"id" TEXT NOT NULL,
"storeId" TEXT NOT NULL,
"sourceShopifyProductId" TEXT NOT NULL,
"targetShopifyProductId" TEXT NOT NULL,
"targetStoreId" TEXT,
"notes" TEXT,
"isActive" BOOLEAN NOT NULL DEFAULT true,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "UpsellProductLink_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "UpsellProductLink_storeId_sourceShopifyProductId_targetShopif_key"
ON "UpsellProductLink"("storeId", "sourceShopifyProductId", "targetShopifyProductId");
CREATE INDEX "UpsellProductLink_storeId_sourceShopifyProductId_isActive_idx"
ON "UpsellProductLink"("storeId", "sourceShopifyProductId", "isActive");
CREATE INDEX "UpsellProductLink_storeId_targetShopifyProductId_isActive_idx"
ON "UpsellProductLink"("storeId", "targetShopifyProductId", "isActive");
ALTER TABLE "UpsellProductLink"
ADD CONSTRAINT "UpsellProductLink_storeId_fkey"
FOREIGN KEY ("storeId") REFERENCES "Store"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "UpsellProductLink"
ADD CONSTRAINT "UpsellProductLink_targetStoreId_fkey"
FOREIGN KEY ("targetStoreId") REFERENCES "Store"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@ -187,11 +187,13 @@ model Store {
lowStockAlertStates LowStockAlertState[]
lowStockShopifyVendorAlertStates LowStockShopifyVendorAlertState[]
supplierAutomationLogs SupplierAutomationLog[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId]) // Add index for performance
upsellProductLinksFrom UpsellProductLink[] @relation("UpsellLinksFromStore")
upsellProductLinksTo UpsellProductLink[] @relation("UpsellLinksToStore")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([userId]) // Add index for performance
}
/**
@ -576,6 +578,8 @@ model EmailLog {
to String
subject String
bodyPreview String?
metadata Json?
storefrontConversationId String?
// Whether this email was triggered automatically by the system
isAutomated Boolean @default(false)
@ -590,6 +594,25 @@ model EmailLog {
@@index([storeId, sentAt])
@@index([userId, sentAt])
@@index([storeId, storefrontConversationId, sentAt])
}
model UpsellProductLink {
id String @id @default(cuid())
storeId String
store Store @relation("UpsellLinksFromStore", fields: [storeId], references: [id], onDelete: Cascade)
sourceShopifyProductId String
targetShopifyProductId String
targetStoreId String?
targetStore Store? @relation("UpsellLinksToStore", fields: [targetStoreId], references: [id], onDelete: SetNull)
notes String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([storeId, sourceShopifyProductId, targetShopifyProductId])
@@index([storeId, sourceShopifyProductId, isActive])
@@index([storeId, targetShopifyProductId, isActive])
}
enum FlaggedEmailPriority {

View File

@ -0,0 +1,79 @@
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 { getTicketThreadForStore } from "@/lib/tickets/emailLogTickets"
export async function GET(
request: Request,
{ params }: { params: Promise<{ ticketId: 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 resolved = await params
const ticketId = decodeURIComponent(resolved.ticketId || "")
const { searchParams } = new URL(request.url)
const before = searchParams.get("before")
const limit = Math.min(
Math.max(Number.parseInt(searchParams.get("limit") || "50", 10) || 50, 1),
200,
)
if (!ticketId) {
return NextResponse.json({ message: "Invalid ticket id" }, { status: 400 })
}
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" }, { status: 400 })
}
const ticket = await getTicketThreadForStore(activeStore.id, ticketId)
if (!ticket) {
return NextResponse.json({ message: "Ticket not found" }, { status: 404 })
}
const sorted = [...ticket.messages].sort(
(a, b) => new Date(b.sentAt).getTime() - new Date(a.sentAt).getTime(),
)
const offset =
before != null
? Math.max(
sorted.findIndex((m) => m.id === before) + 1,
0,
)
: 0
const page = sorted.slice(offset, offset + limit)
const nextCursor =
offset + limit < sorted.length ? page[page.length - 1]?.id || null : null
return NextResponse.json({
ticket: {
...ticket,
messages: page,
},
nextCursor,
totalMessages: sorted.length,
})
} catch (error) {
logger.error("Failed to load ticket detail", error)
return NextResponse.json({ message: "Failed to load ticket detail" }, { status: 500 })
}
}

View File

@ -5,22 +5,12 @@ import { isVendorPortalRole } from "@/lib/auth-roles"
import { prisma } from "@/lib/prisma"
import { logger } from "@/lib/logger"
import { draftCustomerReply } from "@/lib/automation/draftCustomerReply"
import { getTicketThreadForStore } from "@/lib/tickets/emailLogTickets"
const bodySchema = z.object({
ticketId: z.string().trim().min(1).max(600),
})
function normalizeSubject(subject: string | null): string {
if (!subject) return "No subject"
let s = subject.trim()
while (true) {
const match = s.match(/^(re|fw|fwd):\s*/i)
if (!match) break
s = s.slice(match[0].length).trim()
}
return s || "No subject"
}
function stripStorefrontEscalationPrefix(text: string): string {
return text.replace(/^Storefront escalation from visitor [^:]+:\s*/i, "").trim()
}
@ -70,33 +60,12 @@ export async function POST(req: Request) {
return NextResponse.json({ message: "No active store" }, { status: 400 })
}
const splitAt = parsed.data.ticketId.indexOf("::")
const customerEmailRaw =
splitAt >= 0 ? parsed.data.ticketId.slice(0, splitAt) : parsed.data.ticketId
const rootSubjectRaw = splitAt >= 0 ? parsed.data.ticketId.slice(splitAt + 2) : ""
const customerEmail = (customerEmailRaw || "").trim().toLowerCase()
const rootSubject = (rootSubjectRaw || "").trim()
if (!customerEmail || !rootSubject) {
return NextResponse.json({ message: "Invalid ticket id" }, { status: 400 })
}
const logs = await prisma.$queryRaw<
Array<{ subject: string | null; bodyPreview: string | null; sentAt: Date; trigger: string | null }>
>`
SELECT subject, "bodyPreview", "sentAt", trigger
FROM "EmailLog"
WHERE "storeId" = ${store.id}
AND LOWER(TRIM("to")) = ${customerEmail}
ORDER BY "sentAt" DESC
LIMIT 80
`
const matched = logs.filter((l) => normalizeSubject(l.subject) === rootSubject).slice(0, 12)
if (matched.length === 0) {
const ticket = await getTicketThreadForStore(store.id, parsed.data.ticketId)
if (!ticket) {
return NextResponse.json({ message: "No ticket messages found" }, { status: 404 })
}
const conversation = matched
const conversation = ticket.messages
.reverse()
.map((m, idx) => {
const body = stripStorefrontEscalationPrefix((m.bodyPreview || "").trim())
@ -107,8 +76,8 @@ export async function POST(req: Request) {
const drafted = await draftCustomerReply({
storeName: store.businessName || "Our store",
customerEmail,
ticketSubject: rootSubject,
customerEmail: ticket.customerEmail,
ticketSubject: ticket.rootSubject,
conversation,
})

View File

@ -71,6 +71,7 @@ export async function POST(req: Request) {
storeId,
isAutomated: false,
trigger: "TICKET_CUSTOMER_REPLY",
metadata: { ticketId: parsed.data.ticketId, channel: "vendor_reply" },
},
)

View File

@ -3,44 +3,9 @@ import { auth } from "@/auth"
import { isVendorPortalRole } from "@/lib/auth-roles"
import { prisma } from "@/lib/prisma"
import { logger } from "@/lib/logger"
import { listTicketThreadsForStore } from "@/lib/tickets/emailLogTickets"
// Normalize subject by stripping common reply prefixes so we can group conversations
function normalizeSubject(subject: string | null): string {
if (!subject) return "No subject"
let s = subject.trim()
// Remove repeated "Re:" / "Fwd:" prefixes
// e.g. "Re: Re: Hello" -> "Hello"
// Case-insensitive, allow localized variants in future by trimming until no prefix matches
while (true) {
const match = s.match(/^(re|fw|fwd):\s*/i)
if (!match) break
s = s.slice(match[0].length).trim()
}
return s || "No subject"
}
function decodeHtmlEntities(input: string | null | undefined): string {
const text = (input || "").trim()
if (!text) return ""
return text
.replace(/&#(\d+);/g, (_, n: string) => {
const code = Number.parseInt(n, 10)
return Number.isFinite(code) ? String.fromCharCode(code) : _
})
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex: string) => {
const code = Number.parseInt(hex, 16)
return Number.isFinite(code) ? String.fromCharCode(code) : _
})
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&apos;/g, "'")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
}
export async function GET() {
export async function GET(request: Request) {
try {
const session = await auth()
if (!session?.user?.id) {
@ -68,119 +33,29 @@ export async function GET() {
return NextResponse.json({ tickets: [] })
}
// Use a raw query instead of relying on the generated Prisma model,
// to avoid issues if the EmailLog model is not present on the client.
const logs = await prisma.$queryRaw<
Array<{
id: string
"userId": string
"storeId": string | null
to: string
subject: string | null
"bodyPreview": string | null
"isAutomated": boolean
trigger: string | null
status: string
"providerMessageId": string | null
"errorMessage": string | null
"sentAt": Date
}>
>`
SELECT
"id",
"userId",
"storeId",
"to",
"subject",
"bodyPreview",
"isAutomated",
"trigger",
"status",
"providerMessageId",
"errorMessage",
"sentAt"
FROM "EmailLog"
WHERE "storeId" = ${activeStore.id}
AND (
"isAutomated" = TRUE
OR "trigger" = 'GMAIL_INCOMING'
OR "trigger" = 'TICKET_CUSTOMER_REPLY'
)
ORDER BY "sentAt" DESC
LIMIT 200
`
const { searchParams } = new URL(request.url)
const query = (searchParams.get("q") || "").trim().toLowerCase()
type TicketMessage = {
id: string
subject: string
bodyPreview: string | null
status: string
trigger: string | null
sentAt: string
}
type Ticket = {
id: string
subject: string
rootSubject: string
customerEmail: string
status: string
updatedAt: string
messagesCount: number
lastMessageSnippet: string
messages: TicketMessage[]
}
const ticketsMap = new Map<string, Ticket>()
for (const log of logs) {
const customerEmail = (log.to || "").toLowerCase()
const rootSubject = normalizeSubject(log.subject)
const key = `${customerEmail}::${rootSubject}`
const baseMessage: TicketMessage = {
id: log.id,
subject: log.subject || rootSubject,
bodyPreview: decodeHtmlEntities(log.bodyPreview),
status: log.status,
trigger: log.trigger,
sentAt: log.sentAt.toISOString(),
}
const existing = ticketsMap.get(key)
if (!existing) {
ticketsMap.set(key, {
id: key,
subject: log.subject || rootSubject,
rootSubject,
customerEmail,
status: log.status,
updatedAt: log.sentAt.toISOString(),
messagesCount: 1,
lastMessageSnippet: decodeHtmlEntities(log.bodyPreview),
messages: [baseMessage],
})
} else {
existing.messages.push(baseMessage)
existing.messagesCount += 1
// The logs are ordered newest-first, so the first seen message is the latest
// Keep lastMessageSnippet / updatedAt from the first entry
}
}
const tickets = Array.from(ticketsMap.values()).filter((t) => {
const latest = t.messages[0]
const storefrontEscalationThread = t.messages.some((m) => m.trigger === "STOREFRONT_TICKET")
const resolvedByVendorEmail = latest?.trigger === "TICKET_CUSTOMER_REPLY"
// Hide resolved storefront escalation threads after vendor sends customer email.
// If customer replies later (GMAIL_INCOMING), latest trigger changes and thread reappears.
if (storefrontEscalationThread && resolvedByVendorEmail) {
return false
}
return true
})
return NextResponse.json({ tickets })
const tickets = await listTicketThreadsForStore(activeStore.id)
const filtered = query
? tickets.filter((t) => {
const haystack = [
t.subject,
t.rootSubject,
t.customerEmail,
t.lastMessageSnippet,
...t.messages.map((m) => m.bodyPreview || ""),
]
.join("\n")
.toLowerCase()
return haystack.includes(query)
})
: tickets
const light = filtered.map((t) => ({
...t,
messages: [],
}))
return NextResponse.json({ tickets: light })
} catch (error) {
logger.error("Failed to load vendor tickets from email logs", error)
return NextResponse.json(

View File

@ -78,6 +78,20 @@ function buildOrderScopedTicketSubject(moduleName?: string, orderNumber?: string
return `${base} - Order ${clean}`
}
const MAX_STOREFRONT_TICKET_TRANSCRIPT_CHARS = 12000
function formatStorefrontTranscript(
rows: Array<{ senderType: "CUSTOMER" | "AI"; content: string; createdAt: Date }>,
): string {
const lines = rows.map((row) => {
const role = row.senderType === "CUSTOMER" ? "CUSTOMER" : "ASSISTANT"
const when = row.createdAt.toISOString()
return `[${when}] ${role}: ${row.content}`
})
const full = lines.join("\n")
if (full.length <= MAX_STOREFRONT_TICKET_TRANSCRIPT_CHARS) return full
return full.slice(full.length - MAX_STOREFRONT_TICKET_TRANSCRIPT_CHARS)
}
function shouldCreateEscalationTicket(ai: { content: string; meta?: { usedTools?: string[]; module?: string } }): boolean {
if (Array.isArray(ai.meta?.usedTools) && ai.meta.usedTools.includes("ticket_intake")) {
return true
@ -759,6 +773,24 @@ export async function POST(req: Request) {
where: { id: store.id },
select: { userId: true },
})
const transcriptRows = await prisma.storefrontChatMessage.findMany({
where: { conversationId: conversation.id },
orderBy: { createdAt: "asc" },
take: 240,
select: {
senderType: true,
content: true,
createdAt: true,
},
})
const transcript = formatStorefrontTranscript(transcriptRows)
const escalationMetadata = {
channel: "storefront",
module: ai.meta?.module || null,
usedTools: ai.meta?.usedTools || [],
visitorId,
conversationId: conversation.id,
}
if (owner?.userId) {
await prisma.emailLog.create({
data: {
@ -766,7 +798,9 @@ export async function POST(req: Request) {
storeId: store.id,
to: ticketTo,
subject: ticketSubject,
bodyPreview: customerContent.slice(0, 2000),
bodyPreview: transcript,
metadata: escalationMetadata,
storefrontConversationId: conversation.id,
isAutomated: true,
trigger: STOREFRONT_TICKET_TRIGGER,
status: "SENT",

View File

@ -1,6 +1,6 @@
"use client"
import { useEffect, useState } from "react"
import { useCallback, useEffect, useState } from "react"
import { useParams, useRouter } from "next/navigation"
import {
ArrowLeft,
@ -66,6 +66,7 @@ export default function TicketDetailPage() {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [expandedMessages, setExpandedMessages] = useState<Record<string, boolean>>({})
const [chronological, setChronological] = useState(true)
const [orderRefInput, setOrderRefInput] = useState("")
const [askPreviewLoading, setAskPreviewLoading] = useState(false)
@ -83,25 +84,45 @@ export default function TicketDetailPage() {
const [sendingCustomer, setSendingCustomer] = useState(false)
const [customerError, setCustomerError] = useState<string | null>(null)
useEffect(() => {
const loadTicket = async () => {
const loadTicket = useCallback(async () => {
try {
setLoading(true)
setError(null)
const res = await fetch("/api/juno-engine/tickets", {
cache: "no-store",
})
if (!res.ok) {
throw new Error("Failed to load ticket")
}
const data = (await res.json()) as { tickets: TicketData[] }
const decodedId = decodeURIComponent(params.ticketId)
const found = data.tickets.find((t) => t.id === decodedId) || null
if (!found) {
let cursor: string | null = null
let mergedTicket: TicketData | null = null
while (true) {
const url = cursor
? `/api/juno-engine/tickets/${encodeURIComponent(decodedId)}?limit=100&before=${encodeURIComponent(cursor)}`
: `/api/juno-engine/tickets/${encodeURIComponent(decodedId)}?limit=100`
const res = await fetch(url, {
cache: "no-store",
})
if (!res.ok) {
throw new Error("Failed to load ticket")
}
const data = (await res.json()) as { ticket: TicketData; nextCursor?: string | null }
if (!data.ticket) break
if (!mergedTicket) {
mergedTicket = { ...data.ticket, messages: [...(data.ticket.messages || [])] }
} else {
mergedTicket.messages.push(...(data.ticket.messages || []))
mergedTicket.messagesCount = mergedTicket.messages.length
}
if (!data.nextCursor) break
cursor = data.nextCursor
}
if (!mergedTicket) {
setError("Ticket not found.")
} else {
setTicket(found)
setTicket(mergedTicket)
const defaults: Record<string, boolean> = {}
for (const m of mergedTicket.messages || []) {
if (m.trigger === "STOREFRONT_TICKET") {
defaults[m.id] = true
}
}
setExpandedMessages(defaults)
}
} catch (err) {
console.error("Failed to load ticket", err)
@ -109,11 +130,12 @@ export default function TicketDetailPage() {
} finally {
setLoading(false)
}
}
void loadTicket()
}, [params.ticketId])
useEffect(() => {
void loadTicket()
}, [loadTicket])
useEffect(() => {
if (!ticket) return
setCustomerTo(ticket.customerEmail || "")
@ -277,6 +299,7 @@ export default function TicketDetailPage() {
setCustomerModalOpen(false)
setCustomerBody("")
setCustomerError(null)
await loadTicket()
} catch (e) {
setCustomerError(e instanceof Error ? e.message : "Send failed")
} finally {
@ -287,6 +310,14 @@ export default function TicketDetailPage() {
const cleanDisplayText = (text: string): string =>
text.replace(/^Storefront escalation from visitor [^:]+:\s*/i, "").trim()
const sourceLabel = (trigger: string | null): string => {
if (trigger === "GMAIL_INCOMING") return "Email inbound"
if (trigger === "TICKET_CUSTOMER_REPLY") return "Vendor email"
if (trigger === "STOREFRONT_TICKET") return "Storefront escalation"
if (trigger === "AI_L1_REPLY") return "AI auto-reply"
return trigger ? trigger.toLowerCase().replace(/_/g, " ") : "System"
}
const getStatusStyles = (status: TicketStatus | string) => {
switch (status) {
case "open":
@ -384,6 +415,34 @@ export default function TicketDetailPage() {
isLight ? "bg-white border-slate-200" : "bg-white/5 border-white/10",
)}
>
<button
type="button"
onClick={() => setChronological((prev) => !prev)}
className={cn(
"h-9 px-3 rounded-lg font-bold text-xs inline-flex items-center gap-2 border transition-colors",
isLight
? "bg-white text-slate-900 border-slate-200 hover:bg-slate-50"
: "bg-white/10 text-white border-white/10 hover:bg-white/15",
)}
>
{chronological ? "Newest first" : "Oldest first"}
</button>
<button
type="button"
onClick={() =>
setExpandedMessages(
Object.fromEntries((ticket.messages || []).map((m) => [m.id, true])),
)
}
className={cn(
"h-9 px-3 rounded-lg font-bold text-xs inline-flex items-center gap-2 border transition-colors",
isLight
? "bg-white text-slate-900 border-slate-200 hover:bg-slate-50"
: "bg-white/10 text-white border-white/10 hover:bg-white/15",
)}
>
Expand all
</button>
<button
type="button"
onClick={() => setSupplierModalOpen(true)}
@ -770,7 +829,7 @@ export default function TicketDetailPage() {
{/* Conversation timeline - Full width corners chat style */}
<div className="flex-1 overflow-y-auto min-h-0 pr-4 -mr-4 space-y-8 pb-10 custom-scrollbar">
{ticket.messages.map((message) => {
{(chronological ? [...ticket.messages].reverse() : ticket.messages).map((message) => {
const isIncoming =
message.trigger === "GMAIL_INCOMING" || message.status === "RECEIVED"
@ -825,6 +884,20 @@ export default function TicketDetailPage() {
>
{message.status}
</span>
<span
className={cn(
"px-2.5 py-0.5 rounded-md text-[9px] font-black tracking-widest uppercase border",
isIncoming
? isLight
? "bg-indigo-50 border-indigo-100 text-indigo-700"
: "bg-indigo-500/20 text-indigo-200 border-indigo-400/20"
: isLight
? "bg-white border-indigo-200 text-indigo-700"
: "bg-white/10 text-indigo-100 border-white/10",
)}
>
{sourceLabel(message.trigger)}
</span>
</div>
<span className={cn(
"text-[10px] whitespace-nowrap font-bold",
@ -832,9 +905,9 @@ export default function TicketDetailPage() {
? (isLight ? "text-slate-400" : "text-slate-500")
: "text-indigo-200"
)}>
{new Date(message.sentAt).toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
{new Date(message.sentAt).toLocaleString(undefined, {
dateStyle: "medium",
timeStyle: "short",
})}
</span>
</div>

View File

@ -36,6 +36,8 @@ interface TicketData {
export default function TicketsPage() {
const router = useRouter()
const [searchTerm, setSearchTerm] = useState("")
const [showFilters, setShowFilters] = useState(false)
const [statusFilter, setStatusFilter] = useState<"all" | "open" | "pending" | "resolved" | "closed">("all")
const [tickets, setTickets] = useState<TicketData[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
@ -45,7 +47,11 @@ export default function TicketsPage() {
try {
setLoading(true)
setError(null)
const res = await fetch("/api/juno-engine/tickets", {
const query = searchTerm.trim()
const url = query
? `/api/juno-engine/tickets?q=${encodeURIComponent(query)}`
: "/api/juno-engine/tickets"
const res = await fetch(url, {
cache: "no-store",
})
if (!res.ok) {
@ -62,7 +68,7 @@ export default function TicketsPage() {
}
void loadTickets()
}, [])
}, [searchTerm])
const getStatusStyles = (status: TicketStatus | string) => {
switch (status) {
@ -75,13 +81,8 @@ export default function TicketsPage() {
}
const filteredTickets = tickets.filter((ticket) => {
if (!searchTerm.trim()) return true
const q = searchTerm.toLowerCase()
return (
ticket.subject.toLowerCase().includes(q) ||
ticket.customerEmail.toLowerCase().includes(q) ||
ticket.id.toLowerCase().includes(q)
)
if (statusFilter === "all") return true
return (ticket.status || "").toLowerCase() === statusFilter
})
return (
@ -95,7 +96,10 @@ export default function TicketsPage() {
</p>
</div>
<div className="flex gap-2">
<button className="h-10 md:h-11 px-4 bg-white/10 border border-white/10 rounded-xl font-bold text-slate-300 hover:bg-white/15 transition-all flex items-center justify-center gap-2 text-sm shrink-0">
<button
onClick={() => setShowFilters((prev) => !prev)}
className="h-10 md:h-11 px-4 bg-white/10 border border-white/10 rounded-xl font-bold text-slate-300 hover:bg-white/15 transition-all flex items-center justify-center gap-2 text-sm shrink-0"
>
<Filter className="w-4 h-4" /> Filters
</button>
</div>
@ -114,6 +118,24 @@ export default function TicketsPage() {
/>
</div>
</div>
{showFilters && (
<div className="flex flex-wrap items-center gap-2">
{(["all", "open", "pending", "resolved", "closed"] as const).map((status) => (
<button
key={status}
type="button"
onClick={() => setStatusFilter(status)}
className={`h-8 px-3 rounded-lg text-xs font-bold uppercase tracking-wider ${
statusFilter === status
? "bg-indigo-600 text-white"
: "bg-white/10 text-slate-300 border border-white/10"
}`}
>
{status}
</button>
))}
</div>
)}
{/* Tickets Grid */}
<div className="flex-1 overflow-y-auto overflow-x-hidden min-h-0">
@ -153,7 +175,10 @@ export default function TicketsPage() {
</p>
</div>
</div>
<button className="p-2 hover:bg-white/10 rounded-lg text-slate-400 hover:text-indigo-400 transition-all shrink-0">
<button
onClick={(e) => e.stopPropagation()}
className="p-2 hover:bg-white/10 rounded-lg text-slate-400 hover:text-indigo-400 transition-all shrink-0"
>
<MoreHorizontal className="w-4 h-4" />
</button>
</div>

View File

@ -21,6 +21,7 @@ import {
type StorefrontChatAction,
} from "@/lib/storefront-chat/actions"
import { findAutomaticUpsellCandidates } from "@/lib/storefront-chat/upsells"
import { getLinkedUpsellProducts } from "@/lib/ai/tools/upsellLinks"
import {
isAutomationEnabled,
isModuleEnabled,
@ -56,6 +57,14 @@ const L1_MAX_HISTORY_MESSAGE_CHARS = 2000
const EMPTY_STORE_REFERENCE =
"(No matching store, order, or product details were retrieved. If the customer asks for specifics you cannot infer, say briefly that you do not have that information and offer to connect them with the team if appropriate.)"
const STRICT_UPSELL_LINKS =
process.env.STRICT_UPSELL_LINKS === undefined
? true
: process.env.STRICT_UPSELL_LINKS !== "false"
const INCLUDE_PRODUCT_RETRIEVAL_DEFAULT =
process.env.L1_INCLUDE_PRODUCT_RETRIEVAL === undefined
? true
: process.env.L1_INCLUDE_PRODUCT_RETRIEVAL !== "false"
/** Strip NULs and cap length before placing user text inside a delimited prompt block. */
function packCustomerMessageForPrompt(message: string): string {
@ -330,6 +339,7 @@ async function composeReply(params: {
const system = [
`You are the customer support assistant for ${params.storeName}.`,
"Ground answers only in the material between STORE_DATA_START and STORE_DATA_END in the user message. Do not invent order numbers, tracking URLs, prices, or inventory counts.",
"Only recommend or mention specific products if they are explicitly listed in store data as allowed recommendation candidates. If no allowed recommendation candidates are present, do not suggest additional products.",
"If you cannot answer because that material does not include what they asked for (counts, totals, tracking, stock, etc.), reply in one or two short, friendly sentences: you do not have that information or cannot look it up right now. Offer human support if it fits the tone.",
"For general policy questions (shipping time, return window, delivery coverage, refund policy), answer directly from available policy/knowledge text in 2-4 short sentences. Do not ask for order verification unless the user is asking about a specific order.",
"If both general policy and order-specific interpretations are possible, prefer the general policy interpretation unless the customer explicitly references their own order (for example: 'my order', order number, tracking number).",
@ -369,6 +379,45 @@ async function composeReply(params: {
return text
}
function extractShopifyProductIdsFromLineItems(lineItems: unknown): string[] {
const out: string[] = []
if (!Array.isArray(lineItems)) return out
for (const item of lineItems) {
if (!item || typeof item !== "object") continue
const row = item as { product_id?: unknown; productId?: unknown; id?: unknown }
const candidate = row.product_id ?? row.productId ?? row.id
if (candidate === null || candidate === undefined) continue
const normalized = String(candidate).trim()
if (normalized) out.push(normalized)
}
return out
}
function dedupe(values: string[]): string[] {
return Array.from(new Set(values.filter(Boolean)))
}
function sanitizeUpsellText(text: string, allowedTitles: string[]): string {
if (!STRICT_UPSELL_LINKS) return text
const hasUpsellCue =
/\b(also buy|also like|also want|recommend|pair it with|consider adding|goes well with|complement)\b/i.test(
text,
)
if (allowedTitles.length === 0) {
return hasUpsellCue
? "I can help with this product question, and if you want add-on recommendations a human specialist can provide tailored options."
: text
}
const lowered = text.toLowerCase()
const mentionsAllowed = allowedTitles.some((title) =>
lowered.includes(title.toLowerCase()),
)
if (mentionsAllowed) return text
return hasUpsellCue
? "I can help with details for this item. For add-on recommendations, I can connect you with a specialist."
: text
}
function ticketHandoffText(module: L1Module, storeName: string): string {
return [
`Thanks for reaching out to ${storeName}.`,
@ -422,11 +471,12 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
const modules = parseAiModulesJson(input.aiModulesJson)
const sharedAnswers = parseSharedFieldAnswersJson(input.sharedFieldAnswersJson)
const retrieval = await getBlendedStorefrontContext({
let retrieval = await getBlendedStorefrontContext({
storeId: input.storeId,
query: effectiveMessage,
includeProducts: false,
})
const kbContext =
let kbContext =
retrieval.items.length > 0
? retrieval.items
.map((i, idx) => `[${idx + 1}] (${i.source}) ${i.title}: ${i.content}`)
@ -471,6 +521,15 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
input.categoryMetadetailsEnabled !== null
? Boolean(input.categoryMetadetailsEnabled)
: isModuleEnabled(modules, "CATEGORY_METADETAILS")
const runProductSearchBase =
(activeModule === "PRODUCT_FIT" &&
isAutomationEnabled(modules, activeModule, "knowledge")) ||
(activeModule === "INVENTORY_STOCK" && isModuleEnabled(modules, activeModule))
const allowUpsell =
activeModule === "PRODUCT_FIT" &&
isAutomationEnabled(modules, activeModule, "knowledge")
let allowedUpsellProducts: Awaited<ReturnType<typeof getLinkedUpsellProducts>> = []
let allowedUpsellIds: string[] = []
let customerEmail: string | null = null
if (input.channel === "email") {
@ -713,6 +772,7 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
}
let orderFacts = ""
const orderProductIdsFromDetail: string[] = []
if (
customerEmail &&
ORDER_DETAIL_MODULES.includes(activeModule) &&
@ -746,6 +806,9 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
: "Tracking: not available in our cache yet.",
`Line items (JSON): ${JSON.stringify(detail.lineItems).slice(0, 4000)}`,
]
orderProductIdsFromDetail.push(
...extractShopifyProductIdsFromLineItems(detail.lineItems),
)
if (activeModule === "ORDER_CANCEL") {
lines.push(
"Context: cancellation question — use only the statuses above; do not claim the order was cancelled unless those statuses support it.",
@ -765,17 +828,12 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
let productFacts = ""
let productHits: ProductSearchHit[] = []
const runProductSearch =
(activeModule === "PRODUCT_FIT" &&
isAutomationEnabled(modules, activeModule, "knowledge")) ||
(activeModule === "INVENTORY_STOCK" && isModuleEnabled(modules, activeModule)) ||
(input.channel === "widget" &&
retrieval.productCount > 0 &&
!ORDER_DETAIL_MODULES.includes(activeModule))
if (runProductSearch) {
const productCandidateIds: string[] = []
if (runProductSearchBase) {
const hits = await searchProductsForStore(input.storeId, effectiveMessage, 5)
productHits = hits
usedTools.push("search_products")
productCandidateIds.push(...hits.map((h) => h.shopifyProductId))
if (hits.length) {
productFacts = hits
.map(
@ -795,6 +853,45 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
}
}
const upsellSeedIds = dedupe([...orderProductIdsFromDetail, ...productCandidateIds])
if (allowUpsell && upsellSeedIds.length > 0) {
allowedUpsellProducts = await getLinkedUpsellProducts({
storeId: input.storeId,
sourceProductIds: upsellSeedIds,
query: input.message,
limit: 6,
})
allowedUpsellIds = allowedUpsellProducts.map((p) => p.shopifyProductId)
if (STRICT_UPSELL_LINKS && allowedUpsellProducts.length > 0) {
usedTools.push("upsell_links")
productFacts = [
productFacts,
[
"Allowed recommendation candidates (linked products only):",
...allowedUpsellProducts.map(
(p) => `- ${p.title} (SKU ${p.sku ?? "n/a"}, ID ${p.shopifyProductId})`,
),
].join("\n"),
]
.filter(Boolean)
.join("\n\n")
}
}
retrieval = await getBlendedStorefrontContext({
storeId: input.storeId,
query: input.message,
includeProducts: INCLUDE_PRODUCT_RETRIEVAL_DEFAULT && allowUpsell,
allowedProductIds:
STRICT_UPSELL_LINKS && allowUpsell ? allowedUpsellIds : undefined,
})
kbContext =
retrieval.items.length > 0
? retrieval.items
.map((i, idx) => `[${idx + 1}] (${i.source}) ${i.title}: ${i.content}`)
.join("\n")
: ""
const facts = [
type1Facts,
kbContext ? `Knowledge:\n${kbContext}` : "",
@ -810,6 +907,10 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
message: effectiveMessage,
history: historyToGroq(input.history),
})
const safeText = sanitizeUpsellText(
text,
allowedUpsellProducts.map((p) => p.title),
)
const actions: StorefrontChatAction[] = []
let pendingCartProduct: StorefrontActionProduct | null = null
@ -838,10 +939,12 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
module: activeModule,
pillar,
usedTools,
allowedUpsellCount: allowedUpsellProducts.length,
allowedUpsellProductIds: allowedUpsellProducts.map((p) => p.shopifyProductId),
})
return {
text,
text: safeText,
actions,
pendingCartProduct,
meta: {
@ -850,6 +953,7 @@ export async function runL1Orchestrator(input: RunL1Input): Promise<RunL1Result>
usedTools,
kbHits: retrieval.kbCount,
productHits: retrieval.productCount,
allowedUpsellCount: allowedUpsellProducts.length,
},
}
}

View File

@ -55,4 +55,5 @@ export type L1RunMeta = {
usedTools: string[]
kbHits?: number
productHits?: number
allowedUpsellCount?: number
}

View File

@ -56,6 +56,7 @@ export async function generateStorefrontReply(params: {
queryLength: query.length,
kbHits: result.meta.kbHits,
productHits: result.meta.productHits,
allowedUpsellCount: result.meta.allowedUpsellCount ?? 0,
latencyMs: Date.now() - startedAt,
module: result.meta.module,
pillar: result.meta.pillar,
@ -68,6 +69,7 @@ export async function generateStorefrontReply(params: {
meta: {
kbHits: result.meta.kbHits,
productHits: result.meta.productHits,
allowedUpsellCount: result.meta.allowedUpsellCount,
fallbackUsed: false,
module: result.meta.module,
pillar: result.meta.pillar,

View File

@ -0,0 +1,33 @@
import { describe, expect, it } from "vitest"
import {
isProductStatusSellable,
parseLinkedProductIdsFromMetafields,
tokenizeSearchTerms,
} from "@/lib/ai/tools/productCatalog"
describe("productCatalog helpers", () => {
it("treats active and published statuses as sellable", () => {
expect(isProductStatusSellable("active")).toBe(true)
expect(isProductStatusSellable("ACTIVE")).toBe(true)
expect(isProductStatusSellable("published")).toBe(true)
expect(isProductStatusSellable("draft")).toBe(false)
expect(isProductStatusSellable(null)).toBe(false)
})
it("parses linked product ids from juno metafields", () => {
const ids = parseLinkedProductIdsFromMetafields([
{ namespace: "juno", key: "upsell_links", value: "gid://shopify/Product/1, gid://shopify/Product/2" },
{ namespace: "other", key: "upsell_links", value: "ignored" },
])
expect(ids).toEqual(["gid://shopify/Product/1", "gid://shopify/Product/2"])
})
it("tokenizes search terms consistently", () => {
expect(tokenizeSearchTerms("Need USB-C charger for iPhone 15", 4)).toEqual([
"need",
"usb",
"charger",
"for",
])
})
})

View File

@ -0,0 +1,118 @@
export type CachedProductLite = {
shopifyProductId: string
title: string
vendor: string | null
productType: string | null
status: string | null
variants: unknown
metafields: unknown
}
export function tokenizeSearchTerms(text: string, maxTerms = 8): string[] {
return text
.toLowerCase()
.split(/[^a-z0-9]+/g)
.map((t) => t.trim())
.filter((t) => t.length >= 2)
.slice(0, maxTerms)
}
export function scoreByTerms(text: string, terms: string[]): number {
if (!text || terms.length === 0) return 0
const hay = text.toLowerCase()
let score = 0
for (const t of terms) {
if (hay.includes(t)) score += 1
}
return score
}
export function compactText(value: string, max = 420): string {
if (value.length <= max) return value
return `${value.slice(0, max - 1)}...`
}
export function isProductStatusSellable(status: string | null | undefined): boolean {
const normalized = String(status || "")
.trim()
.toLowerCase()
return normalized === "active" || normalized === "published"
}
export function metafieldsToSummary(metafields: unknown, maxValueLength = 240): string {
if (!Array.isArray(metafields) || metafields.length === 0) return ""
const parts: string[] = []
for (const row of metafields) {
if (!row || typeof row !== "object") continue
const r = row as { namespace?: string; key?: string; value?: unknown }
const key = typeof r.key === "string" ? r.key.trim() : ""
const value = r.value
const rendered = value == null ? "" : String(value).trim()
if (!key || !rendered) continue
const ns = typeof r.namespace === "string" && r.namespace.trim() ? `${r.namespace.trim()}.` : ""
parts.push(`${ns}${key}: ${rendered.slice(0, maxValueLength)}`)
}
return parts.join(" | ")
}
export function primarySkuFromVariants(variants: unknown): string | null {
try {
const parsed = typeof variants === "string" ? JSON.parse(variants) : variants
if (!Array.isArray(parsed) || parsed.length === 0) return null
const first = parsed[0] as { sku?: unknown }
const sku = first?.sku == null ? "" : String(first.sku).trim()
return sku || null
} catch {
return null
}
}
export function inventorySummaryFromVariants(variants: unknown): string | null {
try {
const parsed = typeof variants === "string" ? JSON.parse(variants) : variants
if (!Array.isArray(parsed) || parsed.length === 0) return null
const first = parsed[0] as { inventory_quantity?: unknown; sku?: unknown }
const qty = typeof first?.inventory_quantity === "number" ? first.inventory_quantity : null
const sku = first?.sku == null ? "" : String(first.sku).trim()
if (qty == null && !sku) return null
return [sku ? `SKU ${sku}` : null, qty != null ? `qty ${qty}` : null]
.filter(Boolean)
.join(", ")
} catch {
return null
}
}
export function buildCatalogSearchText(product: CachedProductLite): string {
return [
product.title || "",
product.vendor || "",
product.productType || "",
primarySkuFromVariants(product.variants) || "",
JSON.stringify(product.variants || ""),
metafieldsToSummary(product.metafields, 300),
]
.filter(Boolean)
.join(" ")
}
export function parseLinkedProductIdsFromMetafields(metafields: unknown): string[] {
if (!Array.isArray(metafields) || metafields.length === 0) return []
const out = new Set<string>()
for (const row of metafields) {
if (!row || typeof row !== "object") continue
const r = row as { namespace?: unknown; key?: unknown; value?: unknown }
const ns = typeof r.namespace === "string" ? r.namespace.toLowerCase().trim() : ""
const key = typeof r.key === "string" ? r.key.toLowerCase().trim() : ""
const value = typeof r.value === "string" ? r.value.trim() : ""
const looksLinkedField =
ns === "juno" &&
(key === "upsell_links" || key === "related_product_ids" || key === "upsell_product_ids")
if (!looksLinkedField || !value) continue
for (const token of value.split(/[,|\n]/g)) {
const id = token.trim()
if (id) out.add(id)
}
}
return Array.from(out)
}

View File

@ -116,7 +116,6 @@ export function productSearchHitToActionProduct(hit: ProductSearchHit): Storefro
quantity: 1,
}
}
/**
* Keyword search by title / SKU across cached products (MVP; no full-text index).
*/
@ -133,7 +132,7 @@ export async function searchProductsForStore(
const products = await prisma.cachedProduct.findMany({
where: { storeId },
orderBy: { updatedAt: "desc" },
take: 400,
take: 2000,
select: {
shopifyProductId: true,
title: true,
@ -217,7 +216,7 @@ export async function getProductMetafieldsForL1(
where: { storeId, shopifyProductId },
select: { metafields: true },
})
return { metafieldsSummary: metafieldsToSummary(row?.metafields) }
return { metafieldsSummary: metafieldsToSummary(row?.metafields) || null }
} catch (e) {
logger.error("getProductMetafieldsForL1 failed", e)
return { metafieldsSummary: null }

View File

@ -0,0 +1,116 @@
import { prisma } from "@/lib/prisma"
import {
buildCatalogSearchText,
isProductStatusSellable,
parseLinkedProductIdsFromMetafields,
scoreByTerms,
tokenizeSearchTerms,
} from "@/lib/ai/tools/productCatalog"
export type UpsellAllowedProduct = {
shopifyProductId: string
title: string
sku: string | null
}
function firstSku(variants: unknown): string | null {
try {
const parsed = typeof variants === "string" ? JSON.parse(variants) : variants
if (!Array.isArray(parsed) || parsed.length === 0) return null
const value = parsed[0]?.sku
const sku = value == null ? "" : String(value).trim()
return sku || null
} catch {
return null
}
}
function uniquePreserveOrder(values: string[]): string[] {
const out: string[] = []
const seen = new Set<string>()
for (const value of values) {
if (!value || seen.has(value)) continue
seen.add(value)
out.push(value)
}
return out
}
export async function getLinkedUpsellProducts(params: {
storeId: string
sourceProductIds: string[]
query: string
limit?: number
}): Promise<UpsellAllowedProduct[]> {
const { storeId, sourceProductIds, query, limit = 6 } = params
const sources = uniquePreserveOrder(sourceProductIds)
if (!sources.length) return []
const links = await prisma.upsellProductLink.findMany({
where: {
storeId,
sourceShopifyProductId: { in: sources },
isActive: true,
},
select: { targetShopifyProductId: true },
take: 200,
})
const linkedFromDb = links.map((row) => row.targetShopifyProductId)
const sourceProducts = await prisma.cachedProduct.findMany({
where: {
storeId,
shopifyProductId: { in: sources },
},
select: {
metafields: true,
},
})
const linkedFromMetafields = sourceProducts.flatMap((p) =>
parseLinkedProductIdsFromMetafields(p.metafields),
)
const targetIds = uniquePreserveOrder([...linkedFromDb, ...linkedFromMetafields]).slice(0, 200)
if (!targetIds.length) return []
const targetProducts = await prisma.cachedProduct.findMany({
where: {
storeId,
shopifyProductId: { in: targetIds },
},
select: {
shopifyProductId: true,
title: true,
status: true,
variants: true,
vendor: true,
productType: true,
metafields: true,
},
})
const sellable = targetProducts.filter((p) => isProductStatusSellable(p.status))
const terms = tokenizeSearchTerms(query, 8)
if (terms.length === 0) {
return sellable.slice(0, limit).map((p) => ({
shopifyProductId: p.shopifyProductId,
title: p.title,
sku: firstSku(p.variants),
}))
}
return sellable
.map((p) => ({
product: p,
score: scoreByTerms(buildCatalogSearchText(p), terms),
}))
.sort((a, b) => b.score - a.score)
.slice(0, limit)
.map(({ product }) => ({
shopifyProductId: product.shopifyProductId,
title: product.title,
sku: firstSku(product.variants),
}))
}

View File

@ -6,6 +6,8 @@ type SendGmailReplyOptions = {
storeId?: string | null
isAutomated?: boolean
trigger?: string
metadata?: unknown
storefrontConversationId?: string | null
/**
* Original RFC 2822 Message-ID header of the email we are replying to.
* Used for proper threading in recipients' inboxes via In-Reply-To/References.
@ -98,6 +100,8 @@ export async function sendGmailReply(
to,
subject,
bodyPreview,
metadata: options.metadata,
storefrontConversationId: options.storefrontConversationId ?? null,
isAutomated: options.isAutomated ?? false,
trigger: options.trigger,
}

View File

@ -1,4 +1,12 @@
import { prisma } from "@/lib/prisma"
import {
compactText,
isProductStatusSellable,
metafieldsToSummary,
primarySkuFromVariants,
scoreByTerms,
tokenizeSearchTerms,
} from "@/lib/ai/tools/productCatalog"
type RetrievedContextItem = {
source: "KB" | "FAQ" | "PRODUCT"
@ -7,56 +15,28 @@ type RetrievedContextItem = {
score: number
}
function tokenize(text: string) {
return text
.toLowerCase()
.split(/[^a-z0-9]+/g)
.map((t) => t.trim())
.filter((t) => t.length >= 2)
type ProductRow = {
shopifyProductId?: string
title?: string
vendor?: string | null
productType?: string | null
status?: string | null
variants?: unknown
metafields?: unknown
}
function scoreByTerms(text: string, terms: string[]) {
if (!text || !terms.length) return 0
const hay = text.toLowerCase()
let score = 0
for (const t of terms) {
if (hay.includes(t)) score += 1
}
return score
}
function compact(value: string, max = 420) {
if (value.length <= max) return value
return `${value.slice(0, max - 1)}...`
}
function metafieldsForText(metafields: unknown): string {
if (!Array.isArray(metafields) || metafields.length === 0) return ""
const parts: string[] = []
for (const row of metafields) {
if (!row || typeof row !== "object") continue
const r = row as { namespace?: string; key?: string; value?: string | null }
const k = r.key
const v = r.value
if (!k || v === null || v === undefined || String(v).trim() === "") continue
const ns = r.namespace ? `${r.namespace}.` : ""
parts.push(`${ns}${k}: ${String(v).slice(0, 200)}`)
}
return parts.join(" | ")
}
function productPreview(product: any) {
function productPreview(product: ProductRow) {
const variants = Array.isArray(product?.variants) ? product.variants : []
const firstVariant = variants[0] || {}
const price = firstVariant?.price ?? null
const sku = firstVariant?.sku ?? null
const sku = primarySkuFromVariants(firstVariant ? [firstVariant] : product?.variants)
const weight =
firstVariant?.weight !== undefined && firstVariant?.weight !== null
? `${String(firstVariant.weight)}${firstVariant.weight_unit ? ` ${firstVariant.weight_unit}` : ""}`
: ""
const inventory = firstVariant?.inventory_quantity ?? null
const status = product?.status || "unknown"
const mfLine = metafieldsForText(product?.metafields)
const mfLine = metafieldsToSummary(product?.metafields)
const bits = [
`Title: ${product?.title || "Unknown"}`,
@ -78,13 +58,13 @@ export async function getBlendedStorefrontContext(params: {
query: string
topKb?: number
topProducts?: number
includeProducts?: boolean
allowedProductIds?: string[]
}) {
const { storeId, query, topKb = 6, topProducts = 4 } = params
const terms = tokenize(query).slice(0, 10)
const { storeId, query, topKb = 6, topProducts = 4, includeProducts = true } = params
const terms = tokenizeSearchTerms(query, 10)
const p = prisma as any
const kbChunks = await p.knowledgeChunk.findMany({
const kbChunks = await prisma.knowledgeChunk.findMany({
where: { storeId },
orderBy: { createdAt: "desc" },
take: 120,
@ -97,12 +77,12 @@ export async function getBlendedStorefrontContext(params: {
})
const rankedKb: RetrievedContextItem[] = (kbChunks || [])
.map((c: any) => {
.map((c) => {
const score = scoreByTerms(String(c?.content || ""), terms)
return {
source: c?.sourceType === "FAQ" ? "FAQ" : "KB",
title: c?.sourceType === "FAQ" ? "FAQ entry" : "Knowledge document",
content: compact(String(c?.content || "")),
content: compactText(String(c?.content || "")),
score,
} as RetrievedContextItem
})
@ -110,35 +90,45 @@ export async function getBlendedStorefrontContext(params: {
.sort((a: RetrievedContextItem, b: RetrievedContextItem) => b.score - a.score)
.slice(0, topKb)
const products = await p.cachedProduct.findMany({
where: { storeId },
orderBy: { updatedAt: "desc" },
take: 200,
select: {
title: true,
vendor: true,
productType: true,
status: true,
variants: true,
images: true,
metafields: true,
},
})
let rankedProducts: RetrievedContextItem[] = []
if (includeProducts) {
const allowedIds = Array.isArray(params.allowedProductIds)
? params.allowedProductIds.filter(Boolean)
: []
if (allowedIds.length > 0) {
const products = await prisma.cachedProduct.findMany({
where: {
storeId,
shopifyProductId: { in: allowedIds.slice(0, 120) },
},
select: {
shopifyProductId: true,
title: true,
vendor: true,
productType: true,
status: true,
variants: true,
images: true,
metafields: true,
},
})
const rankedProducts: RetrievedContextItem[] = (products || [])
.map((prod: any) => {
const text = `${prod?.title || ""} ${prod?.vendor || ""} ${prod?.productType || ""} ${metafieldsForText(prod?.metafields)}`
const score = scoreByTerms(text, terms)
return {
source: "PRODUCT",
title: prod?.title || "Product",
content: compact(productPreview(prod)),
score,
} as RetrievedContextItem
})
.filter((x: RetrievedContextItem) => x.score > 0)
.sort((a: RetrievedContextItem, b: RetrievedContextItem) => b.score - a.score)
.slice(0, topProducts)
rankedProducts = (products || [])
.filter((prod) => isProductStatusSellable(prod?.status))
.map((prod) => {
const text = `${prod?.title || ""} ${prod?.vendor || ""} ${prod?.productType || ""} ${metafieldsToSummary(prod?.metafields)}`
const score = scoreByTerms(text, terms)
return {
source: "PRODUCT",
title: prod?.title || "Product",
content: compactText(productPreview(prod)),
score,
} as RetrievedContextItem
})
.sort((a: RetrievedContextItem, b: RetrievedContextItem) => b.score - a.score)
.slice(0, topProducts)
}
}
return {
items: [...rankedKb, ...rankedProducts],

View File

@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest"
import {
decodeHtmlEntities,
normalizeSubject,
parseTicketId,
} from "@/lib/tickets/emailLogTickets"
describe("emailLog ticket helpers", () => {
it("normalizes nested reply prefixes", () => {
expect(normalizeSubject("Re: Fwd: re: Shipping delay")).toBe("Shipping delay")
})
it("decodes html entities in previews", () => {
expect(decodeHtmlEntities("Tom &amp; Jerry &#39;hello&#39;")).toBe(
"Tom & Jerry 'hello'",
)
})
it("parses ticket id into email and root subject", () => {
expect(parseTicketId("customer@example.com::Order 1002")).toEqual({
customerEmail: "customer@example.com",
rootSubject: "Order 1002",
})
expect(parseTicketId("invalid")).toBeNull()
})
})

View File

@ -0,0 +1,165 @@
import { prisma } from "@/lib/prisma"
export type TicketMessage = {
id: string
subject: string
bodyPreview: string | null
status: string
trigger: string | null
sentAt: string
metadata?: unknown
storefrontConversationId?: string | null
}
export type TicketThread = {
id: string
subject: string
rootSubject: string
customerEmail: string
status: string
updatedAt: string
messagesCount: number
lastMessageSnippet: string
messages: TicketMessage[]
}
export function normalizeSubject(subject: string | null): string {
if (!subject) return "No subject"
let s = subject.trim()
while (true) {
const match = s.match(/^(re|fw|fwd):\s*/i)
if (!match) break
s = s.slice(match[0].length).trim()
}
return s || "No subject"
}
export function decodeHtmlEntities(input: string | null | undefined): string {
const text = (input || "").trim()
if (!text) return ""
return text
.replace(/&#(\d+);/g, (_, n: string) => {
const code = Number.parseInt(n, 10)
return Number.isFinite(code) ? String.fromCharCode(code) : _
})
.replace(/&#x([0-9a-fA-F]+);/g, (_, hex: string) => {
const code = Number.parseInt(hex, 16)
return Number.isFinite(code) ? String.fromCharCode(code) : _
})
.replace(/&quot;/g, '"')
.replace(/&#39;/g, "'")
.replace(/&apos;/g, "'")
.replace(/&amp;/g, "&")
.replace(/&lt;/g, "<")
.replace(/&gt;/g, ">")
}
export function parseTicketId(ticketId: string): {
customerEmail: string
rootSubject: string
} | null {
const splitAt = ticketId.indexOf("::")
if (splitAt < 0) return null
const customerEmail = ticketId.slice(0, splitAt).trim().toLowerCase()
const rootSubject = ticketId.slice(splitAt + 2).trim()
if (!customerEmail || !rootSubject) return null
return { customerEmail, rootSubject }
}
type RawEmailLog = {
id: string
to: string
subject: string | null
bodyPreview: string | null
status: string
trigger: string | null
sentAt: Date
metadata: unknown
storefrontConversationId: string | null
}
async function getTicketLogsForStore(storeId: string): Promise<RawEmailLog[]> {
return prisma.$queryRaw<RawEmailLog[]>`
SELECT
"id",
"to",
"subject",
"bodyPreview",
"status",
"trigger",
"sentAt",
"metadata",
"storefrontConversationId"
FROM "EmailLog"
WHERE "storeId" = ${storeId}
AND (
"isAutomated" = TRUE
OR "trigger" = 'GMAIL_INCOMING'
OR "trigger" = 'TICKET_CUSTOMER_REPLY'
)
ORDER BY "sentAt" DESC
LIMIT 4000
`
}
function buildThreads(logs: RawEmailLog[]): TicketThread[] {
const map = new Map<string, TicketThread>()
for (const log of logs) {
const customerEmail = (log.to || "").toLowerCase()
const rootSubject = normalizeSubject(log.subject)
const key = `${customerEmail}::${rootSubject}`
const message: TicketMessage = {
id: log.id,
subject: log.subject || rootSubject,
bodyPreview: decodeHtmlEntities(log.bodyPreview),
status: log.status,
trigger: log.trigger,
sentAt: log.sentAt.toISOString(),
metadata: log.metadata,
storefrontConversationId: log.storefrontConversationId,
}
const existing = map.get(key)
if (!existing) {
map.set(key, {
id: key,
subject: log.subject || rootSubject,
rootSubject,
customerEmail,
status: log.status,
updatedAt: log.sentAt.toISOString(),
messagesCount: 1,
lastMessageSnippet: decodeHtmlEntities(log.bodyPreview),
messages: [message],
})
continue
}
existing.messages.push(message)
existing.messagesCount += 1
}
return Array.from(map.values())
}
export async function listTicketThreadsForStore(storeId: string): Promise<TicketThread[]> {
const logs = await getTicketLogsForStore(storeId)
return buildThreads(logs).filter((t) => {
const latest = t.messages[0]
const storefrontEscalationThread = t.messages.some(
(m) => m.trigger === "STOREFRONT_TICKET",
)
const resolvedByVendorEmail = latest?.trigger === "TICKET_CUSTOMER_REPLY"
if (storefrontEscalationThread && resolvedByVendorEmail) {
return false
}
return true
})
}
export async function getTicketThreadForStore(
storeId: string,
ticketId: string,
): Promise<TicketThread | null> {
const parsed = parseTicketId(ticketId)
if (!parsed) return null
const threads = await listTicketThreadsForStore(storeId)
return threads.find((t) => t.id === `${parsed.customerEmail}::${parsed.rootSubject}`) || null
}