diff --git a/salesflow-saas/AGENTS.md b/salesflow-saas/AGENTS.md
index c373d6fa..5d802d3a 100644
--- a/salesflow-saas/AGENTS.md
+++ b/salesflow-saas/AGENTS.md
@@ -13,7 +13,7 @@
- FastAPI 0.115.6 on Python 3.12
- SQLAlchemy 2.0 async with PostgreSQL 16
- Celery 5.x with Redis broker
-- JWT authentication (python-jose)
+- JWT authentication (PyJWT)
- Multi-tenant data isolation via `tenant_id`
### Frontend (`salesflow-saas/frontend/`)
diff --git a/salesflow-saas/README.md b/salesflow-saas/README.md
index 05f2ea34..51260773 100644
--- a/salesflow-saas/README.md
+++ b/salesflow-saas/README.md
@@ -26,6 +26,12 @@ docker-compose up --build
Backend: `http://localhost:8000/docs`
Frontend: `http://localhost:3000`
+**If the browser shows connection refused on `:3000` or `:8000`:** nothing is listening on that port yet. Start the stack (`docker compose up` from this folder) or run `uvicorn` / `npm run dev` manually. Confirm with `curl -sSf http://127.0.0.1:8000/api/v1/health` and ensure the browser is on the same machine as the server (not WSL/remote without port forwarding).
+
+**Without Docker:** install Python 3.12+ and Node 22+, copy `.env` and `frontend/.env.local`, run Postgres/Redis (or point `DATABASE_URL` / `REDIS_URL` at existing instances), then `cd backend && uvicorn app.main:app --reload --host 0.0.0.0 --port 8000` and `cd frontend && npm run dev`.
+
+**E2E locally:** after `npm ci`, run `npx playwright install chromium` once, then `npm run test:e2e` (matches CI).
+
**Customer onboarding (B2B):** `GET /api/v1/customer-onboarding/journey` and `docs/CUSTOMER_OS_ONBOARDING_AR.md`. Dashboard tab: **مسار التشغيل مع العميل**.
**Launch verification:** see `docs/LAUNCH_CHECKLIST.md`. From `salesflow-saas`: copy `frontend/.env.example` to `frontend/.env.local` and set `NEXT_PUBLIC_API_URL`. Run `.\verify-launch.ps1 -HttpCheck -SoftReady` (use `-BaseUrl` if the API is not on port 8000).
diff --git a/salesflow-saas/backend/app/agents/memory_layer.py b/salesflow-saas/backend/app/agents/memory_layer.py
index e2d61c28..cc32582f 100644
--- a/salesflow-saas/backend/app/agents/memory_layer.py
+++ b/salesflow-saas/backend/app/agents/memory_layer.py
@@ -4,17 +4,23 @@ import json
try:
from mem0 import Memory
+ from mem0.configs.base import MemoryConfig
except ImportError:
- # Fallback mock for testing environments where mem0ai isn't available yet
- class Memory:
- def __init__(self, config=None):
- self.store = []
-
- def search(self, query: str, user_id: str, **kwargs):
- return [{"text": "Mocked memory context."}]
-
- def add(self, text: str, user_id: str, metadata: dict = None, **kwargs):
- self.store.append({"text": text, "user_id": user_id, "metadata": metadata})
+ Memory = None # type: ignore[misc, assignment]
+ MemoryConfig = None # type: ignore[misc, assignment]
+
+
+class _MockMemory:
+ """Used when mem0 is unavailable or cannot initialize (CI, missing API keys)."""
+
+ def __init__(self, config=None):
+ self.store = []
+
+ def search(self, query: str, user_id: str, **kwargs):
+ return [{"text": "Mocked memory context."}]
+
+ def add(self, text: str, user_id: str, metadata: dict = None, **kwargs):
+ self.store.append({"text": text, "user_id": user_id, "metadata": metadata})
class SelfHealingMemory:
"""
@@ -33,7 +39,12 @@ class SelfHealingMemory:
}
}
}
- self.memory = Memory(config=self.config)
+ self.memory = _MockMemory()
+ if Memory is not None and MemoryConfig is not None:
+ try:
+ self.memory = Memory(config=MemoryConfig.model_validate(self.config))
+ except Exception:
+ self.memory = _MockMemory()
def get_context(self, company_name: str, context_type: str = "general") -> str:
"""
diff --git a/salesflow-saas/backend/app/models/sequence.py b/salesflow-saas/backend/app/models/sequence.py
index 382482c6..05d977dd 100644
--- a/salesflow-saas/backend/app/models/sequence.py
+++ b/salesflow-saas/backend/app/models/sequence.py
@@ -101,7 +101,7 @@ class SequenceEvent(BaseModel):
channel = Column(String(50), nullable=False)
status = Column(String(50), nullable=False, default=SequenceEventStatus.SENT.value)
sent_at = Column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
- metadata = Column(JSONB, default=dict)
+ event_metadata = Column("metadata", JSONB, default=dict)
enrollment = relationship("SequenceEnrollment", back_populates="events")
step = relationship("SequenceStep", back_populates="events")
diff --git a/salesflow-saas/backend/app/services/auth_service.py b/salesflow-saas/backend/app/services/auth_service.py
index 508d96e1..93211d8b 100644
--- a/salesflow-saas/backend/app/services/auth_service.py
+++ b/salesflow-saas/backend/app/services/auth_service.py
@@ -8,7 +8,8 @@ from datetime import datetime, timedelta, timezone
from typing import Optional
from uuid import UUID
-from jose import JWTError, jwt
+import jwt
+from jwt.exceptions import PyJWTError
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -75,7 +76,7 @@ class AuthService:
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]
)
return payload
- except JWTError:
+ except PyJWTError:
return None
# ── OTP ───────────────────────────────────────
diff --git a/salesflow-saas/backend/app/services/sequence_engine.py b/salesflow-saas/backend/app/services/sequence_engine.py
index 798a5583..09a33fa9 100644
--- a/salesflow-saas/backend/app/services/sequence_engine.py
+++ b/salesflow-saas/backend/app/services/sequence_engine.py
@@ -216,7 +216,7 @@ class SequenceEngine:
self.db.add(SequenceEvent(
enrollment_id=enrollment.id, step_id=step.id, channel=step.channel,
status=SequenceEventStatus.FAILED.value,
- metadata={"reason": "no_consent", "detail": cr.message},
+ event_metadata={"reason": "no_consent", "detail": cr.message},
))
enrollment.status = SequenceStatus.STOPPED.value
await self.db.flush()
@@ -226,7 +226,7 @@ class SequenceEngine:
self.db.add(SequenceEvent(
enrollment_id=enrollment.id, step_id=step.id, channel=step.channel,
status=SequenceEventStatus.SENT.value,
- metadata={"variant": step.variant, "preview": step.template_content[:80]},
+ event_metadata={"variant": step.variant, "preview": step.template_content[:80]},
))
enrollment.current_step = idx + 1
if enrollment.current_step >= len(steps):
diff --git a/salesflow-saas/backend/app/utils/security.py b/salesflow-saas/backend/app/utils/security.py
index a3db1c53..0ed616b4 100644
--- a/salesflow-saas/backend/app/utils/security.py
+++ b/salesflow-saas/backend/app/utils/security.py
@@ -2,7 +2,9 @@ from datetime import datetime, timedelta, timezone
from typing import Optional
import bcrypt
-from jose import JWTError, jwt
+import jwt
+
+from jwt.exceptions import PyJWTError
from app.config import get_settings
@@ -46,5 +48,5 @@ def decode_token(token: str) -> Optional[dict]:
try:
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
return payload
- except JWTError:
+ except PyJWTError:
return None
diff --git a/salesflow-saas/backend/app/workers/sequence_tasks.py b/salesflow-saas/backend/app/workers/sequence_tasks.py
index df2680bd..52d41bf2 100644
--- a/salesflow-saas/backend/app/workers/sequence_tasks.py
+++ b/salesflow-saas/backend/app/workers/sequence_tasks.py
@@ -92,7 +92,7 @@ def process_pending_sequences(self):
channel=step.channel,
status="failed",
sent_at=datetime.now(timezone.utc),
- metadata={"reason": "pdpl_consent_missing"},
+ event_metadata={"reason": "pdpl_consent_missing"},
)
db.add(event)
enrollment.current_step += 1
@@ -181,7 +181,7 @@ def execute_sequence_step(self, enrollment_id, step_id, lead_id, channel, conten
channel=channel,
status="sent" if success else "failed",
sent_at=datetime.now(timezone.utc),
- metadata={"content_preview": content[:100] if content else ""},
+ event_metadata={"content_preview": content[:100] if content else ""},
)
db.add(event)
diff --git a/salesflow-saas/backend/requirements.txt b/salesflow-saas/backend/requirements.txt
index 8b69be6d..8e8a34cf 100644
--- a/salesflow-saas/backend/requirements.txt
+++ b/salesflow-saas/backend/requirements.txt
@@ -1,8 +1,8 @@
# === Core Framework ===
fastapi==0.115.5
uvicorn[standard]==0.32.1
-pydantic==2.9.2
-pydantic-settings==2.6.1
+pydantic>=2.10.0,<3
+pydantic-settings>=2.10.1,<3
pydantic-extra-types[phonenumbers]>=2.0.0 # Saudi phone validation (+966)
python-multipart==0.0.12
@@ -14,17 +14,17 @@ alembic==1.14.0
pgvector==0.3.6
# === AI / LLM Providers ===
-litellm>=1.40.0 # Unified LLM provider (Groq/OpenAI/Claude/Gemini) with fallback
+litellm>=1.74.0,<2 # httpx>=0.28 compatible (older litellm capped httpx<0.28)
instructor>=1.14.0 # Structured LLM outputs via Pydantic models
groq==0.12.0
-openai==1.57.0
-langchain==0.3.9
+openai>=2.8.0,<3 # litellm 1.8x+ requires openai>=2.8; mem0ai 1.x supports it
+langchain==0.3.28
langchain-groq==0.2.1
-langchain-community==0.3.9
+langchain-community==0.3.28
langchain-anthropic==0.2.0
langgraph==0.2.53
-crewai==0.80.0
-mem0ai==0.1.18
+crewai>=0.95.0,<1 # aligns with litellm/langchain stack; 0.80 pulled incompatible crewai-tools
+mem0ai>=1.0.0,<2 # 0.1.x capped openai<2; incompatible with current litellm
# === Arabic NLP ===
camel-tools>=1.5.0 # Arabic morphology, NER, dialect detection (NYU Abu Dhabi)
@@ -35,7 +35,7 @@ pywa>=3.0.0 # Direct WhatsApp Cloud API (async, webhooks, templ
twilio==9.3.7 # Twilio fallback
# === Communication ===
-httpx==0.27.2
+httpx>=0.28.1,<0.29.0
resend>=2.0.0 # Transactional email API (free tier, FastAPI-native)
# === Saudi-specific ===
@@ -73,10 +73,10 @@ statsforecast>=1.7.0 # Fast statistical time-series forecasting
# === Data & Utilities ===
beautifulsoup4==4.12.3
lxml==5.3.0
-requests==2.32.3
+requests>=2.32.5,<3
python-dateutil==2.9.0
pandas==2.2.3
-numpy==2.1.3
+numpy>=1.26.2,<2 # camel-tools requires numpy<2; langchain needs >=1.26.2 on Py3.12
python-decouple==3.8
paramiko==3.5.0
qrcode==8.0
diff --git a/salesflow-saas/frontend/e2e/subscriber-journey.spec.ts b/salesflow-saas/frontend/e2e/subscriber-journey.spec.ts
index c0f91bf2..eb3fba0f 100644
--- a/salesflow-saas/frontend/e2e/subscriber-journey.spec.ts
+++ b/salesflow-saas/frontend/e2e/subscriber-journey.spec.ts
@@ -13,7 +13,7 @@ test.describe("Subscriber journey (public shell)", () => {
test("home shows Dealix value and navigation affordances", async ({ page }) => {
await page.goto("/");
await expect(page.getByText("Dealix", { exact: false }).first()).toBeVisible();
- await expect(page.getByText(/لماذا Dealix/)).toBeVisible();
+ await expect(page.getByText(/هل تواجه هذه التحديات/)).toBeVisible();
});
test("landing page loads CTA toward app", async ({ page }) => {
diff --git a/salesflow-saas/frontend/src/app/dashboard/page.tsx b/salesflow-saas/frontend/src/app/dashboard/page.tsx
index afc12c32..fe8c7e56 100644
--- a/salesflow-saas/frontend/src/app/dashboard/page.tsx
+++ b/salesflow-saas/frontend/src/app/dashboard/page.tsx
@@ -24,6 +24,10 @@ import {
Receipt,
Layers,
LogOut,
+ MousePointerClick,
+ UserCheck,
+ TrendingUp,
+ Crosshair,
} from "lucide-react";
import { DashboardView } from "../../components/dealix/dashboard-view";
@@ -48,6 +52,17 @@ import { PipelineKanban } from "../../components/dealix/pipeline-kanban";
import { UnifiedInbox } from "../../components/dealix/unified-inbox";
import { LeadScoreCard } from "../../components/dealix/lead-score-card";
+const dashboardLeadScoreDemo = {
+ score: 82,
+ breakdown: [
+ { key: "engagement", label: "التفاعل", value: 24, icon: MousePointerClick },
+ { key: "profile", label: "الملف الشخصي", value: 20, icon: UserCheck },
+ { key: "behavior", label: "السلوك", value: 22, icon: TrendingUp },
+ { key: "intent", label: "نية الشراء", value: 16, icon: Crosshair },
+ ],
+ recommendation: "عميل واعد — تابع خلال ٢٤ ساعة",
+};
+
export default function DashboardPage() {
const auth = useRequireAuth();
const [activeTab, setActiveTab] = useState("overview");
@@ -128,7 +143,7 @@ export default function DashboardPage() {
case "inbox":
return