From 8c3d91c0704cd6f54add0fd3dc9e3eafc7019675 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 12 Apr 2026 10:32:05 +0000 Subject: [PATCH] fix(dealix): resolve Python deps, SQLAlchemy metadata, JWT, and frontend CI - Align httpx, litellm, langchain, openai, mem0ai, crewai, numpy, requests, pydantic - Rename SequenceEvent ORM attribute to event_metadata (DB column stays metadata) - Use PyJWT instead of python-jose in security and auth service - Mem0: MemoryConfig + graceful fallback when init fails (CI without keys) - Frontend: I18nProvider in root layout, fix dashboard LeadScoreCard props, Section id, kpi-card useRef, en.json nameAr parity, e2e assertion for premium landing - README: troubleshooting for connection refused and local E2E Playwright install Co-authored-by: VoXc2 --- salesflow-saas/AGENTS.md | 2 +- salesflow-saas/README.md | 6 ++++ .../backend/app/agents/memory_layer.py | 33 ++++++++++++------- salesflow-saas/backend/app/models/sequence.py | 2 +- .../backend/app/services/auth_service.py | 5 +-- .../backend/app/services/sequence_engine.py | 4 +-- salesflow-saas/backend/app/utils/security.py | 6 ++-- .../backend/app/workers/sequence_tasks.py | 4 +-- salesflow-saas/backend/requirements.txt | 22 ++++++------- .../frontend/e2e/subscriber-journey.spec.ts | 2 +- .../frontend/src/app/dashboard/page.tsx | 17 +++++++++- salesflow-saas/frontend/src/app/layout.tsx | 3 +- .../src/components/dealix/premium-landing.tsx | 3 ++ .../frontend/src/components/ui/kpi-card.tsx | 2 +- salesflow-saas/frontend/src/i18n/en.json | 1 + 15 files changed, 76 insertions(+), 36 deletions(-) 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 ; case "scoring": - return ; + return ; case "onboarding": return ; default: diff --git a/salesflow-saas/frontend/src/app/layout.tsx b/salesflow-saas/frontend/src/app/layout.tsx index ec5631ec..89078ec4 100644 --- a/salesflow-saas/frontend/src/app/layout.tsx +++ b/salesflow-saas/frontend/src/app/layout.tsx @@ -1,6 +1,7 @@ import type { Metadata } from "next"; import { Noto_Kufi_Arabic } from "next/font/google"; import "./globals.css"; +import { I18nProvider } from "@/i18n"; const kufi = Noto_Kufi_Arabic({ subsets: ["arabic", "latin"], @@ -26,7 +27,7 @@ export default function RootLayout({
- {children} + {children} ); diff --git a/salesflow-saas/frontend/src/components/dealix/premium-landing.tsx b/salesflow-saas/frontend/src/components/dealix/premium-landing.tsx index 23ae7832..600e5b69 100644 --- a/salesflow-saas/frontend/src/components/dealix/premium-landing.tsx +++ b/salesflow-saas/frontend/src/components/dealix/premium-landing.tsx @@ -40,15 +40,18 @@ const stagger = { function Section({ children, className = "", + id, }: { children: React.ReactNode; className?: string; + id?: string; }) { const ref = useRef(null); const inView = useInView(ref, { once: true, margin: "-60px" }); return ( (); + const frameRef = useRef(undefined); useEffect(() => { const start = performance.now(); diff --git a/salesflow-saas/frontend/src/i18n/en.json b/salesflow-saas/frontend/src/i18n/en.json index b3b009e2..5cab5ef3 100644 --- a/salesflow-saas/frontend/src/i18n/en.json +++ b/salesflow-saas/frontend/src/i18n/en.json @@ -108,6 +108,7 @@ "guarantee": "Full refund within 30 days if not satisfied", "plan": { "name": "Dealix All-in-One", + "nameAr": "ديلكس الشاملة", "priceMonthly": "1,499", "priceYearly": "14,999", "priceYearlySave": "Save 2 months",