From 07557c4be9181813724d2a058584492c39d6ddd5 Mon Sep 17 00:00:00 2001 From: Sami Assiri Date: Mon, 13 Apr 2026 05:08:39 +0300 Subject: [PATCH] feat(dealix): GTM polish, CRM/AI APIs, launch verification hardening - Add integrations CRM and AI routing APIs; Salesforce OAuth refresh; lead CRM metadata - Marketer hub, settings CRM UI, OS views; premium landing and strategy_summary differentiators - Docs: API-MAP, product guide, competitive matrix, launch simulation, AGENT-MAP LLM routing - Sync script: strategy legal + competitive matrix to public; pytest DB isolation (.pytest_dealix.sqlite) - Tests: CRM status and AI routing smoke; check_go_live_gate UTF-8 stdout on Windows - Alembic migrations for strategic deal links and lead company/sector/city Made-with: Cursor --- salesflow-saas/.gitignore | 1 + .../20260413_0002_strategic_deal_crm_links.py | 40 ++ .../20260413_0003_lead_company_sector_city.py | 43 ++ .../backend/app/api/v1/ai_routing.py | 116 ++++ .../backend/app/api/v1/integrations_crm.py | 226 ++++++++ .../backend/app/api/v1/operations.py | 4 + salesflow-saas/backend/app/api/v1/router.py | 4 + .../backend/app/api/v1/strategic_deals.py | 379 ++++++++++++ .../backend/app/api/v1/strategy_summary.py | 50 ++ salesflow-saas/backend/app/models/lead.py | 3 + .../backend/app/models/strategic_deal.py | 6 + .../backend/app/services/crm_sync_service.py | 117 +++- .../app/services/dealix_os/__init__.py | 14 + .../services/dealix_os/partner_archetypes.py | 77 +++ .../app/services/dealix_os/policy_engine.py | 110 ++++ .../services/dealix_os/vertical_playbooks.py | 61 ++ .../backend/app/services/integration_probe.py | 73 +++ .../backend/app/services/lead_service.py | 63 +- .../backend/app/services/operations_hub.py | 1 + .../backend/app/services/salesforce_oauth.py | 53 ++ salesflow-saas/backend/tests/conftest.py | 10 + .../tests/test_integrations_ai_routing_api.py | 80 +++ salesflow-saas/docs/AGENT-MAP.md | 11 + salesflow-saas/docs/API-MAP.md | 59 ++ salesflow-saas/docs/COMPETITIVE_MATRIX_AR.md | 35 ++ .../docs/DEALIX_OS_PRODUCT_GUIDE_AR.md | 39 ++ salesflow-saas/docs/ENTERPRISE_ROADMAP.md | 34 ++ salesflow-saas/docs/LAUNCH_CHECKLIST.md | 5 +- salesflow-saas/docs/LAUNCH_SIMULATION.md | 34 ++ .../dealix-marketing/LOCAL-ONLY-NEXT.txt | 2 + .../dealix-presentations/investor-deck.md | 217 ------- salesflow-saas/frontend/public/favicon.svg | 23 +- salesflow-saas/frontend/public/logo.svg | 42 +- .../public/strategy/COMPETITIVE_MATRIX_AR.md | 35 ++ .../strategy/legal/affiliate-rules-ar.md | 34 ++ .../strategy/legal/commission-policy-ar.md | 37 ++ .../strategy/legal/consent-policy-ar.md | 35 ++ .../strategy/legal/data-protection-ar.md | 36 ++ .../strategy/legal/privacy-policy-ar.md | 57 ++ .../public/strategy/legal/refund-policy-ar.md | 28 + .../strategy/legal/terms-of-service-ar.md | 54 ++ .../frontend/src/app/dashboard/page.tsx | 181 ++++-- salesflow-saas/frontend/src/app/globals.css | 7 + .../frontend/src/app/landing/page.tsx | 4 +- salesflow-saas/frontend/src/app/layout.tsx | 5 + .../frontend/src/app/settings/page.tsx | 330 ++++++++++- .../components/dealix/agent-quality-view.tsx | 79 +++ .../src/components/dealix/dealix-3d-logo.tsx | 37 +- .../dealix/go-live-readiness-card.tsx | 73 +++ .../dealix/governance-metrics-view.tsx | 103 ++++ .../dealix/growth-playbook-view.tsx | 85 +++ .../components/dealix/identity-graph-view.tsx | 119 ++++ .../dealix/intelligence-dashboard.tsx | 10 +- .../components/dealix/lead-generator-view.tsx | 7 +- .../components/dealix/marketer-hub-view.tsx | 171 ++++++ .../dealix/operating-model-view.tsx | 161 ++++++ .../dealix/partnership-studio-view.tsx | 540 ++++++++++++++++++ .../src/components/dealix/premium-landing.tsx | 120 ++-- .../dealix/vertical-playbooks-view.tsx | 106 ++++ salesflow-saas/scripts/check_go_live_gate.py | 6 + .../scripts/sync-marketing-to-public.cjs | 25 + 61 files changed, 4118 insertions(+), 399 deletions(-) create mode 100644 salesflow-saas/backend/alembic/versions/20260413_0002_strategic_deal_crm_links.py create mode 100644 salesflow-saas/backend/alembic/versions/20260413_0003_lead_company_sector_city.py create mode 100644 salesflow-saas/backend/app/api/v1/ai_routing.py create mode 100644 salesflow-saas/backend/app/api/v1/integrations_crm.py create mode 100644 salesflow-saas/backend/app/services/dealix_os/__init__.py create mode 100644 salesflow-saas/backend/app/services/dealix_os/partner_archetypes.py create mode 100644 salesflow-saas/backend/app/services/dealix_os/policy_engine.py create mode 100644 salesflow-saas/backend/app/services/dealix_os/vertical_playbooks.py create mode 100644 salesflow-saas/backend/app/services/integration_probe.py create mode 100644 salesflow-saas/backend/app/services/salesforce_oauth.py create mode 100644 salesflow-saas/backend/tests/test_integrations_ai_routing_api.py create mode 100644 salesflow-saas/docs/COMPETITIVE_MATRIX_AR.md create mode 100644 salesflow-saas/docs/DEALIX_OS_PRODUCT_GUIDE_AR.md create mode 100644 salesflow-saas/docs/ENTERPRISE_ROADMAP.md create mode 100644 salesflow-saas/docs/LAUNCH_SIMULATION.md delete mode 100644 salesflow-saas/frontend/public/dealix-presentations/investor-deck.md create mode 100644 salesflow-saas/frontend/public/strategy/COMPETITIVE_MATRIX_AR.md create mode 100644 salesflow-saas/frontend/public/strategy/legal/affiliate-rules-ar.md create mode 100644 salesflow-saas/frontend/public/strategy/legal/commission-policy-ar.md create mode 100644 salesflow-saas/frontend/public/strategy/legal/consent-policy-ar.md create mode 100644 salesflow-saas/frontend/public/strategy/legal/data-protection-ar.md create mode 100644 salesflow-saas/frontend/public/strategy/legal/privacy-policy-ar.md create mode 100644 salesflow-saas/frontend/public/strategy/legal/refund-policy-ar.md create mode 100644 salesflow-saas/frontend/public/strategy/legal/terms-of-service-ar.md create mode 100644 salesflow-saas/frontend/src/components/dealix/agent-quality-view.tsx create mode 100644 salesflow-saas/frontend/src/components/dealix/go-live-readiness-card.tsx create mode 100644 salesflow-saas/frontend/src/components/dealix/governance-metrics-view.tsx create mode 100644 salesflow-saas/frontend/src/components/dealix/growth-playbook-view.tsx create mode 100644 salesflow-saas/frontend/src/components/dealix/identity-graph-view.tsx create mode 100644 salesflow-saas/frontend/src/components/dealix/marketer-hub-view.tsx create mode 100644 salesflow-saas/frontend/src/components/dealix/operating-model-view.tsx create mode 100644 salesflow-saas/frontend/src/components/dealix/partnership-studio-view.tsx create mode 100644 salesflow-saas/frontend/src/components/dealix/vertical-playbooks-view.tsx diff --git a/salesflow-saas/.gitignore b/salesflow-saas/.gitignore index 7e3413d9..61b12d93 100644 --- a/salesflow-saas/.gitignore +++ b/salesflow-saas/.gitignore @@ -49,6 +49,7 @@ coverage/ # Local SQLite / CI DB artifacts (never commit) backend/*.db backend/ci_*.db +backend/.pytest_dealix.sqlite # Playwright / E2E output frontend/test-results/ diff --git a/salesflow-saas/backend/alembic/versions/20260413_0002_strategic_deal_crm_links.py b/salesflow-saas/backend/alembic/versions/20260413_0002_strategic_deal_crm_links.py new file mode 100644 index 00000000..9046fe1a --- /dev/null +++ b/salesflow-saas/backend/alembic/versions/20260413_0002_strategic_deal_crm_links.py @@ -0,0 +1,40 @@ +"""Add lead_id and sales_deal_id to strategic_deals. + +Revision ID: 20260413_0002 +Revises: 20260403_0001 +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "20260413_0002" +down_revision: Union[str, None] = "20260403_0001" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + bind = op.get_bind() + dialect = bind.dialect.name if bind else "" + uid = sa.Uuid(as_uuid=True) + if dialect == "sqlite": + with op.batch_alter_table("strategic_deals") as batch: + batch.add_column(sa.Column("lead_id", uid, nullable=True)) + batch.add_column(sa.Column("sales_deal_id", uid, nullable=True)) + else: + op.add_column("strategic_deals", sa.Column("lead_id", uid, nullable=True)) + op.add_column("strategic_deals", sa.Column("sales_deal_id", uid, nullable=True)) + + +def downgrade() -> None: + bind = op.get_bind() + dialect = bind.dialect.name if bind else "" + if dialect == "sqlite": + with op.batch_alter_table("strategic_deals") as batch: + batch.drop_column("sales_deal_id") + batch.drop_column("lead_id") + else: + op.drop_column("strategic_deals", "sales_deal_id") + op.drop_column("strategic_deals", "lead_id") diff --git a/salesflow-saas/backend/alembic/versions/20260413_0003_lead_company_sector_city.py b/salesflow-saas/backend/alembic/versions/20260413_0003_lead_company_sector_city.py new file mode 100644 index 00000000..44cd7d52 --- /dev/null +++ b/salesflow-saas/backend/alembic/versions/20260413_0003_lead_company_sector_city.py @@ -0,0 +1,43 @@ +"""Add company_name, sector, city to leads. + +Revision ID: 20260413_0003 +Revises: 20260413_0002 +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "20260413_0003" +down_revision: Union[str, None] = "20260413_0002" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + bind = op.get_bind() + dialect = bind.dialect.name if bind else "" + if dialect == "sqlite": + with op.batch_alter_table("leads") as batch: + batch.add_column(sa.Column("company_name", sa.String(255), nullable=True)) + batch.add_column(sa.Column("sector", sa.String(100), nullable=True)) + batch.add_column(sa.Column("city", sa.String(100), nullable=True)) + else: + op.add_column("leads", sa.Column("company_name", sa.String(255), nullable=True)) + op.add_column("leads", sa.Column("sector", sa.String(100), nullable=True)) + op.add_column("leads", sa.Column("city", sa.String(100), nullable=True)) + + +def downgrade() -> None: + bind = op.get_bind() + dialect = bind.dialect.name if bind else "" + if dialect == "sqlite": + with op.batch_alter_table("leads") as batch: + batch.drop_column("city") + batch.drop_column("sector") + batch.drop_column("company_name") + else: + op.drop_column("leads", "city") + op.drop_column("leads", "sector") + op.drop_column("leads", "company_name") diff --git a/salesflow-saas/backend/app/api/v1/ai_routing.py b/salesflow-saas/backend/app/api/v1/ai_routing.py new file mode 100644 index 00000000..4cc9aa22 --- /dev/null +++ b/salesflow-saas/backend/app/api/v1/ai_routing.py @@ -0,0 +1,116 @@ +"""Tenant-level LLM routing policy (no API keys exposed).""" + +from __future__ import annotations + +from typing import Any, Dict, Literal + +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel, Field +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.deps import get_current_user, require_role +from app.config import get_settings +from app.database import get_db +from app.models.tenant import Tenant +from app.models.user import User + +router = APIRouter(prefix="/ai", tags=["AI — routing"]) + +TaskKey = Literal["discovery", "negotiation", "compliance", "strategy_summary", "embeddings"] + + +class TaskRoute(BaseModel): + provider: str = Field(..., description="groq | openai | anthropic | etc.") + model: str = Field(..., description="Model id for that provider") + + +class RoutingMap(BaseModel): + discovery: TaskRoute | None = None + negotiation: TaskRoute | None = None + compliance: TaskRoute | None = None + strategy_summary: TaskRoute | None = None + embeddings: TaskRoute | None = None + + +def _defaults_from_settings() -> Dict[str, Dict[str, str]]: + s = get_settings() + primary = (s.LLM_PRIMARY_PROVIDER or "groq").lower() + if primary == "openai": + default_model = s.OPENAI_MODEL + else: + default_model = s.GROQ_MODEL + return { + "discovery": {"provider": primary, "model": default_model}, + "negotiation": {"provider": primary, "model": default_model}, + "compliance": {"provider": primary, "model": s.OPENAI_MINI_MODEL if primary == "openai" else s.GROQ_FAST_MODEL}, + "strategy_summary": {"provider": primary, "model": default_model}, + "embeddings": {"provider": "openai", "model": s.EMBEDDING_MODEL}, + } + + +def _available_providers() -> list[str]: + s = get_settings() + out = [] + if s.GROQ_API_KEY: + out.append("groq") + if s.OPENAI_API_KEY: + out.append("openai") + if s.ANTHROPIC_API_KEY: + out.append("anthropic") + if s.DEEPSEEK_API_KEY: + out.append("deepseek") + if s.GOOGLE_API_KEY: + out.append("google") + if s.ZAI_API_KEY: + out.append("zai") + return out + + +def _merge_routing(tenant_settings: dict | None) -> Dict[str, Dict[str, str]]: + base = _defaults_from_settings() + custom = (tenant_settings or {}).get("llm_routing") or {} + for k, v in custom.items(): + if isinstance(v, dict) and v.get("provider") and v.get("model"): + base[k] = {"provider": str(v["provider"]), "model": str(v["model"])} + return base + + +@router.get("/routing") +async def get_ai_routing( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + r = await db.execute(select(Tenant).where(Tenant.id == current_user.tenant_id)) + tenant = r.scalar_one_or_none() + if not tenant: + raise HTTPException(status_code=404, detail="Tenant not found") + return { + "effective": _merge_routing(tenant.settings), + "available_providers": _available_providers(), + "note_ar": "المفاتيح تبقى في الخادم فقط — الواجهة ترى أسماء المزودين والنماذج فقط.", + } + + +@router.put("/routing", dependencies=[Depends(require_role("owner", "manager", "admin"))]) +async def put_ai_routing( + body: RoutingMap, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + r = await db.execute(select(Tenant).where(Tenant.id == current_user.tenant_id)) + tenant = r.scalar_one_or_none() + if not tenant: + raise HTTPException(status_code=404, detail="Tenant not found") + patch: Dict[str, Any] = {} + data = body.model_dump(exclude_none=True) + for task, spec in data.items(): + if isinstance(spec, dict): + patch[task] = spec + base = dict(tenant.settings or {}) + lr = dict(base.get("llm_routing") or {}) + lr.update(patch) + base["llm_routing"] = lr + tenant.settings = base + await db.flush() + return {"status": "ok", "effective": _merge_routing(base)} diff --git a/salesflow-saas/backend/app/api/v1/integrations_crm.py b/salesflow-saas/backend/app/api/v1/integrations_crm.py new file mode 100644 index 00000000..d15cfde8 --- /dev/null +++ b/salesflow-saas/backend/app/api/v1/integrations_crm.py @@ -0,0 +1,226 @@ +"""CRM integrations API — Salesforce & HubSpot sync (JWT).""" + +from __future__ import annotations + +from uuid import UUID + +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel, Field +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.api.deps import get_current_user, require_role +from app.config import get_settings +from app.database import get_db +from app.models.tenant import Tenant +from app.models.user import User +from app.services.crm_sync_service import CRMSyncService +from app.services.lead_service import LeadService +from app.services.operations_hub import upsert_connector_status + +router = APIRouter(prefix="/integrations/crm", tags=["Integrations — CRM"]) +settings = get_settings() + + +class TenantCRMUpdate(BaseModel): + """Store non-secret CRM overrides on tenant.settings['crm'] (encrypt at rest in production).""" + + salesforce: dict | None = None + hubspot: dict | None = None + + +async def _tenant(db: AsyncSession, tenant_id: UUID) -> Tenant: + r = await db.execute(select(Tenant).where(Tenant.id == tenant_id)) + t = r.scalar_one_or_none() + if not t: + raise HTTPException(status_code=404, detail="Tenant not found") + return t + + +@router.get("/status") +async def crm_status( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """Which CRM keys are present (env vs tenant) — no secrets returned.""" + tenant = await _tenant(db, current_user.tenant_id) + crm = (tenant.settings or {}).get("crm") or {} + sf_t = (crm.get("salesforce") or {}) + hs_t = (crm.get("hubspot") or {}) + return { + "salesforce": { + "env_refresh_configured": bool( + settings.SALESFORCE_CLIENT_ID + and settings.SALESFORCE_CLIENT_SECRET + and settings.SALESFORCE_REFRESH_TOKEN + ), + "tenant_refresh_override": bool(sf_t.get("refresh_token")), + "domain": sf_t.get("domain") or settings.SALESFORCE_DOMAIN or "login.salesforce.com", + }, + "hubspot": { + "env_token_configured": bool(settings.HUBSPOT_API_KEY), + "tenant_token_override": bool(hs_t.get("private_app_token") or hs_t.get("access_token")), + }, + "docs": { + "integration_master_ar": "/strategy/INTEGRATION_MASTER_AR.md", + "api_map": "docs/API-MAP.md", + }, + } + + +@router.put("/tenant-settings", dependencies=[Depends(require_role("owner", "manager", "admin"))]) +async def put_tenant_crm_settings( + body: TenantCRMUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + tenant = await _tenant(db, current_user.tenant_id) + base = dict(tenant.settings or {}) + crm = dict(base.get("crm") or {}) + if body.salesforce is not None: + crm["salesforce"] = {**(crm.get("salesforce") or {}), **body.salesforce} + if body.hubspot is not None: + crm["hubspot"] = {**(crm.get("hubspot") or {}), **body.hubspot} + base["crm"] = crm + tenant.settings = base + await db.flush() + return {"status": "ok"} + + +@router.post("/salesforce/test", dependencies=[Depends(require_role("owner", "manager", "admin"))]) +async def salesforce_test( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + svc = CRMSyncService(db) + try: + creds = await svc._get_crm_credentials(str(current_user.tenant_id), "salesforce") + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)[:500]) from e + if not creds: + raise HTTPException(status_code=400, detail="Salesforce not configured (refresh token + client id/secret)") + probe = await svc.salesforce_identity_probe(creds) + ok = bool(probe.get("ok")) + await upsert_connector_status( + db, + current_user.tenant_id, + "crm_salesforce", + status="ok" if ok else "error", + success=ok, + last_error=None if ok else str(probe.get("detail") or probe)[:500], + ) + return probe + + +@router.post("/salesforce/push-lead/{lead_id}", dependencies=[Depends(require_role("owner", "manager", "admin"))]) +async def salesforce_push_lead( + lead_id: UUID, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + svc = CRMSyncService(db) + res = await svc.sync_lead_to_crm(str(current_user.tenant_id), str(lead_id), "salesforce") + if res.get("status") == "success" and res.get("salesforce_id"): + ls = LeadService(db) + await ls.merge_lead_extra_metadata( + str(current_user.tenant_id), + str(lead_id), + {"salesforce_lead_id": res["salesforce_id"]}, + ) + await upsert_connector_status( + db, current_user.tenant_id, "crm_salesforce", status="ok", success=True, + ) + elif res.get("status") == "error": + await upsert_connector_status( + db, + current_user.tenant_id, + "crm_salesforce", + status="error", + last_error=str(res.get("message", res))[:500], + ) + return res + + +class PullBody(BaseModel): + since: str | None = Field(None, description="Ignored for Salesforce MVP; reserved for SOQL") + + +@router.post("/salesforce/pull-leads", dependencies=[Depends(require_role("owner", "manager", "admin"))]) +async def salesforce_pull_leads( + body: PullBody | None = None, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + _ = body + svc = CRMSyncService(db) + res = await svc.full_sync(str(current_user.tenant_id), "salesforce") + if res.get("status") == "completed": + await upsert_connector_status( + db, current_user.tenant_id, "crm_salesforce", status="ok", success=True, + ) + return res + + +@router.post("/hubspot/test", dependencies=[Depends(require_role("owner", "manager", "admin"))]) +async def hubspot_test( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + svc = CRMSyncService(db) + creds = await svc._get_crm_credentials(str(current_user.tenant_id), "hubspot") + if not creds: + raise HTTPException(status_code=400, detail="HubSpot token not configured") + probe = await svc.hubspot_identity_probe(creds.get("api_key", "")) + ok = bool(probe.get("ok")) + await upsert_connector_status( + db, + current_user.tenant_id, + "crm_hubspot", + status="ok" if ok else "error", + success=ok, + last_error=None if ok else str(probe.get("detail") or probe)[:500], + ) + return probe + + +@router.post("/hubspot/push-lead/{lead_id}", dependencies=[Depends(require_role("owner", "manager", "admin"))]) +async def hubspot_push_lead( + lead_id: UUID, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + svc = CRMSyncService(db) + res = await svc.sync_lead_to_crm(str(current_user.tenant_id), str(lead_id), "hubspot") + if res.get("status") == "success" and res.get("hubspot_id"): + ls = LeadService(db) + await ls.merge_lead_extra_metadata( + str(current_user.tenant_id), + str(lead_id), + {"hubspot_contact_id": res["hubspot_id"]}, + ) + await upsert_connector_status( + db, current_user.tenant_id, "crm_hubspot", status="ok", success=True, + ) + elif res.get("status") == "error": + await upsert_connector_status( + db, + current_user.tenant_id, + "crm_hubspot", + status="error", + last_error=str(res.get("message", res))[:500], + ) + return res + + +@router.post("/hubspot/pull-contacts", dependencies=[Depends(require_role("owner", "manager", "admin"))]) +async def hubspot_pull_contacts( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + svc = CRMSyncService(db) + res = await svc.full_sync(str(current_user.tenant_id), "hubspot") + if res.get("status") == "completed": + await upsert_connector_status( + db, current_user.tenant_id, "crm_hubspot", status="ok", success=True, + ) + return res diff --git a/salesflow-saas/backend/app/api/v1/operations.py b/salesflow-saas/backend/app/api/v1/operations.py index 8d6caeaa..15a303ba 100644 --- a/salesflow-saas/backend/app/api/v1/operations.py +++ b/salesflow-saas/backend/app/api/v1/operations.py @@ -108,6 +108,7 @@ def _demo_snapshot() -> Dict[str, Any]: "audit_events_24h": 0, "connectors": [ {"connector_key": "crm_salesforce", "display_name_ar": "Salesforce CRM", "status": "unknown", "last_success_at": None, "last_attempt_at": None, "last_error": None}, + {"connector_key": "crm_hubspot", "display_name_ar": "HubSpot CRM", "status": "unknown", "last_success_at": None, "last_attempt_at": None, "last_error": None}, {"connector_key": "whatsapp_cloud", "display_name_ar": "واتساب Cloud API", "status": "unknown", "last_success_at": None, "last_attempt_at": None, "last_error": None}, {"connector_key": "stripe_billing", "display_name_ar": "Stripe — الفوترة", "status": "unknown", "last_success_at": None, "last_attempt_at": None, "last_error": None}, {"connector_key": "email_sync", "display_name_ar": "مزامنة البريد", "status": "unknown", "last_success_at": None, "last_attempt_at": None, "last_error": None}, @@ -154,6 +155,9 @@ async def operations_snapshot( pending = await count_pending_approvals(db, user.tenant_id) ev = await count_events_since(db, user.tenant_id, 24) aud = await count_audits_since(db, user.tenant_id, 24) + from app.services.integration_probe import probe_and_persist_crm_connectors + + await probe_and_persist_crm_connectors(db, user.tenant_id) connectors = await list_integration_connectors(db, user.tenant_id) tenant_id_str = str(user.tenant_id) esc = await refresh_pending_escalations(db, user.tenant_id) diff --git a/salesflow-saas/backend/app/api/v1/router.py b/salesflow-saas/backend/app/api/v1/router.py index d1ac4282..272e266b 100644 --- a/salesflow-saas/backend/app/api/v1/router.py +++ b/salesflow-saas/backend/app/api/v1/router.py @@ -25,6 +25,8 @@ from app.api.v1 import customer_onboarding as customer_onboarding_router from app.api.v1 import sales_os as sales_os_router from app.api.v1 import operations as operations_router from app.api.v1 import proposals as proposals_router +from app.api.v1 import integrations_crm as integrations_crm_router +from app.api.v1 import ai_routing as ai_routing_router api_router = APIRouter() @@ -58,6 +60,8 @@ api_router.include_router(value_proposition_router.router) api_router.include_router(customer_onboarding_router.router) api_router.include_router(sales_os_router.router) api_router.include_router(operations_router.router) +api_router.include_router(integrations_crm_router.router) +api_router.include_router(ai_routing_router.router) api_router.include_router(analytics.router, tags=["Analytics & AI"]) api_router.include_router(webhooks.router, tags=["Webhooks"]) api_router.include_router(prospecting.router, prefix="/prospecting", tags=["Prospecting"]) diff --git a/salesflow-saas/backend/app/api/v1/strategic_deals.py b/salesflow-saas/backend/app/api/v1/strategic_deals.py index f36dc8b1..f7f875c7 100644 --- a/salesflow-saas/backend/app/api/v1/strategic_deals.py +++ b/salesflow-saas/backend/app/api/v1/strategic_deals.py @@ -24,6 +24,11 @@ from app.services.strategic_deals.company_profiler import CompanyProfiler from app.services.strategic_deals.deal_matcher import DealMatcher from app.services.strategic_deals.deal_negotiator import DealNegotiator, NegotiationStrategy from app.services.strategic_deals.deal_agent import DealAgent +from app.services.strategic_deals.operating_modes import OperatingMode, ModeEnforcer +from app.services.strategic_deals.deal_taxonomy import DealTaxonomyService +from app.services.dealix_os.vertical_playbooks import get_playbook, list_playbook_ids +from app.services.dealix_os.partner_archetypes import list_archetypes, archetype_for_deal_type +from app.services.dealix_os.policy_engine import evaluate_action, suggested_playbook_for_industry router = APIRouter(prefix="/strategic-deals", tags=["Strategic Deals"]) @@ -93,6 +98,8 @@ class DealCreate(Schema): proposed_terms: dict = {} estimated_value_sar: Optional[float] = None channel: str = "whatsapp" + lead_id: Optional[UUID] = None + sales_deal_id: Optional[UUID] = None class DealResponse(Schema): @@ -117,6 +124,8 @@ class DealResponse(Schema): negotiation_history: list = [] notes: Optional[str] = None notes_ar: Optional[str] = None + lead_id: Optional[UUID] = None + sales_deal_id: Optional[UUID] = None created_at: datetime updated_at: Optional[datetime] = None closed_at: Optional[datetime] = None @@ -161,9 +170,44 @@ class BarterScanRequest(Schema): profile_id: UUID +class OperatingModeSet(Schema): + mode: int = Field(..., ge=0, le=4, description="OperatingMode 0–4") + + +class PolicyEvaluateRequest(Schema): + channel: str = "whatsapp" + action: str = "send_custom_message" + deal_value_sar: float = 0.0 + industry: Optional[str] = None + + +class DealLinksUpdate(Schema): + lead_id: Optional[UUID] = None + sales_deal_id: Optional[UUID] = None + + # ── Profile Endpoints ──────────────────────────────────────────────────────── +@router.get("/profiles", response_model=list[ProfileResponse]) +async def list_profiles( + page: int = Query(1, ge=1), + per_page: int = Query(20, ge=1, le=100), + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """List company profiles for the tenant. | عرض ملفات الشركات""" + q = ( + select(CompanyProfile) + .where(CompanyProfile.tenant_id == current_user.tenant_id) + .order_by(CompanyProfile.created_at.desc()) + .offset((page - 1) * per_page) + .limit(per_page) + ) + result = await db.execute(q) + return [ProfileResponse.model_validate(p) for p in result.scalars().all()] + + @router.post("/profiles", response_model=ProfileResponse, status_code=201) async def create_profile( data: ProfileCreate, @@ -339,6 +383,8 @@ async def create_deal( channel=data.channel, ai_confidence=0.0, negotiation_history=[], + lead_id=data.lead_id, + sales_deal_id=data.sales_deal_id, ) db.add(deal) await db.flush() @@ -373,6 +419,313 @@ async def list_deals( return [DealResponse.model_validate(d) for d in result.scalars().all()] +@router.get("/operating-model") +async def get_operating_model( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """Current AI operating mode and all mode definitions. | وضع التشغيل والأوصاف""" + mode = await ModeEnforcer.get_current_mode(str(current_user.tenant_id), db) + policy = ModeEnforcer.get_mode_policy(mode) + return { + "current": { + "mode": mode.value, + "name": mode.name, + "label_ar": policy.label_ar, + "description_ar": policy.description_ar, + "auto_send": policy.auto_send, + "auto_negotiate": policy.auto_negotiate, + "max_auto_commitment_sar": policy.max_auto_commitment_sar, + "allowed_channels": policy.allowed_channels, + }, + "modes": ModeEnforcer.get_all_modes(), + "roles_ar": [ + {"id": "owner", "label": "المالك", "scope": "تغيير وضع التشغيل، الالتزامات الكبرى"}, + {"id": "revops", "label": "عمليات الإيرادات", "scope": "القمع، السياسات، التقارير"}, + {"id": "partner_manager", "label": "مدير شراكات", "scope": "مسار الشراكات والتفاوض"}, + {"id": "compliance", "label": "الامتثال", "scope": "الموافقات الحساسة والقطاعات المنظمة"}, + ], + "sla_hints_ar": { + "response_window": "الرد على العملاء المؤهلين خلال ٢٤–٤٨ ساعة عمل", + "followup_cap": "حد أقصى ٣ متابعات تلقائية ثم تصعيد بشري", + "opt_out": "احترام طلب التوقف فوراً وتسجيله في السجل", + }, + } + + +@router.put("/operating-model") +async def set_operating_model( + data: OperatingModeSet, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """Set tenant operating mode (stored on first company profile). | تعيين وضع التشغيل""" + try: + om = OperatingMode(data.mode) + except ValueError: + raise HTTPException(status_code=400, detail="وضع تشغيل غير صالح | Invalid mode") + try: + await ModeEnforcer.set_mode(str(current_user.tenant_id), om, db) + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + return {"status": "ok", "mode": om.value, "name": om.name} + + +@router.get("/taxonomy/deal-types") +async def taxonomy_deal_types(): + """Full 15-type partnership taxonomy for UI. | تصنيف أنواع الصفقات""" + return [t.model_dump() for t in DealTaxonomyService.get_all_types()] + + +@router.get("/taxonomy/deal-types/{type_id}") +async def taxonomy_deal_type_detail(type_id: str): + spec = DealTaxonomyService.get_deal_type(type_id) + if not spec: + raise HTTPException(status_code=404, detail="نوع غير معروف | Unknown type") + return spec.model_dump() + + +@router.get("/partner-archetypes") +async def partner_archetypes(): + """Map DB deal_type values to operational archetypes. | أنماط الشراكات التشغيلية""" + return {"archetypes": list_archetypes()} + + +@router.get("/playbooks") +async def playbooks_list(): + """Vertical playbooks (sector defaults). | قوالب قطاعية""" + return { + "ids": list_playbook_ids(), + "items": [get_playbook(i) for i in list_playbook_ids()], + } + + +@router.get("/playbooks/{playbook_id}") +async def playbook_detail(playbook_id: str): + pb = get_playbook(playbook_id) + if not pb: + raise HTTPException(status_code=404, detail="playbook not found") + return pb + + +@router.post("/policy/evaluate") +async def policy_evaluate( + data: PolicyEvaluateRequest, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """Graded policy: auto_execute | approval_required | blocked.""" + result = await evaluate_action( + tenant_id=current_user.tenant_id, + channel=data.channel, + action=data.action, + deal_value_sar=data.deal_value_sar, + industry=data.industry, + db=db, + ) + sp = suggested_playbook_for_industry(data.industry) + result["suggested_playbook_id"] = sp + return result + + +@router.get("/identity/graph") +async def identity_graph( + profile_id: UUID, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """Counts and links for one company profile (light account graph).""" + pr = await db.execute( + select(CompanyProfile).where( + CompanyProfile.id == profile_id, + CompanyProfile.tenant_id == current_user.tenant_id, + ) + ) + profile = pr.scalar_one_or_none() + if not profile: + raise HTTPException(status_code=404, detail="Profile not found") + + deals_init = await db.execute( + select(func.count()).select_from(StrategicDeal).where( + StrategicDeal.tenant_id == current_user.tenant_id, + StrategicDeal.initiator_profile_id == profile_id, + ) + ) + deals_tgt = await db.execute( + select(func.count()).select_from(StrategicDeal).where( + StrategicDeal.tenant_id == current_user.tenant_id, + StrategicDeal.target_profile_id == profile_id, + ) + ) + matches_a = await db.execute( + select(func.count()).select_from(DealMatch).where( + DealMatch.tenant_id == current_user.tenant_id, + DealMatch.company_a_id == profile_id, + ) + ) + matches_b = await db.execute( + select(func.count()).select_from(DealMatch).where( + DealMatch.tenant_id == current_user.tenant_id, + DealMatch.company_b_id == profile_id, + ) + ) + linked_leads = await db.execute( + select(func.count()).select_from(StrategicDeal).where( + StrategicDeal.tenant_id == current_user.tenant_id, + (StrategicDeal.initiator_profile_id == profile_id) + | (StrategicDeal.target_profile_id == profile_id), + StrategicDeal.lead_id.isnot(None), + ) + ) + linked_sales = await db.execute( + select(func.count()).select_from(StrategicDeal).where( + StrategicDeal.tenant_id == current_user.tenant_id, + (StrategicDeal.initiator_profile_id == profile_id) + | (StrategicDeal.target_profile_id == profile_id), + StrategicDeal.sales_deal_id.isnot(None), + ) + ) + + return { + "profile_id": str(profile_id), + "company_name": profile.company_name, + "suggested_playbook_id": suggested_playbook_for_industry(profile.industry), + "archetype_hint": archetype_for_deal_type("partnership"), + "counts": { + "strategic_deals_as_initiator": deals_init.scalar() or 0, + "strategic_deals_as_target": deals_tgt.scalar() or 0, + "matches_as_party_a": matches_a.scalar() or 0, + "matches_as_party_b": matches_b.scalar() or 0, + "deals_with_lead_link": linked_leads.scalar() or 0, + "deals_with_sales_deal_link": linked_sales.scalar() or 0, + }, + } + + +@router.get("/governance/snapshot") +async def governance_snapshot( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """North-star style KPIs + policy posture for dashboards.""" + mode = await ModeEnforcer.get_current_mode(str(current_user.tenant_id), db) + policy = ModeEnforcer.get_mode_policy(mode) + + tenant_id = current_user.tenant_id + total_deals = (await db.execute( + select(func.count()).select_from(StrategicDeal).where(StrategicDeal.tenant_id == tenant_id) + )).scalar() or 0 + hist_rows = ( + await db.execute( + select(StrategicDeal.negotiation_history).where(StrategicDeal.tenant_id == tenant_id) + ) + ).all() + deals_with_history = sum( + 1 for (h,) in hist_rows if isinstance(h, list) and len(h) > 0 + ) + + return { + "operating_mode": {"value": mode.value, "name": mode.name, "label_ar": policy.label_ar}, + "north_star_hints_ar": { + "touch_to_meeting": "تقليل الزمن من أول لمسة إلى اجتماع مؤهل", + "stage_conversion": "تحسين تحويل المراحل في القمع", + "partner_attribution": "مساهمة الشراكات في خط الأنابيب", + }, + "governance_kpis": { + "auto_send_enabled": policy.auto_send, + "auto_negotiate_enabled": policy.auto_negotiate, + "max_auto_commitment_sar": policy.max_auto_commitment_sar, + "strategic_deals_total": total_deals, + "deals_with_negotiation_rounds": deals_with_history, + }, + } + + +@router.get("/growth/checklist") +async def growth_ma_checklist(): + """Light M&A / expansion checklist (human decisions required).""" + return { + "disclaimer_ar": "قائمة إرشادية فقط — لا تغني عن مستشار قانوني أو مالي.", + "phases": [ + { + "id": "thesis", + "title_ar": "أطروحة الاستثمار", + "items_ar": [ + "تحديد القطاع والجغرافيا والحجم المستهدف", + "ربط الصفقة بأهداف الشركة الاستراتيجية (٣–٥ نقاط)", + ], + }, + { + "id": "screen", + "title_ar": "فرز أولي", + "items_ar": [ + "تطبيق معايير إقصاء واضحة (حجم، نمو، تركيز)", + "تسجيل مصادر البيانات لكل هدف", + ], + }, + { + "id": "dd_lite", + "title_ar": "عناية واجبة خفيفة", + "items_ar": [ + "المالية: إيرادات، هامش، تدفقات", + "التقنية والمنتج: نضج، ديون تقنية، IP", + "العملاء: تركيز، انحراف، مخاطر تجميع", + ], + }, + { + "id": "approval", + "title_ar": "موافقة وإغلاق داخلي", + "items_ar": [ + "لجنة استثمار / مجلس إدارة حسب الحوكمة", + "توثيق الشروط الرئيسية قبل أي التزام", + ], + }, + ], + } + + +@router.get("/agent-quality/snapshot") +async def agent_quality_snapshot( + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """Proxy metrics for QA / improvement loop (extend with real message logs later).""" + tenant_id = current_user.tenant_id + total = (await db.execute( + select(func.count()).select_from(StrategicDeal).where(StrategicDeal.tenant_id == tenant_id) + )).scalar() or 0 + with_hist = (await db.execute( + select(StrategicDeal.negotiation_history).where(StrategicDeal.tenant_id == tenant_id) + )).all() + rounds = 0 + for row in with_hist: + h = row[0] or [] + if isinstance(h, list): + rounds += len(h) + avg_rounds = (rounds / total) if total else 0.0 + high_conf = (await db.execute( + select(func.count()).select_from(StrategicDeal).where( + StrategicDeal.tenant_id == tenant_id, + StrategicDeal.ai_confidence >= 0.7, + ) + )).scalar() or 0 + + return { + "labels_ar": { + "negotiation_depth": "عمق جولات التفاوض المسجّل", + "high_confidence_deals": "صفقات بثقة نموذج مرتفعة", + }, + "strategic_deals_total": total, + "negotiation_rounds_total": rounds, + "avg_negotiation_rounds_per_deal": round(avg_rounds, 2), + "deals_high_ai_confidence": high_conf, + "loop_hints_ar": [ + "اربط هذه المؤشرات لاحقاً بردود العملاء الفعلية ومعدلات التحويل", + "استخدم وضع «مسودات» عند ارتفاع معدل التصعيد", + ], + } + + @router.get("/{deal_id}", response_model=DealResponse) async def get_deal( deal_id: UUID, @@ -392,6 +745,32 @@ async def get_deal( return DealResponse.model_validate(deal) +@router.patch("/{deal_id}/links", response_model=DealResponse) +async def patch_deal_links( + deal_id: UUID, + data: DealLinksUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """Link strategic deal to CRM lead and/or sales deal. | ربط الصفقة بعميل محتمل أو صفقة مبيعات""" + result = await db.execute( + select(StrategicDeal).where( + StrategicDeal.id == deal_id, + StrategicDeal.tenant_id == current_user.tenant_id, + ) + ) + deal = result.scalar_one_or_none() + if not deal: + raise HTTPException(status_code=404, detail="الصفقة غير موجودة | Deal not found") + if data.lead_id is not None: + deal.lead_id = data.lead_id + if data.sales_deal_id is not None: + deal.sales_deal_id = data.sales_deal_id + await db.flush() + await db.refresh(deal) + return DealResponse.model_validate(deal) + + # ── Negotiation ────────────────────────────────────────────────────────────── diff --git a/salesflow-saas/backend/app/api/v1/strategy_summary.py b/salesflow-saas/backend/app/api/v1/strategy_summary.py index d856cc66..96cd9d61 100644 --- a/salesflow-saas/backend/app/api/v1/strategy_summary.py +++ b/salesflow-saas/backend/app/api/v1/strategy_summary.py @@ -25,6 +25,33 @@ async def strategy_summary() -> dict: "Measurable self-improvement loops when enabled", "OpenClaw-style durable flows + revision posture (see openclaw-config.yaml)", ], + "differentiators_verifiable": [ + { + "id": "api_surface", + "title_ar": "مسارات API موثقة ومفتوحة للتحقق", + "evidence": "docs/API-MAP.md + OpenAPI /docs + scripts/verify_frontend_openapi_paths.py", + }, + { + "id": "crm_sync", + "title_ar": "تكامل Salesforce/HubSpot مع اختبار ودفع وسحب", + "evidence": "POST /api/v1/integrations/crm/*/test|push-lead|pull-*", + }, + { + "id": "llm_routing", + "title_ar": "توجيه نماذج حسب المهمة دون كشف مفاتيح", + "evidence": "GET/PUT /api/v1/ai/routing", + }, + { + "id": "arabic_os", + "title_ar": "واجهة عربية ومسارات Dealix OS في منتج واحد", + "evidence": "dashboard hubs + docs/DEALIX_OS_PRODUCT_GUIDE_AR.md", + }, + { + "id": "competitive_doc", + "title_ar": "مصفوفة تنافسية صريحة بدون أرقام غير مثبتة", + "evidence": "docs/COMPETITIVE_MATRIX_AR.md (نسخة ويب /strategy/COMPETITIVE_MATRIX_AR.md بعد المزامنة)", + }, + ], "competitive_moat": { "durable_runtime": "OpenClaw 2026.4.2 pattern — checkpoints, retries, bounded plugins", "self_improvement": "6-phase loop: signals → diagnose → experiments → A/B → governance → promote/rollback", @@ -90,6 +117,7 @@ async def strategy_summary() -> dict: "full_markdown_web": "/strategy/DEALIX_NEXT_LEVEL_MASTER_PLAN_AR.md", "ultimate_execution_ar": "/strategy/ULTIMATE_EXECUTION_MASTER_AR.md", "integration_master_ar": "/strategy/INTEGRATION_MASTER_AR.md", + "competitive_matrix_ar": "/strategy/COMPETITIVE_MATRIX_AR.md", "investor_html": "/dealix-marketing/investor/00-investor-dealix-full-ar.html", }, "repo_paths": { @@ -98,4 +126,26 @@ async def strategy_summary() -> dict: "ultimate_doc": "salesflow-saas/docs/ULTIMATE_EXECUTION_MASTER_AR.md", "integration_master": "salesflow-saas/docs/INTEGRATION_MASTER_AR.md", }, + "dealix_os_three_pillars": { + "sales": { + "label_ar": "محرك المبيعات", + "focus": "اكتشاف، قمع، قنوات، إغلاق مع حوكمة إرسال", + "primary_api_surface": "leads, pipeline, inbox, agents", + }, + "partnerships": { + "label_ar": "شراكات استراتيجية", + "focus": "ملفات B2B، مطابقة، تفاوض، Partnership Studio", + "primary_api_surface": "/api/v1/strategic-deals", + }, + "growth": { + "label_ar": "نمو واستعداد استحواذ", + "focus": "ذكاء استراتيجي، قوائم مهام، قرار بشري للالتزامات الكبرى", + "primary_api_surface": "autonomous_core, strategy_summary, growth/checklist", + }, + "governance": { + "label_ar": "حوكمة وثقة", + "focus": "أوضاع تشغيل، سياسات متدرجة، go-live، سجلات", + "primary_api_surface": "operating-model, policy/evaluate, go-live-gate", + }, + }, } diff --git a/salesflow-saas/backend/app/models/lead.py b/salesflow-saas/backend/app/models/lead.py index 53550784..af0e7039 100644 --- a/salesflow-saas/backend/app/models/lead.py +++ b/salesflow-saas/backend/app/models/lead.py @@ -11,6 +11,9 @@ class Lead(BaseModel): tenant_id = Column(UUID(as_uuid=True), ForeignKey("tenants.id"), nullable=False, index=True) assigned_to = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=True) name = Column(String(255), nullable=False) + company_name = Column(String(255), nullable=True) + sector = Column(String(100), nullable=True) + city = Column(String(100), nullable=True) phone = Column(String(20)) email = Column(String(255)) source = Column(String(100)) # whatsapp, website, referral, social, phone diff --git a/salesflow-saas/backend/app/models/strategic_deal.py b/salesflow-saas/backend/app/models/strategic_deal.py index d7315cd4..4f0f90f5 100644 --- a/salesflow-saas/backend/app/models/strategic_deal.py +++ b/salesflow-saas/backend/app/models/strategic_deal.py @@ -182,6 +182,10 @@ class StrategicDeal(TenantModel): closed_at = Column(DateTime(timezone=True), nullable=True) + # Links to core CRM entities (Sales OS — optional) + lead_id = Column(UUID(as_uuid=True), ForeignKey("leads.id"), nullable=True, index=True) + sales_deal_id = Column(UUID(as_uuid=True), ForeignKey("deals.id"), nullable=True, index=True) + # Relationships initiator_profile = relationship( "CompanyProfile", back_populates="initiated_deals", @@ -191,6 +195,8 @@ class StrategicDeal(TenantModel): "CompanyProfile", back_populates="targeted_deals", foreign_keys=[target_profile_id], ) + lead = relationship("Lead", foreign_keys=[lead_id]) + sales_deal = relationship("Deal", foreign_keys=[sales_deal_id]) # ── Deal Match ─────────────────────────────────────────────────────────────── diff --git a/salesflow-saas/backend/app/services/crm_sync_service.py b/salesflow-saas/backend/app/services/crm_sync_service.py index 99058ea1..b961f563 100644 --- a/salesflow-saas/backend/app/services/crm_sync_service.py +++ b/salesflow-saas/backend/app/services/crm_sync_service.py @@ -3,12 +3,15 @@ CRM Sync Service — Bidirectional sync with Salesforce, HubSpot, and generic CR """ import uuid -from datetime import datetime, timezone from typing import Optional import httpx +from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession + from app.config import get_settings +from app.models.tenant import Tenant +from app.services.salesforce_oauth import refresh_salesforce_access_token settings = get_settings() @@ -32,9 +35,13 @@ class CRMSyncService: if not access_token or not instance_url: return {"status": "error", "message": "Invalid Salesforce credentials"} + fn = (lead.get("full_name") or lead.get("name") or "").strip() + parts = fn.split() if fn else [] + first = parts[0] if parts else "Unknown" + last = parts[-1] if len(parts) > 1 else "." sf_lead = { - "FirstName": lead.get("full_name", "").split()[0] if lead.get("full_name") else "", - "LastName": lead.get("full_name", "").split()[-1] if lead.get("full_name") else "Unknown", + "FirstName": first, + "LastName": last, "Phone": lead.get("phone", ""), "Email": lead.get("email", ""), "Company": lead.get("company_name", "Unknown"), @@ -65,10 +72,11 @@ class CRMSyncService: access_token = credentials.get("access_token") instance_url = credentials.get("instance_url") - query = "SELECT Id, FirstName, LastName, Phone, Email, Company, Industry, City, Rating FROM Lead" - if since: - query += f" WHERE LastModifiedDate > {since}" - query += " ORDER BY LastModifiedDate DESC LIMIT 100" + # SOQL: avoid injecting raw `since` without proper quoting — use full window + LIMIT + query = ( + "SELECT Id, FirstName, LastName, Phone, Email, Company, Industry, City, Rating " + "FROM Lead ORDER BY LastModifiedDate DESC LIMIT 100" + ) async with httpx.AsyncClient() as client: response = await client.get( @@ -100,8 +108,14 @@ class CRMSyncService: """Push a contact from Dealix to HubSpot.""" hs_contact = { "properties": { - "firstname": lead.get("full_name", "").split()[0] if lead.get("full_name") else "", - "lastname": lead.get("full_name", "").split()[-1] if lead.get("full_name") else "", + "firstname": ( + ((lead.get("full_name") or lead.get("name") or "").split() or [""])[0] + ), + "lastname": ( + ((lead.get("full_name") or lead.get("name") or "").split() or ["", "."])[-1] + if len((lead.get("full_name") or lead.get("name") or "").split()) > 1 + else "." + ), "phone": lead.get("phone", ""), "email": lead.get("email", ""), "company": lead.get("company_name", ""), @@ -209,11 +223,19 @@ class CRMSyncService: for ext_lead in external_leads: try: + em = (ext_lead.get("email") or "").strip() + if em: + existing = await lead_svc.get_lead_by_email(tenant_id, em) + if existing: + continue + name = ext_lead.get("full_name") or "Unknown" + if not name.strip(): + name = "Unknown" await lead_svc.create_lead( tenant_id=tenant_id, - full_name=ext_lead["full_name"], + full_name=name, phone=ext_lead.get("phone", ""), - email=ext_lead.get("email", ""), + email=em, company_name=ext_lead.get("company_name", ""), sector=ext_lead.get("sector", ""), city=ext_lead.get("city", ""), @@ -236,19 +258,72 @@ class CRMSyncService: # ── Helpers ─────────────────────────────────── async def _get_crm_credentials(self, tenant_id: str, provider: str) -> Optional[dict]: - """Get CRM credentials from tenant settings.""" - # In production, this would fetch from encrypted tenant settings + """Resolve CRM credentials: tenant.settings.crm overrides global env.""" + tid = uuid.UUID(tenant_id) + result = await self.db.execute(select(Tenant).where(Tenant.id == tid)) + tenant = result.scalar_one_or_none() + tset = dict(tenant.settings or {}) if tenant else {} + crm = dict(tset.get("crm") or {}) + if provider == "salesforce": - return { - "access_token": settings.SALESFORCE_CLIENT_SECRET, - "instance_url": "", - } if settings.SALESFORCE_CLIENT_ID else None - elif provider == "hubspot": - return { - "api_key": settings.HUBSPOT_API_KEY, - } if settings.HUBSPOT_API_KEY else None + sf = dict(crm.get("salesforce") or {}) + client_id = (sf.get("client_id") or settings.SALESFORCE_CLIENT_ID or "").strip() + client_secret = (sf.get("client_secret") or settings.SALESFORCE_CLIENT_SECRET or "").strip() + refresh_token = (sf.get("refresh_token") or settings.SALESFORCE_REFRESH_TOKEN or "").strip() + domain_host = (sf.get("domain") or settings.SALESFORCE_DOMAIN or "login.salesforce.com").strip() + if not client_id or not client_secret or not refresh_token: + return None + try: + tok = await refresh_salesforce_access_token( + domain_host=domain_host, + client_id=client_id, + client_secret=client_secret, + refresh_token=refresh_token, + ) + return { + "access_token": tok["access_token"], + "instance_url": tok["instance_url"], + } + except Exception: + return None + + if provider == "hubspot": + hs = dict(crm.get("hubspot") or {}) + token = (hs.get("private_app_token") or hs.get("access_token") or settings.HUBSPOT_API_KEY or "").strip() + if not token: + return None + return {"api_key": token} + return None + async def salesforce_identity_probe(self, credentials: dict) -> dict: + """Lightweight Salesforce API probe (limits resource).""" + access_token = credentials.get("access_token") + instance_url = credentials.get("instance_url") + if not access_token or not instance_url: + return {"ok": False, "error": "missing_token_or_instance"} + async with httpx.AsyncClient() as client: + r = await client.get( + f"{instance_url}/services/data/{settings.SALESFORCE_API_VERSION}/limits", + headers={"Authorization": f"Bearer {access_token}"}, + timeout=25.0, + ) + if r.status_code == 200: + return {"ok": True, "status_code": r.status_code} + return {"ok": False, "status_code": r.status_code, "detail": r.text[:300]} + + async def hubspot_identity_probe(self, api_key: str) -> dict: + async with httpx.AsyncClient() as client: + r = await client.get( + "https://api.hubapi.com/crm/v3/objects/contacts", + params={"limit": 1}, + headers={"Authorization": f"Bearer {api_key}"}, + timeout=25.0, + ) + if r.status_code == 200: + return {"ok": True, "status_code": r.status_code} + return {"ok": False, "status_code": r.status_code, "detail": r.text[:300]} + @staticmethod def _score_to_sf_rating(score: int) -> str: if score >= 80: diff --git a/salesflow-saas/backend/app/services/dealix_os/__init__.py b/salesflow-saas/backend/app/services/dealix_os/__init__.py new file mode 100644 index 00000000..58d6b3da --- /dev/null +++ b/salesflow-saas/backend/app/services/dealix_os/__init__.py @@ -0,0 +1,14 @@ +from app.services.dealix_os.vertical_playbooks import VERTICAL_PLAYBOOKS, get_playbook, list_playbook_ids +from app.services.dealix_os.partner_archetypes import ARCHETYPE_MAP, list_archetypes, archetype_for_deal_type +from app.services.dealix_os.policy_engine import evaluate_action, suggested_playbook_for_industry + +__all__ = [ + "VERTICAL_PLAYBOOKS", + "get_playbook", + "list_playbook_ids", + "ARCHETYPE_MAP", + "list_archetypes", + "archetype_for_deal_type", + "evaluate_action", + "suggested_playbook_for_industry", +] diff --git a/salesflow-saas/backend/app/services/dealix_os/partner_archetypes.py b/salesflow-saas/backend/app/services/dealix_os/partner_archetypes.py new file mode 100644 index 00000000..1742d304 --- /dev/null +++ b/salesflow-saas/backend/app/services/dealix_os/partner_archetypes.py @@ -0,0 +1,77 @@ +""" +Maps core deal_type values (DB enum) to partnership archetypes and taxonomy hints. +يربط نوع الصفقة المخزّن بأنماط شراكة تشغيلية أوضح للواجهة والوكلاء. +""" + +from typing import Any + +# Strategic deal deal_type column uses DealType enum strings; taxonomy uses richer IDs. +ARCHETYPE_MAP: dict[str, dict[str, Any]] = { + "partnership": { + "archetype_id": "strategic_alliance", + "label_ar": "شراكة عامة / تحالف", + "label_en": "General partnership / alliance", + "description_ar": "تعاون مرن قد يتطور إلى تسويق مشترك أو بيع مشترك.", + "default_taxonomy_ids": ["strategic_alliance", "co_marketing", "co_selling"], + "negotiation_focus_ar": ["الأهداف المشتركة", "مدة التعاون", "مشاركة العملاء المحتملين"], + }, + "distribution": { + "archetype_id": "channel", + "label_ar": "قناة توزيع", + "label_en": "Channel / distribution", + "description_ar": "الوصول لعملاء الشريك عبر شبكة بيع أو توزيع.", + "default_taxonomy_ids": ["channel_partnership", "reseller"], + "negotiation_focus_ar": ["المنطقة", "الهامش", "أهداف الأداء", "التدريب"], + }, + "franchise": { + "archetype_id": "expansion", + "label_ar": "امتياز / توسع علامة", + "label_en": "Franchise / brand expansion", + "description_ar": "تكرار نموذج عمل تحت علامة موحّدة مع حوكمة أعلى.", + "default_taxonomy_ids": ["strategic_alliance", "joint_venture"], + "negotiation_focus_ar": ["الرسوم", "الامتثال للعلامة", "الأداء الإقليمي"], + }, + "jv": { + "archetype_id": "jv", + "label_ar": "مشروع مشترك (JV)", + "label_en": "Joint venture", + "description_ar": "كيان أو مشروع مشترك مع مساهمات ورقابة من الطرفين.", + "default_taxonomy_ids": ["joint_venture", "tender_consortium"], + "negotiation_focus_ar": ["نسب الملكية", "الحوكمة", "الخروج", "توزيع الأرباح"], + }, + "referral": { + "archetype_id": "referral", + "label_ar": "إحالة وعمولة", + "label_en": "Referral", + "description_ar": "إحالة عملاء مؤهلين مقابل عمولة أو مقايضة قيمة.", + "default_taxonomy_ids": ["referral_partnership"], + "negotiation_focus_ar": ["نسبة العمولة", "التتبع", "تعريف العميل المؤهل"], + }, + "acquisition": { + "archetype_id": "corp_dev", + "label_ar": "نمو واستحواذ (مساعد قرار)", + "label_en": "Growth / M&A assist", + "description_ar": "استكشاف وتأهيل أهداف؛ القرار النهائي والعناية الواجبة بشرية.", + "default_taxonomy_ids": ["acquisition_scouting", "investment_intro"], + "negotiation_focus_ar": ["معايير الهدف", "الخصومية", "نطاق الفحص المبدئي"], + }, + "barter": { + "archetype_id": "barter", + "label_ar": "مقايضة / تبادل خدمات", + "label_en": "Barter / service exchange", + "description_ar": "تبادل قيمة بدون تدفق نقدي كامل أو مع دفعات جزئية.", + "default_taxonomy_ids": ["service_barter"], + "negotiation_focus_ar": ["تعادل القيمة", "نطاق التسليم", "مدة الالتزام"], + }, +} + + +def list_archetypes() -> list[dict[str, Any]]: + return [ + {"deal_type": k, **v} + for k, v in ARCHETYPE_MAP.items() + ] + + +def archetype_for_deal_type(deal_type: str) -> dict[str, Any] | None: + return ARCHETYPE_MAP.get(deal_type) diff --git a/salesflow-saas/backend/app/services/dealix_os/policy_engine.py b/salesflow-saas/backend/app/services/dealix_os/policy_engine.py new file mode 100644 index 00000000..b03f7335 --- /dev/null +++ b/salesflow-saas/backend/app/services/dealix_os/policy_engine.py @@ -0,0 +1,110 @@ +""" +Graded policy evaluation: auto_execute | approval_required | blocked. +محرك سياسات متدرج مرتبط بوضع التشغيل والقطاع والقناة والقيمة. +""" + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.services.strategic_deals.operating_modes import ModeEnforcer, OperatingMode, MODE_POLICIES +from app.services.dealix_os.vertical_playbooks import VERTICAL_PLAYBOOKS + +REGULATED_SECTORS = frozenset( + {"healthcare", "pharma", "medical", "finance", "banking", "insurance", "real_estate"} +) + + +def _normalize_industry(industry: str | None) -> str: + if not industry: + return "" + return industry.strip().lower().replace(" ", "_") + + +async def evaluate_action( + *, + tenant_id, + channel: str, + action: str, + deal_value_sar: float, + industry: str | None, + db: AsyncSession, +) -> dict: + mode = await ModeEnforcer.get_current_mode(str(tenant_id), db) + policy = MODE_POLICIES.get(mode) + if not policy: + return { + "level": "blocked", + "reason_ar": "وضع تشغيل غير معرّف", + "mode": mode.value, + "mode_name": mode.name, + } + + ind = _normalize_industry(industry) + is_regulated = any(s in ind for s in REGULATED_SECTORS) or ind in REGULATED_SECTORS + + channel_ok, ch_msg = await ModeEnforcer.check_channel(mode, channel) + if not channel_ok: + return { + "level": "blocked", + "reason_ar": ch_msg, + "mode": mode.value, + "mode_name": mode.name, + "channel": channel, + "action": action, + } + + action_ok, act_msg = await ModeEnforcer.check_action(mode, action, deal_value_sar, db) + if not action_ok: + return { + "level": "approval_required", + "reason_ar": act_msg, + "mode": mode.value, + "mode_name": mode.name, + "channel": channel, + "action": action, + } + + # Extra gates: regulated + high-touch channels + if is_regulated and channel == "whatsapp" and policy.auto_send: + return { + "level": "approval_required", + "reason_ar": "قطاع حساس: يُفضّل موافقة بشرية قبل إرسال واتساب.", + "mode": mode.value, + "mode_name": mode.name, + "channel": channel, + "action": action, + } + + if action in {"send_custom_message", "run_outreach_campaign"} and deal_value_sar > policy.max_auto_commitment_sar > 0: + return { + "level": "approval_required", + "reason_ar": "قيمة الصفقة تتجاوز حد الالتزام التلقائي لوضع التشغيل الحالي.", + "mode": mode.value, + "mode_name": mode.name, + "channel": channel, + "action": action, + } + + return { + "level": "auto_execute", + "reason_ar": "مسموح ضمن السياسات الحالية.", + "mode": mode.value, + "mode_name": mode.name, + "channel": channel, + "action": action, + } + + +def suggested_playbook_for_industry(industry: str | None) -> str | None: + """Pick a default vertical playbook id from coarse industry string.""" + ind = _normalize_industry(industry) + if not ind: + return None + if any(x in ind for x in ("عقار", "real", "estate", "property")): + return "real_estate" + if any(x in ind for x in ("صح", "health", "medical", "clinic", "hospital")): + return "healthcare" + if any(x in ind for x in ("saas", "software", "tech", "برمج")): + return "saas_b2b" + if any(x in ind for x in ("consult", "legal", "محاسب", "service")): + return "professional_services" + return None diff --git a/salesflow-saas/backend/app/services/dealix_os/vertical_playbooks.py b/salesflow-saas/backend/app/services/dealix_os/vertical_playbooks.py new file mode 100644 index 00000000..783142b2 --- /dev/null +++ b/salesflow-saas/backend/app/services/dealix_os/vertical_playbooks.py @@ -0,0 +1,61 @@ +""" +Vertical playbooks — sector defaults for ICP, channels, and governance hints. +طبقات إعداد قطاعية تغذي الوكلاء والواجهة دون تعديل كود لكل عميل. +""" + +from typing import Any + +VERTICAL_PLAYBOOKS: dict[str, dict[str, Any]] = { + "real_estate": { + "id": "real_estate", + "label_ar": "العقار والتطوير", + "label_en": "Real estate & development", + "icp_hints_ar": ["مطورون، وكلاء عقاريون، صناديق عقارية، إدارة أملاك"], + "primary_channels": ["whatsapp", "email"], + "objection_patterns_ar": ["العمولة", "حصرية الحي", "زمن البيع", "الترخيص"], + "approval_value_threshold_sar": 250_000, + "suggested_deal_types": ["channel_partnership", "referral_partnership", "co_marketing"], + "compliance_notes_ar": ["تأكد من إعلانات الهيئة العقارية عند الاقتضاء", "لا التزامات تسعير نهائي بدون موافقة"], + }, + "healthcare": { + "id": "healthcare", + "label_ar": "الرعاية الصحية", + "label_en": "Healthcare", + "icp_hints_ar": ["مستشفيات، عيادات، موردو أجهزة، منصات صحية"], + "primary_channels": ["email", "in_person"], + "objection_patterns_ar": ["الترخيص SFDA", "خصوصية البيانات", "التكامل مع أنظمة المستشفى"], + "approval_value_threshold_sar": 100_000, + "suggested_deal_types": ["subcontracting", "white_label", "strategic_alliance"], + "compliance_notes_ar": ["قطاع حساس — افتراض موافقة بشرية قبل إرسال واتساب", "تجنب ادعاءات علاجية في المحتوى الآلي"], + }, + "saas_b2b": { + "id": "saas_b2b", + "label_ar": "SaaS وB2B تقني", + "label_en": "B2B SaaS / technology", + "icp_hints_ar": ["شركات برمجيات، شركاء تكامل، موزعون، استشاريو تحول رقمي"], + "primary_channels": ["email", "linkedin", "whatsapp"], + "objection_patterns_ar": ["الأمان", "SLA", "التكامل", "التسعير حسب المقعد"], + "approval_value_threshold_sar": 150_000, + "suggested_deal_types": ["reseller", "co_selling", "white_label", "channel_partnership"], + "compliance_notes_ar": ["مراجعات DPA عند مشاركة بيانات عملاء"], + }, + "professional_services": { + "id": "professional_services", + "label_ar": "الخدمات المهنية", + "label_en": "Professional services", + "icp_hints_ar": ["محاسبة، قانون، استشارات إدارية، تدريب"], + "primary_channels": ["email", "linkedin"], + "objection_patterns_ar": ["نطاق الخدمة", "الاستشاري المسؤول", "السرية المهنية"], + "approval_value_threshold_sar": 80_000, + "suggested_deal_types": ["referral_partnership", "co_selling", "capability_gap_fill"], + "compliance_notes_ar": ["لا تقديم استشارة قانونية/مالية كأمر تنفيذي آلي"], + }, +} + + +def list_playbook_ids() -> list[str]: + return list(VERTICAL_PLAYBOOKS.keys()) + + +def get_playbook(playbook_id: str) -> dict[str, Any] | None: + return VERTICAL_PLAYBOOKS.get(playbook_id) diff --git a/salesflow-saas/backend/app/services/integration_probe.py b/salesflow-saas/backend/app/services/integration_probe.py new file mode 100644 index 00000000..6846c0c2 --- /dev/null +++ b/salesflow-saas/backend/app/services/integration_probe.py @@ -0,0 +1,73 @@ +"""Probe external integrations and persist status on IntegrationSyncState.""" + +from __future__ import annotations + +import logging +from uuid import UUID + +from sqlalchemy.ext.asyncio import AsyncSession + +from app.services.crm_sync_service import CRMSyncService +from app.services.operations_hub import upsert_connector_status + +logger = logging.getLogger("dealix.integration_probe") + + +async def probe_and_persist_crm_connectors(db: AsyncSession, tenant_id: UUID) -> None: + """Update crm_salesforce / crm_hubspot connector rows from live probes (best-effort).""" + svc = CRMSyncService(db) + tid = str(tenant_id) + + # Salesforce + try: + creds = await svc._get_crm_credentials(tid, "salesforce") + if not creds: + await upsert_connector_status( + db, tenant_id, "crm_salesforce", status="unknown", + ) + else: + probe = await svc.salesforce_identity_probe(creds) + if probe.get("ok"): + await upsert_connector_status( + db, tenant_id, "crm_salesforce", status="ok", success=True, + ) + else: + await upsert_connector_status( + db, + tenant_id, + "crm_salesforce", + status="error", + last_error=str(probe.get("detail") or probe)[:500], + ) + except Exception as e: + logger.exception("Salesforce probe failed") + await upsert_connector_status( + db, tenant_id, "crm_salesforce", status="error", last_error=str(e)[:500], + ) + + # HubSpot + try: + creds = await svc._get_crm_credentials(tid, "hubspot") + if not creds: + await upsert_connector_status( + db, tenant_id, "crm_hubspot", status="unknown", + ) + else: + probe = await svc.hubspot_identity_probe(creds.get("api_key", "")) + if probe.get("ok"): + await upsert_connector_status( + db, tenant_id, "crm_hubspot", status="ok", success=True, + ) + else: + await upsert_connector_status( + db, + tenant_id, + "crm_hubspot", + status="error", + last_error=str(probe.get("detail") or probe)[:500], + ) + except Exception as e: + logger.exception("HubSpot probe failed") + await upsert_connector_status( + db, tenant_id, "crm_hubspot", status="error", last_error=str(e)[:500], + ) diff --git a/salesflow-saas/backend/app/services/lead_service.py b/salesflow-saas/backend/app/services/lead_service.py index 30b9daa7..8aa447d6 100644 --- a/salesflow-saas/backend/app/services/lead_service.py +++ b/salesflow-saas/backend/app/services/lead_service.py @@ -39,12 +39,12 @@ class LeadService: lead = Lead( id=uuid.uuid4(), tenant_id=uuid.UUID(tenant_id), - full_name=full_name, + name=full_name, phone=phone, email=email, - company_name=company_name, - sector=sector, - city=city, + company_name=company_name or None, + sector=sector or None, + city=city or None, source=source, status="new", score=0, @@ -156,6 +156,47 @@ class LeadService: await self.db.flush() return self._to_dict(lead) + async def get_lead_by_email(self, tenant_id: str, email: str) -> Optional[dict]: + from app.models.lead import Lead + + if not (email or "").strip(): + return None + normalized = email.strip().lower() + result = await self.db.execute( + select(Lead) + .where( + Lead.tenant_id == uuid.UUID(tenant_id), + func.lower(Lead.email) == normalized, + ) + .limit(1) + ) + lead = result.scalars().first() + return self._to_dict(lead) if lead else None + + async def merge_lead_extra_metadata( + self, tenant_id: str, lead_id: str, crm_patch: dict + ) -> Optional[dict]: + """Merge into extra_metadata.crm (e.g. salesforce_lead_id, hubspot_contact_id).""" + from app.models.lead import Lead + + result = await self.db.execute( + select(Lead).where( + Lead.id == uuid.UUID(lead_id), + Lead.tenant_id == uuid.UUID(tenant_id), + ) + ) + lead = result.scalar_one_or_none() + if not lead: + return None + base = dict(lead.extra_metadata or {}) + crm = dict(base.get("crm") or {}) + crm.update(crm_patch) + base["crm"] = crm + lead.extra_metadata = base + lead.updated_at = datetime.now(timezone.utc) + await self.db.flush() + return self._to_dict(lead) + async def get_lead_by_phone(self, tenant_id: str, phone: str) -> Optional[dict]: from app.models.lead import Lead @@ -371,6 +412,7 @@ class LeadService: def _to_dict(lead) -> dict: if not lead: return {} + em = lead.extra_metadata if getattr(lead, "extra_metadata", None) else {} return { "id": str(lead.id), "tenant_id": str(lead.tenant_id), @@ -378,15 +420,16 @@ class LeadService: "source": lead.source, "status": lead.status, "score": lead.score, - "full_name": lead.full_name, + "full_name": lead.name, "phone": lead.phone, "email": lead.email, - "company_name": lead.company_name, - "sector": lead.sector, - "city": lead.city, + "company_name": getattr(lead, "company_name", None) or em.get("company_name", ""), + "sector": getattr(lead, "sector", None) or em.get("sector", ""), + "city": getattr(lead, "city", None) or em.get("city", ""), "notes": lead.notes, - "qualified_at": lead.qualified_at.isoformat() if lead.qualified_at else None, - "converted_at": lead.converted_at.isoformat() if lead.converted_at else None, + "extra_metadata": dict(em) if em else {}, + "qualified_at": lead.qualified_at.isoformat() if getattr(lead, "qualified_at", None) else None, + "converted_at": lead.converted_at.isoformat() if getattr(lead, "converted_at", None) else None, "created_at": lead.created_at.isoformat() if lead.created_at else None, "updated_at": lead.updated_at.isoformat() if lead.updated_at else None, } diff --git a/salesflow-saas/backend/app/services/operations_hub.py b/salesflow-saas/backend/app/services/operations_hub.py index 49067a89..48c9dca9 100644 --- a/salesflow-saas/backend/app/services/operations_hub.py +++ b/salesflow-saas/backend/app/services/operations_hub.py @@ -58,6 +58,7 @@ async def count_pending_approvals(db: AsyncSession, tenant_id: UUID) -> int: _DEFAULT_CONNECTORS: List[Dict[str, str]] = [ {"connector_key": "crm_salesforce", "display_name_ar": "Salesforce CRM", "status": "unknown"}, + {"connector_key": "crm_hubspot", "display_name_ar": "HubSpot CRM", "status": "unknown"}, {"connector_key": "whatsapp_cloud", "display_name_ar": "واتساب Cloud API", "status": "unknown"}, {"connector_key": "stripe_billing", "display_name_ar": "Stripe — الفوترة", "status": "unknown"}, {"connector_key": "email_sync", "display_name_ar": "مزامنة البريد", "status": "unknown"}, diff --git a/salesflow-saas/backend/app/services/salesforce_oauth.py b/salesflow-saas/backend/app/services/salesforce_oauth.py new file mode 100644 index 00000000..a566fbf4 --- /dev/null +++ b/salesflow-saas/backend/app/services/salesforce_oauth.py @@ -0,0 +1,53 @@ +"""Salesforce OAuth2 refresh-token flow for REST API calls.""" + +from __future__ import annotations + +import logging + +import httpx + +logger = logging.getLogger("dealix.salesforce_oauth") + + +def salesforce_token_url(domain_host: str) -> str: + d = (domain_host or "login.salesforce.com").strip().rstrip("/") + if d.startswith("http://") or d.startswith("https://"): + return f"{d}/services/oauth2/token" + return f"https://{d}/services/oauth2/token" + + +async def refresh_salesforce_access_token( + *, + domain_host: str, + client_id: str, + client_secret: str, + refresh_token: str, +) -> dict: + """ + Exchange refresh token for access_token + instance_url. + Returns dict with access_token, instance_url, and optional issued fields. + """ + url = salesforce_token_url(domain_host) + async with httpx.AsyncClient() as client: + response = await client.post( + url, + data={ + "grant_type": "refresh_token", + "client_id": client_id, + "client_secret": client_secret, + "refresh_token": refresh_token, + }, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + timeout=45.0, + ) + if response.status_code != 200: + logger.warning("Salesforce token refresh failed: %s", response.text[:500]) + response.raise_for_status() + data = response.json() + instance = (data.get("instance_url") or "").rstrip("/") + if not data.get("access_token") or not instance: + raise ValueError("Salesforce token response missing access_token or instance_url") + return { + "access_token": data["access_token"], + "instance_url": instance, + } diff --git a/salesflow-saas/backend/tests/conftest.py b/salesflow-saas/backend/tests/conftest.py index a93d0aa8..f0274e6d 100644 --- a/salesflow-saas/backend/tests/conftest.py +++ b/salesflow-saas/backend/tests/conftest.py @@ -1,9 +1,19 @@ import asyncio import os +from pathlib import Path # JWT-based API tests require this gate to be off (production may set .env). os.environ["DEALIX_INTERNAL_API_TOKEN"] = "" +# Fresh SQLite schema per pytest session — avoids stale ./dealix.db missing new columns. +_backend_root = Path(__file__).resolve().parent.parent +_test_sqlite = _backend_root / ".pytest_dealix.sqlite" +try: + _test_sqlite.unlink(missing_ok=True) +except OSError: + pass +os.environ["DATABASE_URL"] = "sqlite+aiosqlite:///" + _test_sqlite.resolve().as_posix() + import pytest import pytest_asyncio from httpx import AsyncClient, ASGITransport diff --git a/salesflow-saas/backend/tests/test_integrations_ai_routing_api.py b/salesflow-saas/backend/tests/test_integrations_ai_routing_api.py new file mode 100644 index 00000000..b7869514 --- /dev/null +++ b/salesflow-saas/backend/tests/test_integrations_ai_routing_api.py @@ -0,0 +1,80 @@ +"""Smoke tests for CRM status and AI routing (JWT bypass via dependency override).""" + +import uuid + +import pytest +import pytest_asyncio +from httpx import ASGITransport, AsyncClient +from sqlalchemy import select + +from app.api.deps import get_current_user +from app.database import async_session +from app.main import app +from app.models.tenant import Tenant +from app.models.user import User + + +@pytest_asyncio.fixture +async def owner_user_id(): + suffix = uuid.uuid4().hex[:10] + async with async_session() as db: + tenant = Tenant( + name=f"IntTest {suffix}", + slug=f"inttest-{suffix}", + email=f"tenant-{suffix}@example.com", + ) + db.add(tenant) + await db.flush() + user = User( + tenant_id=tenant.id, + email=f"owner-{suffix}@example.com", + password_hash="$2b$12$dummyNotForLoginxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + full_name="Owner", + role="owner", + ) + db.add(user) + await db.commit() + uid = str(user.id) + yield uid + + +def _user_override(user_id: str): + async def _dep(): + async with async_session() as db: + row = (await db.execute(select(User).where(User.id == user_id))).scalar_one() + return row + + return _dep + + +@pytest.mark.asyncio +async def test_integrations_crm_status_shape(owner_user_id): + app.dependency_overrides[get_current_user] = _user_override(owner_user_id) + try: + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as ac: + r = await ac.get("/api/v1/integrations/crm/status") + assert r.status_code == 200, r.text + data = r.json() + assert "salesforce" in data and "hubspot" in data + assert "env_refresh_configured" in data["salesforce"] + assert "docs" in data + finally: + app.dependency_overrides.pop(get_current_user, None) + + +@pytest.mark.asyncio +async def test_ai_routing_shape(owner_user_id): + app.dependency_overrides[get_current_user] = _user_override(owner_user_id) + try: + transport = ASGITransport(app=app) + async with AsyncClient(transport=transport, base_url="http://test") as ac: + r = await ac.get("/api/v1/ai/routing") + assert r.status_code == 200, r.text + data = r.json() + assert "effective" in data + assert "available_providers" in data + assert isinstance(data["effective"], dict) + assert "note_ar" in data + finally: + app.dependency_overrides.pop(get_current_user, None) diff --git a/salesflow-saas/docs/AGENT-MAP.md b/salesflow-saas/docs/AGENT-MAP.md index 181ec063..066dbc94 100644 --- a/salesflow-saas/docs/AGENT-MAP.md +++ b/salesflow-saas/docs/AGENT-MAP.md @@ -231,6 +231,17 @@ Action Handler Human Handoff Log to ai_conversations ``` +## LLM routing policy (per tenant) + +وكلاء الجدول أعلاه يستهلكون نماذج LLM عبر طبقة التطبيق. **اختيار المزود والنموذج لكل فئة مهمة** (مثل استكشاف، تفاوض، امتثال، ملخص استراتيجي، تضمينات) يُخزَّن في `tenant.settings["llm_routing"]` ويُعرض ويُحدَّث عبر واجهة موحّدة: + +| Method | Path | ملاحظة | +|--------|------|--------| +| GET | `/api/v1/ai/routing` | خريطة فعّالة + قائمة `available_providers` (بدون مفاتيح API) | +| PUT | `/api/v1/ai/routing` | تحديث جزئي لسياسة المستأجر (صلاحيات owner / manager / admin) | + +تفاصيل الحقول والمسارات المجاورة: [`API-MAP.md`](API-MAP.md) (قسم AI routing). عند إضافة وكيل جديد، اربط نوع مهمته بأقرب مفتاح في سياسة التوجيه حتى يبقى السلوك قابلاً للضبط من لوحة واحدة. + ## Agent Configuration Each agent is defined in `ai-agents/` with: diff --git a/salesflow-saas/docs/API-MAP.md b/salesflow-saas/docs/API-MAP.md index 6f0e3f03..b1280289 100644 --- a/salesflow-saas/docs/API-MAP.md +++ b/salesflow-saas/docs/API-MAP.md @@ -272,3 +272,62 @@ All routes are prefixed with `/api/v1`. Authentication is required unless marked | GET | `/health` | Basic health check `[public]` | | GET | `/health/ready` | Readiness (DB + Redis) `[public]` | | GET | `/health/version` | App version `[public]` | + +## Strategic Deals (Dealix OS / B2B) + +Prefix: `/strategic-deals`. Company profiles, matches, deals, negotiation, governance. + +| Method | Route | Description | +|--------|-------|-------------| +| GET | `/strategic-deals/profiles` | List tenant company profiles (paginated) | +| POST | `/strategic-deals/profiles` | Create company profile | +| PUT | `/strategic-deals/profiles/{id}/enrich` | AI-enrich profile | +| POST | `/strategic-deals/profiles/{id}/analyze-needs` | Needs analysis | +| GET | `/strategic-deals/matches` | List AI matches | +| POST | `/strategic-deals/matches/{id}/approve` | Approve match | +| POST | `/strategic-deals/scan` | Discovery scan | +| POST | `/strategic-deals/barter-scan` | Barter chain scan | +| POST | `/strategic-deals` | Create strategic deal (`lead_id`, `sales_deal_id` optional) | +| GET | `/strategic-deals` | List deals (filters: status, deal_type, profile_id) | +| GET | `/strategic-deals/operating-model` | Current OS mode + all modes + roles/SLA hints | +| PUT | `/strategic-deals/operating-model` | Set operating mode (0–4) on tenant profile | +| GET | `/strategic-deals/taxonomy/deal-types` | 15-type partnership taxonomy | +| GET | `/strategic-deals/taxonomy/deal-types/{type_id}` | One taxonomy type | +| GET | `/strategic-deals/partner-archetypes` | Map `deal_type` → operational archetypes | +| GET | `/strategic-deals/playbooks` | Vertical sector playbooks | +| GET | `/strategic-deals/playbooks/{id}` | One playbook | +| POST | `/strategic-deals/policy/evaluate` | Graded policy: auto_execute / approval_required / blocked | +| GET | `/strategic-deals/identity/graph` | Light account graph (`profile_id` query) | +| GET | `/strategic-deals/governance/snapshot` | Governance KPIs + operating mode | +| GET | `/strategic-deals/growth/checklist` | M&A-style checklist (guidance) | +| GET | `/strategic-deals/agent-quality/snapshot` | Agent quality proxy metrics | +| GET | `/strategic-deals/{deal_id}` | Deal detail | +| PATCH | `/strategic-deals/{deal_id}/links` | Link CRM `lead_id` / `sales_deal_id` | +| PUT | `/strategic-deals/{deal_id}/negotiate` | Negotiation round | +| POST | `/strategic-deals/{deal_id}/outreach` | Outreach | +| POST | `/strategic-deals/{deal_id}/proposal` | Generate proposal | +| POST | `/strategic-deals/{deal_id}/term-sheet` | Term sheet | +| GET | `/strategic-deals/analytics/overview` | Deal analytics | + +## Integrations (CRM) + +Prefix: `/integrations`. Salesforce / HubSpot sync and health (JWT). + +| Method | Route | Description | +|--------|-------|-------------| +| GET | `/integrations/crm/status` | Config presence + last probe (no secrets) | +| POST | `/integrations/crm/salesforce/test` | Probe Salesforce token/API | +| POST | `/integrations/crm/salesforce/push-lead/{lead_id}` | Push one lead to Salesforce | +| POST | `/integrations/crm/salesforce/pull-leads` | Pull leads (optional `since`) | +| POST | `/integrations/crm/hubspot/test` | Probe HubSpot API | +| POST | `/integrations/crm/hubspot/push-lead/{lead_id}` | Push one contact to HubSpot | +| POST | `/integrations/crm/hubspot/pull-contacts` | Pull contacts page | + +## AI routing (tenant) + +Prefix: `/ai`. Model routing policy per task category (no API keys in response). + +| Method | Route | Description | +|--------|-------|-------------| +| GET | `/ai/routing` | Effective routing map for current tenant | +| PUT | `/ai/routing` | Update tenant `settings.llm_routing` (owner/manager) | diff --git a/salesflow-saas/docs/COMPETITIVE_MATRIX_AR.md b/salesflow-saas/docs/COMPETITIVE_MATRIX_AR.md new file mode 100644 index 00000000..baba27a7 --- /dev/null +++ b/salesflow-saas/docs/COMPETITIVE_MATRIX_AR.md @@ -0,0 +1,35 @@ +# مصفوفة تنافسية — Dealix مقابل منصات الإيرادات والـ CRM + +هذا المستند يقارن **قدرات المنتج كما تظهر في المستودع** (واجهة، API، تكاملات، حوكمة) مع فئات أدوات السوق الشائعة. لا تُذكر أرقام سوق أو معدلات اعتماد غير مثبتة. + +## الصفوف: محاور Dealix + +| المحور | Dealix (ما يُثبت في الكود) | +|--------|---------------------------| +| حوكمة وإرسال | مسارات سياسات، موافقات، سجلات؛ تكامل مع `go-live-gate` ووثائق التشغيل | +| واتساب والقنوات المحلية | مسارات بريد/واتساب في التكاملات؛ تجربة عربية RTL في الواجهة | +| شراكات B2B | `strategic-deals`، Partnership Studio، هوية وصفقات استراتيجية | +| نظام تشغيل ثلاثي | أعمدة المبيعات / الشراكات / النمو في الداشبورد و`strategy_summary` | +| تكاملات CRM | Salesforce (OAuth + refresh + push/pull)، HubSpot (token + push/pull)، حالة في `integrations/crm` و`operations` | +| ذكاء متعدد المزودين | `GET/PUT /api/v1/ai/routing` — نماذج حسب نوع المهمة دون كشف مفاتيح | + +## الأعمدة: فئات منافسين (مرجعية) + +| الفئة | مثال مرجعي | ملاحظة مقارنة | +|-------|------------|----------------| +| Salesforce Sales Cloud | منصة CRM عالمية واسعة | Dealix لا يستبدل كل وحدات Salesforce؛ يُغطى **التنسيق والمزامنة** مع Leads/Contacts حسب التكامل الحالي | +| HubSpot CRM | تسويق وCRM مدمج | نفس نمط **الوجهة/المصدر** عبر API HubSpot في الخدمة الموحدة | +| منصات Revenue / GTM orchestration | أدلة مشتري عامة (Fullcast، Revenue.io، إلخ) | Dealix يركّز على **سوق سعودي أولاً**، حوكمة، ومسارات شراكات B2B ضمن منتج واحد | + +## فروقات قابلة للإثبات (بدون أرقام ادعائية) + +1. **شفافية تقنية:** مسارات API موثقة في `docs/API-MAP.md` ومطابقة OpenAPI عبر `scripts/verify_frontend_openapi_paths.py`. +2. **تكامل CRM قابل للتشغيل:** نقاط `test` و`push` و`pull` تحت `/api/v1/integrations/crm/`. +3. **سياسة نماذج:** توجيه المهام (`discovery`, `negotiation`, …) عبر `/api/v1/ai/routing` مع بقاء الأسرار في الخادم. +4. **قصة منتج موحدة:** `docs/DEALIX_OS_PRODUCT_GUIDE_AR.md` و`/strategy/summary` يربطان الواجهة بالوثائق. + +## روابط + +- [دليل التكاملات](INTEGRATION_MASTER_AR.md) +- [خريطة API](API-MAP.md) +- [قائمة الإطلاق](LAUNCH_CHECKLIST.md) diff --git a/salesflow-saas/docs/DEALIX_OS_PRODUCT_GUIDE_AR.md b/salesflow-saas/docs/DEALIX_OS_PRODUCT_GUIDE_AR.md new file mode 100644 index 00000000..13c159e3 --- /dev/null +++ b/salesflow-saas/docs/DEALIX_OS_PRODUCT_GUIDE_AR.md @@ -0,0 +1,39 @@ +# دليل منتج Dealix OS — مسارات المستخدمين + +يربط هذا الملف بين **واجهة الداشبورد**، **واجهات البرمجة**، والوثائق التفصيلية. للتكاملات والبيئة راجع [`INTEGRATION_MASTER_AR.md`](INTEGRATION_MASTER_AR.md) و[`LAUNCH_CHECKLIST.md`](LAUNCH_CHECKLIST.md). + +## 1) لمن هذا المنتج؟ + +| الجمهور | المحور في الداشبورد | مرجع تقني | +|--------|----------------------|-----------| +| الإدارة والمالك | المنصة والحوكمة | [`strategic_deals` operating-model](API-MAP.md)، [`go-live-gate`](../backend/app/services/go_live_matrix.py) | +| فريق المبيعات | محرك المبيعات | Leads، Pipeline، Inbox، الوكلاء | +| المسوقون والشركاء | مركز المسوق + المسوقين والموظفين | [`/affiliates/program`](../backend/app/api/v1/affiliates.py)، قسم `marketer-hub` | +| الشراكات B2B | الشراكات الاستراتيجية | [`/strategic-deals`](API-MAP.md)، Partnership Studio | +| الاستراتيجية والنمو | النمو والاستراتيجية | Intelligence، Growth checklist، [`autonomous_core`](../backend/app/services/autonomous_core.py) | + +## 2) الثلاثة أعمدة + الحوكمة + +- **مبيعات:** اكتشاف، قمع، قنوات، إغلاق — مع موافقات عند الإرسال الحساس. +- **شراكات:** ملفات شركات، مطابقة، تفاوض، ربط اختياري بـ CRM (`lead_id` / `sales_deal_id` على الصفقة الاستراتيجية). +- **نمو:** ذكاء استراتيجي وقوائم مهام؛ الالتزامات القانونية والمالية الكبرى **بشرية**. +- **حوكمة:** أوضاع تشغيل (0–4)، [`policy/evaluate`](API-MAP.md)، سجلات وتكاملات. + +مصدر نصي موحّد للرؤية: [`strategy/summary`](../backend/app/api/v1/strategy_summary.py) (`dealix_os_three_pillars`). + +## 3) وثائق يجب أن يعرفها فريق التنفيذ + +- خريطة الوكلاء: [`AGENT-MAP.md`](AGENT-MAP.md) +- نموذج البيانات: [`DATA-MODEL.md`](DATA-MODEL.md) +- مسار Enterprise: [`ENTERPRISE_ROADMAP.md`](ENTERPRISE_ROADMAP.md) +- مصفوفة تنافسية: [`COMPETITIVE_MATRIX_AR.md`](COMPETITIVE_MATRIX_AR.md) + +## 4) فحص تناغم الفرونت مع الـ API + +من جذر `salesflow-saas`: + +```bash +py -3 scripts/verify_frontend_openapi_paths.py +``` + +يُنصح بتشغيله قبل الإصدار؛ راجع أيضاً بنود [`LAUNCH_CHECKLIST.md`](LAUNCH_CHECKLIST.md). diff --git a/salesflow-saas/docs/ENTERPRISE_ROADMAP.md b/salesflow-saas/docs/ENTERPRISE_ROADMAP.md new file mode 100644 index 00000000..6fe66fb5 --- /dev/null +++ b/salesflow-saas/docs/ENTERPRISE_ROADMAP.md @@ -0,0 +1,34 @@ +# Dealix Enterprise roadmap + +Concise path from pilot to enterprise-grade deployment. This document complements `INTEGRATION_MASTER_AR.md` and the in-app go-live gate. + +## Phase 1 — Tenant isolation and audit (0–90 days) + +- Enforce tenant scoping on every strategic-deals and CRM read/write (verify RLS or equivalent on PostgreSQL). +- Structured audit log for: operating mode changes, outbound sends, policy blocks, and human approvals. +- Backup and restore runbook per tenant (or per region). + +## Phase 2 — Identity and access (90–180 days) + +- SSO (SAML/OIDC) for owner and compliance roles. +- SCIM or admin API for user lifecycle (optional). +- Role matrix aligned with UI: owner, RevOps, partner manager, compliance — mapped to API scopes. + +## Phase 3 — Data governance (180–365 days) + +- Data retention policies per tenant and per channel (PDPL-aware). +- Export and delete workflows for subject requests. +- Encryption at rest review; field-level encryption for highly sensitive notes if required. + +## Phase 4 — Scale and SLOs + +- Per-tenant rate limits on agent and strategic-deals endpoints. +- Observability: RED metrics on API, agent latency histograms, error budgets. +- Multi-region readiness assessment (data residency constraints). + +## Dependencies in this repo + +- Strategic deals and OS endpoints: `backend/app/api/v1/strategic_deals.py` +- Operating modes: `backend/app/services/strategic_deals/operating_modes.py` +- Policy evaluation: `backend/app/services/dealix_os/policy_engine.py` +- Frontend hubs: `frontend/src/app/dashboard/page.tsx` diff --git a/salesflow-saas/docs/LAUNCH_CHECKLIST.md b/salesflow-saas/docs/LAUNCH_CHECKLIST.md index 93d62c09..046e7ff2 100644 --- a/salesflow-saas/docs/LAUNCH_CHECKLIST.md +++ b/salesflow-saas/docs/LAUNCH_CHECKLIST.md @@ -7,7 +7,10 @@ - [ ] `cd frontend && npm run lint && npm run build` (أو تُغطّى بواسطة `verify-launch.ps1`). - [ ] من جذر `salesflow-saas`: `node scripts/sync-marketing-to-public.cjs` (يُشغَّل أيضاً تلقائياً قبل `npm run build`). - [ ] **E2E (Playwright):** بعد `npm run build`، حرّر المنفذ **3000** ثم من `frontend/`: `CI=true npm run test:e2e`. إن ظهر «port already in use» أو timeout على `webServer`: من جذر `salesflow-saas` شغّل `.\scripts\kill-port-3000.ps1` ثم أعد المحاولة. -- [ ] (اختياري) من جذر `salesflow-saas`: `py -3 scripts/verify_frontend_openapi_paths.py` (أو `python3 scripts/...`) — يطابق مسارات `/api/v1` في الفرونت مع OpenAPI (حرفيًا وفي قوالب مثل `` `${base}/api/v1/...` ``). +- [ ] (موصى به) من جذر `salesflow-saas`: `py -3 scripts/verify_frontend_openapi_paths.py` (أو `python3 scripts/...`) — يطابق مسارات `/api/v1` في الفرونت مع OpenAPI (حرفيًا وفي قوالب مثل `` `${base}/api/v1/...` ``). يشمل استدعاءات `strategic-deals` و`integrations/crm` و`ai/routing` عند إضافتها في الواجهة. +- [ ] مراجعة [`docs/API-MAP.md`](API-MAP.md) مقابل OpenAPI الفعلي (`/docs` على الخادم) بعد أي إصدار يضيف مسارات جديدة. +- [ ] قراءة سريعة لـ [`docs/DEALIX_OS_PRODUCT_GUIDE_AR.md`](DEALIX_OS_PRODUCT_GUIDE_AR.md) للتأكد من توافق قصة المنتج مع الداشبورد. +- [ ] (موصى به) تشغيل سيناريو محاكاة الإطلاق في [`docs/LAUNCH_SIMULATION.md`](LAUNCH_SIMULATION.md) وتسجيل نتيجة `go-live-gate` لكل بيئة. ## 2. الخادم (API) diff --git a/salesflow-saas/docs/LAUNCH_SIMULATION.md b/salesflow-saas/docs/LAUNCH_SIMULATION.md new file mode 100644 index 00000000..d1e94497 --- /dev/null +++ b/salesflow-saas/docs/LAUNCH_SIMULATION.md @@ -0,0 +1,34 @@ +# محاكاة إطلاق Dealix (Staging → جاهزية) + +وثيقة قصيرة لتشغيل **سيناريو إطلاق** يدوياً والتسجيل في سجل التشغيل. تكمّل [`LAUNCH_CHECKLIST.md`](LAUNCH_CHECKLIST.md). + +## 1. التحضير + +1. فرع كود محدث؛ قاعدة بيانات متوافقة مع آخر هجرات Alembic. +2. نسخ متغيرات البيئة من الأمثلة (`backend/.env`، `frontend/.env.local` / `NEXT_PUBLIC_API_URL`). +3. من جذر `salesflow-saas`: + `node scripts/sync-marketing-to-public.cjs` + +## 2. البناء والتحقق + +1. `.\verify-launch.ps1` (أو pytest + lint + build يدوياً كما في قائمة الإطلاق). +2. `py -3 scripts/verify_frontend_openapi_paths.py` +3. تشغيل API: `py -3 -m uvicorn app.main:app --host 127.0.0.1 --port 8000` من `backend/`. + +## 3. بوابة go-live + +1. استدعاء `GET /api/v1/autonomous-foundation/integrations/go-live-gate` (مع JWT إن لزم حسب البيئة). +2. تسجيل: `launch_allowed`، `blocked_reasons`، و`blocking` في ملاحظات الإصدار. + +## 4. فحوص تكامل (رملي حيث أمكن) + +| النظام | فعل مقترح | نتيجة متوقعة | +|--------|-----------|---------------| +| CRM Salesforce | `POST /api/v1/integrations/crm/salesforce/test` | `ok: true` أو رسالة خطأ واضحة | +| CRM HubSpot | `POST /api/v1/integrations/crm/hubspot/test` | كما فوق | +| واجهة | تبويب الإعدادات → تكاملات؛ مركز المسوق في الداشبورد | تحميل الحالة بدون أخطاء console حرجة | + +## 5. الخاتمة + +- وثّق التاريخ، البيئة (staging)، ونسخة الـ commit. +- أي فشل: أضف بنداً في `LAUNCH_CHECKLIST` أو issue مع `blocked_reasons` المنسوخة من الـ API. diff --git a/salesflow-saas/frontend/public/dealix-marketing/LOCAL-ONLY-NEXT.txt b/salesflow-saas/frontend/public/dealix-marketing/LOCAL-ONLY-NEXT.txt index cbbd1e50..1d310048 100644 --- a/salesflow-saas/frontend/public/dealix-marketing/LOCAL-ONLY-NEXT.txt +++ b/salesflow-saas/frontend/public/dealix-marketing/LOCAL-ONLY-NEXT.txt @@ -9,6 +9,8 @@ http://localhost:3000/dealix-presentations/ http://localhost:3000/resources http://localhost:3000/strategy + http://localhost:3000/strategy/legal/ (وثائق قانونية بعد المزامنة) + http://localhost:3000/strategy/COMPETITIVE_MATRIX_AR.md لتحديث النسخ بعد تعديل الملفات الأصلية: node scripts/sync-marketing-to-public.cjs diff --git a/salesflow-saas/frontend/public/dealix-presentations/investor-deck.md b/salesflow-saas/frontend/public/dealix-presentations/investor-deck.md deleted file mode 100644 index cfa207c1..00000000 --- a/salesflow-saas/frontend/public/dealix-presentations/investor-deck.md +++ /dev/null @@ -1,217 +0,0 @@ -# Dealix — Investor Pitch Deck -# ديلكس — عرض المستثمرين - ---- - -## Slide 1: Cover - -# Dealix | ديلكس -### نظام المبيعات الذكي للسعودية -### The Smart Sales System for Saudi Arabia - -**AI-Powered Revenue + Partnership + Strategic Deal OS** - ---- - -## Slide 2: Problem | المشكلة - -### الشركات السعودية تخسر ملايين بسبب: - -| المشكلة | الأثر | -|---------|-------| -| **٧٠٪ من العملاء المحتملين يضيعون** | بسبب عدم المتابعة | -| **فوضى الواتساب** | رسائل ضايعة، ما تعرف مين رد | -| **لا يوجد CRM عربي ذكي** | الأنظمة الأجنبية مو مصممة للسوق السعودي | -| **فرص شراكات ضائعة** | لا يوجد نظام يكتشف ويفاوض الصفقات تلقائياً | -| **غرامات PDPL** | حتى ٥ مليون ر.س لكل مخالفة | - -**السوق يحتاج نظام مبني للسعودية، عربي أولاً، ذكي فعلاً.** - ---- - -## Slide 3: Solution | الحل - -### Dealix = 4 طبقات متكاملة - -``` -Layer 3: Strategic Growth OS - → رصد استحواذات، خريطة شركاء، محاكاة ROI - -Layer 2: Deal Exchange OS - → مطابقة شركاء، تبادل خدمات، غرف صفقات - -Layer 1: Sales OS - → ليدات، تواصل واتساب ذكي، عروض أسعار، pipeline - -Layer 0: Core Platform - → Company Twin، حوكمة، PDPL، ذاكرة تجارية -``` - -**أول منصة سعودية تجمع بين المبيعات والشراكات والنمو الاستراتيجي.** - ---- - -## Slide 4: Market | السوق - -### سوق CRM السعودي - -| المقياس | الرقم | -|---------|-------| -| **حجم السوق ٢٠٢٤** | $652M | -| **المتوقع ٢٠٣٣** | $1.46B | -| **معدل النمو** | 9.4% CAGR | -| **سوق AI السعودي ٢٠٢٦** | $680M | -| **المتوقع ٢٠٣١** | $2.8B (32.9% CAGR) | -| **عدد الشركات المسجلة** | 1.2M+ | -| **استخدام WhatsApp** | 85%+ (30M+ مستخدم) | - -### رؤية 2030 تدفع التحول الرقمي -- استثمار Salesforce $500M في الرياض -- AWS + Humain: 150,000 AI accelerator في الرياض -- SDAIA تقود حوكمة الذكاء الاصطناعي - ---- - -## Slide 5: Product | المنتج - -### ميزات رئيسية - -| الميزة | الوصف | -|--------|-------| -| 🤖 **AI عربي** | NLP يفهم اللهجة السعودية + تقييم ذكي للعملاء | -| 📱 **واتساب ذكي** | بوت يبيع ويدعم ويتفاوض بالعربي | -| 🔄 **صفقات استراتيجية** | مطابقة شركاء + تبادل خدمات + مفاوض AI | -| 📊 **Pipeline بصري** | Kanban مع drag-and-drop | -| 💰 **عروض أسعار** | CPQ مع ضريبة القيمة المضافة تلقائياً | -| 🛡️ **PDPL مدمج** | موافقات + حقوق بيانات + audit trail | -| 📈 **تنبؤات مبيعات** | AI forecasting بالعربي | -| 🌍 **ثنائي اللغة** | عربي/إنجليزي بتبديل فوري | - ---- - -## Slide 6: Technology | التقنية - -### البنية التقنية - -- **Backend**: FastAPI + Python 3.12 (async) -- **Frontend**: Next.js 15 + React 19 + TypeScript -- **Database**: PostgreSQL 16 + Redis + pgvector -- **AI**: Groq + OpenAI + Arabic NLP (camel-tools) -- **Orchestration**: Hermes + OpenClaw 2026.4.11 -- **Channels**: WhatsApp Business API + Email + LinkedIn -- **Infrastructure**: Docker + Nginx + Celery - -### AI Stack -- Arabic NLP with Saudi dialect detection -- AI Lead Scoring (0-100, 4 dimensions) -- Conversation Intelligence (Arabic dialogue analysis) -- Autonomous Sales Agent (WhatsApp qualification bot) -- Strategic Deal Negotiator (multi-round Arabic negotiation) - ---- - -## Slide 7: Business Model | نموذج العمل - -### SaaS Subscription - -| الباقة | السعر/شهر | المستهدف | -|--------|-----------|----------| -| **Starter** | 59 ر.س | شركات صغيرة (1-3 مستخدمين) | -| **Professional** | 149 ر.س | شركات متوسطة (1-10 مستخدمين) | -| **Enterprise** | 225 ر.س | شركات كبيرة (لا محدود) | - -### مصادر إيراد إضافية -- **Success Fees**: 1-3% من قيمة الصفقات المكتملة عبر Deal Exchange -- **Marketplace**: عمولة على شراكات ناجحة -- **API Access**: للمطورين والتكاملات -- **Premium Support**: دعم مخصص للمؤسسات - ---- - -## Slide 8: Go-to-Market | خطة الذهاب للسوق - -### Phase A: Real Estate (الآن) -- 20,000+ وكالة عقارية في الرياض -- متوسط قيمة الصفقة: 500K-5M ر.س -- WhatsApp = قناة التواصل الأساسية - -### Phase B: Healthcare (شهر 3-6) -- عيادات وأسنان ومراكز طبية -- حجوزات + متابعة مرضى - -### Phase C: Contracting (شهر 6-12) -- مقاولات وخدمات -- عروض أسعار + متابعة مشاريع - -### استراتيجية الاكتساب -- Cold outreach (email-first) -- Content marketing (Arabic SEO) -- WhatsApp viral (referral program) -- LinkedIn thought leadership -- Saudi Chamber partnerships - ---- - -## Slide 9: Competition | المنافسة - -### لماذا Dealix يتفوق - -| الميزة | Dealix | Salesforce | Zoho | HubSpot | -|--------|--------|------------|------|---------| -| عربي أولاً | ✅ | ❌ | ⚠️ | ❌ | -| واتساب مدمج | ✅ | ❌ | ⚠️ | ❌ | -| AI عربي | ✅ | ❌ | ❌ | ❌ | -| PDPL مدمج | ✅ | ❌ | ❌ | ❌ | -| صفقات استراتيجية | ✅ | ❌ | ❌ | ❌ | -| السعر/مستخدم/شهر | 59 ر.س | 656 ر.س | 52 ر.س | 562 ر.س | -| سيرفر سعودي | ✅ | ❌ | ✅ | ❌ | - -**لا يوجد منافس يجمع: AI عربي + WhatsApp أساسي + PDPL + صفقات استراتيجية** - ---- - -## Slide 10: Revenue Projections | التوقعات المالية - -| الفترة | عملاء | MRR | ARR | -|--------|-------|-----|-----| -| شهر 1 | 5 | 745 ر.س | - | -| شهر 3 | 25 | 3,725 ر.س | - | -| شهر 6 | 100 | 14,900 ر.س | 178K ر.س | -| سنة 1 | 300 | 44,700 ر.س | 536K ر.س | -| سنة 2 | 1,000 | 149,000 ر.س | 1.79M ر.س | -| سنة 3 | 5,000 | 745,000 ر.س | 8.94M ر.س | - ---- - -## Slide 11: Ask | الطلب - -### نبحث عن: جولة Pre-Seed / Seed - -| البند | المبلغ | -|-------|--------| -| **المطلوب** | 2-5M ر.س | -| **التقييم** | 15-25M ر.س (pre-money) | -| **الاستخدام** | التطوير (40%) + التسويق (30%) + التشغيل (20%) + احتياطي (10%) | - -### المعالم (12 شهر) -- 1,000 عميل مدفوع -- 1.79M ر.س ARR -- 3 قطاعات مغطاة -- Deal Exchange OS مُطلق -- فريق 8-12 شخص - ---- - -## Slide 12: Contact | تواصل - -### Dealix | ديلكس -- **الموقع**: dealix.sa -- **الإيميل**: invest@dealix.sa -- **الواتساب**: +966 5X XXX XXXX -- **LinkedIn**: linkedin.com/company/dealix-sa - -> "نبني أول نظام تجاري ذكي مصمم للسعودية" - ---- - -*صنع بحب في السعودية 🇸🇦* diff --git a/salesflow-saas/frontend/public/favicon.svg b/salesflow-saas/frontend/public/favicon.svg index 471c365a..17a28bf8 100644 --- a/salesflow-saas/frontend/public/favicon.svg +++ b/salesflow-saas/frontend/public/favicon.svg @@ -1,13 +1,18 @@ - + - - - + + + + + + + - - - + + + + + + diff --git a/salesflow-saas/frontend/public/logo.svg b/salesflow-saas/frontend/public/logo.svg index c1169e42..d50ed3cb 100644 --- a/salesflow-saas/frontend/public/logo.svg +++ b/salesflow-saas/frontend/public/logo.svg @@ -1,20 +1,32 @@ - + - - - + + + - - - + + + + + + + - - - - - - + + + + + + + + + + + + + + diff --git a/salesflow-saas/frontend/public/strategy/COMPETITIVE_MATRIX_AR.md b/salesflow-saas/frontend/public/strategy/COMPETITIVE_MATRIX_AR.md new file mode 100644 index 00000000..baba27a7 --- /dev/null +++ b/salesflow-saas/frontend/public/strategy/COMPETITIVE_MATRIX_AR.md @@ -0,0 +1,35 @@ +# مصفوفة تنافسية — Dealix مقابل منصات الإيرادات والـ CRM + +هذا المستند يقارن **قدرات المنتج كما تظهر في المستودع** (واجهة، API، تكاملات، حوكمة) مع فئات أدوات السوق الشائعة. لا تُذكر أرقام سوق أو معدلات اعتماد غير مثبتة. + +## الصفوف: محاور Dealix + +| المحور | Dealix (ما يُثبت في الكود) | +|--------|---------------------------| +| حوكمة وإرسال | مسارات سياسات، موافقات، سجلات؛ تكامل مع `go-live-gate` ووثائق التشغيل | +| واتساب والقنوات المحلية | مسارات بريد/واتساب في التكاملات؛ تجربة عربية RTL في الواجهة | +| شراكات B2B | `strategic-deals`، Partnership Studio، هوية وصفقات استراتيجية | +| نظام تشغيل ثلاثي | أعمدة المبيعات / الشراكات / النمو في الداشبورد و`strategy_summary` | +| تكاملات CRM | Salesforce (OAuth + refresh + push/pull)، HubSpot (token + push/pull)، حالة في `integrations/crm` و`operations` | +| ذكاء متعدد المزودين | `GET/PUT /api/v1/ai/routing` — نماذج حسب نوع المهمة دون كشف مفاتيح | + +## الأعمدة: فئات منافسين (مرجعية) + +| الفئة | مثال مرجعي | ملاحظة مقارنة | +|-------|------------|----------------| +| Salesforce Sales Cloud | منصة CRM عالمية واسعة | Dealix لا يستبدل كل وحدات Salesforce؛ يُغطى **التنسيق والمزامنة** مع Leads/Contacts حسب التكامل الحالي | +| HubSpot CRM | تسويق وCRM مدمج | نفس نمط **الوجهة/المصدر** عبر API HubSpot في الخدمة الموحدة | +| منصات Revenue / GTM orchestration | أدلة مشتري عامة (Fullcast، Revenue.io، إلخ) | Dealix يركّز على **سوق سعودي أولاً**، حوكمة، ومسارات شراكات B2B ضمن منتج واحد | + +## فروقات قابلة للإثبات (بدون أرقام ادعائية) + +1. **شفافية تقنية:** مسارات API موثقة في `docs/API-MAP.md` ومطابقة OpenAPI عبر `scripts/verify_frontend_openapi_paths.py`. +2. **تكامل CRM قابل للتشغيل:** نقاط `test` و`push` و`pull` تحت `/api/v1/integrations/crm/`. +3. **سياسة نماذج:** توجيه المهام (`discovery`, `negotiation`, …) عبر `/api/v1/ai/routing` مع بقاء الأسرار في الخادم. +4. **قصة منتج موحدة:** `docs/DEALIX_OS_PRODUCT_GUIDE_AR.md` و`/strategy/summary` يربطان الواجهة بالوثائق. + +## روابط + +- [دليل التكاملات](INTEGRATION_MASTER_AR.md) +- [خريطة API](API-MAP.md) +- [قائمة الإطلاق](LAUNCH_CHECKLIST.md) diff --git a/salesflow-saas/frontend/public/strategy/legal/affiliate-rules-ar.md b/salesflow-saas/frontend/public/strategy/legal/affiliate-rules-ar.md new file mode 100644 index 00000000..df494f3a --- /dev/null +++ b/salesflow-saas/frontend/public/strategy/legal/affiliate-rules-ar.md @@ -0,0 +1,34 @@ +# قواعد المسوقين بالعمولة - Dealix + +## ما هو مسموح +- التعريف بنفسك كمستشار مبيعات في Dealix +- مشاركة المواد التسويقية المعتمدة +- التواصل مع العملاء المحتملين بطريقة مهنية +- استخدام السكربتات والبرزنتيشنات المقدمة +- العمل في أي وقت ومن أي مكان +- إحالة مسوقين آخرين + +## ما هو محظور +- انتحال صفة مؤسس أو مدير الشركة +- تقديم وعود غير مكتوبة في السياسات الرسمية +- إرسال رسائل جماعية مزعجة (spam) +- مشاركة بيانات عملاء آخرين +- تسجيل عملاء وهميين أو مكررين +- التلاعب في بيانات الإحالة +- إساءة استخدام اسم الشركة +- مخالفة سياسات التواصل المعتمدة + +## العقوبات +| المخالفة | العقوبة | +|---------|--------| +| مخالفة أولى بسيطة | تحذير كتابي | +| مخالفة ثانية | تجميد العمولات 30 يوم + تحذير نهائي | +| مخالفة ثالثة أو مخالفة جسيمة | إنهاء العلاقة فوراً | +| احتيال مثبت | إنهاء + استرجاع عمولات + إجراء قانوني | + +## حقوق المسوق +- الاطلاع على كشف العمولات الشهري +- الاعتراض على أي حساب خاطئ +- الحصول على التدريب والأدوات المحدثة +- الترقية للتوظيف عند تحقيق الأهداف +- إنهاء العلاقة في أي وقت مع استلام العمولات المستحقة diff --git a/salesflow-saas/frontend/public/strategy/legal/commission-policy-ar.md b/salesflow-saas/frontend/public/strategy/legal/commission-policy-ar.md new file mode 100644 index 00000000..4f79299f --- /dev/null +++ b/salesflow-saas/frontend/public/strategy/legal/commission-policy-ar.md @@ -0,0 +1,37 @@ +# سياسة العمولات - Dealix + +## هيكل العمولات + +| الباقة | السعر | نسبة العمولة (مسوق حر) | نسبة العمولة (موظف) | +|--------|-------|----------------------|-------------------| +| أساسي | 299 ر.س | 15% (~45 ر.س) | 20% (~60 ر.س) | +| احترافي | 699 ر.س | 20% (~140 ر.س) | 25% (~175 ر.س) | +| مؤسسات | 1,499 ر.س | 25% (~375 ر.س) | 30% (~450 ر.س) | + +## دورة حياة العمولة +1. **مسودة (Draft)**: عند تسجيل الصفقة +2. **معلقة (Pending)**: بعد تأكيد الصفقة +3. **معتمدة (Approved)**: بعد تأكيد دفع العميل +4. **مجمدة (Held)**: في حال وجود نزاع أو مراجعة +5. **مدفوعة (Paid)**: بعد التحويل للمسوق +6. **مسترجعة (Clawback)**: إذا استرجع العميل خلال فترة الضمان + +## المكافآت الشهرية +- 5 شركات/شهر = 500 ر.س بونس +- 10 شركات/شهر = 1,500 ر.س بونس + أهلية التوظيف +- 15+ شركات/شهر = 3,000 ر.س بونس + +## موعد الدفع +- تُحسب العمولات في بداية كل شهر ميلادي +- تُدفع خلال أول 10 أيام عمل من الشهر التالي +- الحد الأدنى للصرف: 100 ر.س + +## قواعد الإسناد (Attribution) +- العميل يُنسب للمسوق الأول الذي سجله في النظام +- صلاحية الإسناد: 90 يوم من تاريخ التسجيل +- في حال تعدد المسوقين: الأولوية للأول مع إمكانية التقسيم بقرار إداري + +## النزاعات +- يحق للمسوق الاعتراض خلال 14 يوم من نشر كشف العمولات +- المراجعة خلال 5 أيام عمل +- القرار نهائي مع حق الاستئناف مرة واحدة diff --git a/salesflow-saas/frontend/public/strategy/legal/consent-policy-ar.md b/salesflow-saas/frontend/public/strategy/legal/consent-policy-ar.md new file mode 100644 index 00000000..edbad7f7 --- /dev/null +++ b/salesflow-saas/frontend/public/strategy/legal/consent-policy-ar.md @@ -0,0 +1,35 @@ +# سياسة الموافقة والاشتراك - Dealix + +## مبدأ عام +لا يتم التواصل مع أي شخص عبر أي قناة إلا بموافقته المسبقة الصريحة. + +## أنواع الموافقة + +### واتساب +- يجب الحصول على opt-in صريح قبل إرسال أي رسالة +- الموافقة تُسجل مع التاريخ والمصدر +- حق الانسحاب (opt-out) يُنفذ فوراً +- يُستخدم فقط قوالب معتمدة من Meta للرسائل الأولى + +### البريد الإلكتروني +- رابط إلغاء الاشتراك إلزامي في كل رسالة +- opt-in عند التسجيل أو تعبئة نموذج +- لا يُرسل أكثر من 3 رسائل بدون رد قبل التوقف + +### الرسائل النصية (SMS) +- موافقة مسبقة مطلوبة +- تُستخدم فقط للإشعارات الضرورية + +### المكالمات الصوتية +- لا يُتصل بدون سبب مشروع (عميل محتمل مسجل) +- تسجيل المكالمات يتطلب إبلاغ وموافقة + +## سجل الموافقة +- كل موافقة تُسجل بـ: التاريخ، القناة، المصدر، IP (إن توفر) +- كل انسحاب يُنفذ خلال 24 ساعة كحد أقصى +- السجلات تُحفظ 36 شهر + +## حقوق صاحب البيانات +- الوصول لسجل الموافقات الخاص به +- سحب الموافقة في أي وقت لأي قناة +- تقديم شكوى في حال مخالفة diff --git a/salesflow-saas/frontend/public/strategy/legal/data-protection-ar.md b/salesflow-saas/frontend/public/strategy/legal/data-protection-ar.md new file mode 100644 index 00000000..e1a6df8b --- /dev/null +++ b/salesflow-saas/frontend/public/strategy/legal/data-protection-ar.md @@ -0,0 +1,36 @@ +# حماية البيانات - Dealix + +## الامتثال لنظام حماية البيانات الشخصية (PDPL) + +### المبادئ الأساسية +1. **تقليل البيانات**: نجمع فقط البيانات الضرورية لتقديم الخدمة +2. **تحديد الغرض**: كل بيان يُجمع لغرض محدد ومعلن +3. **الشفافية**: سياسات واضحة ومتاحة للجميع +4. **الأمان**: حماية تقنية وتنظيمية مناسبة +5. **المساءلة**: سجلات تدقيق لكل عملية + +### التدابير التقنية +- **التشفير**: TLS 1.3 للنقل، AES-256 للتخزين +- **النسخ الاحتياطي**: يومي، مشفر، خارج الموقع +- **الصلاحيات**: نظام أدوار وصلاحيات (RBAC) +- **المراقبة**: رصد مستمر للوصول غير المصرح +- **التدقيق**: سجل لكل عملية وصول أو تعديل + +### حقوق أصحاب البيانات +- **الوصول**: طلب نسخة من بياناتك خلال 30 يوم +- **التصحيح**: طلب تعديل بيانات غير صحيحة +- **الحذف**: طلب حذف بياناتك (حق النسيان) +- **النقل**: طلب نقل بياناتك بصيغة قابلة للقراءة +- **الاعتراض**: الاعتراض على أي معالجة + +### تقديم طلب +1. أرسل طلبك عبر: privacy@dealix.sa +2. حدد نوع الطلب (وصول/تصحيح/حذف/نقل) +3. إشعار استلام خلال 48 ساعة +4. تنفيذ خلال 30 يوم كحد أقصى + +### الإبلاغ عن خرق +في حال اكتشاف خرق للبيانات: +- إبلاغ الجهات المختصة خلال 72 ساعة +- إبلاغ المتضررين خلال 72 ساعة +- توثيق الخرق والإجراءات المتخذة diff --git a/salesflow-saas/frontend/public/strategy/legal/privacy-policy-ar.md b/salesflow-saas/frontend/public/strategy/legal/privacy-policy-ar.md new file mode 100644 index 00000000..81a4c37d --- /dev/null +++ b/salesflow-saas/frontend/public/strategy/legal/privacy-policy-ar.md @@ -0,0 +1,57 @@ +# سياسة الخصوصية - Dealix (ديل اي اكس) + +**تاريخ السريان:** 2026-03-31 +**آخر تحديث:** 2026-03-31 + +## 1. مقدمة +تلتزم Dealix (ديل اي اكس) بحماية خصوصية مستخدميها وعملائها وفقاً لنظام حماية البيانات الشخصية (PDPL) في المملكة العربية السعودية. + +## 2. البيانات التي نجمعها +- **بيانات الحساب**: الاسم، البريد الإلكتروني، رقم الجوال، اسم الشركة، القطاع +- **بيانات الاستخدام**: سجلات الدخول، الصفحات المزارة، الإجراءات المنفذة +- **بيانات التواصل**: سجلات المحادثات، الرسائل، المكالمات (بموافقة مسبقة) +- **بيانات الدفع**: معلومات الفوترة (تُعالج عبر مزود دفع آمن) + +## 3. كيف نستخدم بياناتك +- تقديم وتحسين خدماتنا +- التواصل معك بخصوص حسابك +- إرسال إشعارات الخدمة والتحديثات +- تحليل الاستخدام لتحسين المنصة +- الامتثال للمتطلبات القانونية + +## 4. مشاركة البيانات +لا نبيع بياناتك الشخصية. قد نشاركها مع: +- مزودي الخدمة (استضافة، بريد، رسائل) بعقود حماية +- الجهات الرسمية عند الطلب القانوني +- شركاء الأعمال بموافقتك المسبقة + +## 5. حقوقك +لك الحق في: +- الوصول لبياناتك الشخصية +- تصحيح بياناتك +- حذف بياناتك +- الاعتراض على المعالجة +- نقل بياناتك +- سحب الموافقة في أي وقت + +## 6. أمن البيانات +- تشفير كامل للبيانات أثناء النقل والتخزين (TLS 1.3 + AES-256) +- نسخ احتياطية يومية مشفرة +- صلاحيات وصول محددة حسب الدور +- مراقبة أمنية مستمرة +- سجلات تدقيق لكل عملية وصول + +## 7. الاحتفاظ بالبيانات +- بيانات الحساب: طوال مدة الاشتراك + 12 شهر بعد الإلغاء +- سجلات التواصل: 24 شهر +- سجلات التدقيق: 36 شهر +- بيانات الدفع: حسب المتطلبات الضريبية + +## 8. ملفات الارتباط (Cookies) +نستخدم ملفات ارتباط ضرورية لتشغيل المنصة وتحسين تجربتك. + +## 9. التواصل +لأي استفسارات تتعلق بالخصوصية: privacy@dealix.sa + +## 10. التعديلات +نحتفظ بحق تعديل هذه السياسة مع إشعار مسبق 30 يوم. diff --git a/salesflow-saas/frontend/public/strategy/legal/refund-policy-ar.md b/salesflow-saas/frontend/public/strategy/legal/refund-policy-ar.md new file mode 100644 index 00000000..e42fc2ef --- /dev/null +++ b/salesflow-saas/frontend/public/strategy/legal/refund-policy-ar.md @@ -0,0 +1,28 @@ +# سياسة الاسترجاع والضمان الذهبي - Dealix + +## الضمان الذهبي (30 يوم) + +### الوعد +إذا استخدمت Dealix لمدة 30 يوم ولم تشهد تحسناً في عمليات مبيعاتك، نسترجع لك المبلغ كاملاً. + +### شروط الأهلية +1. استخدام المنصة لمدة 14 يوم متواصل على الأقل +2. إدخال 20 عميل محتمل كحد أدنى +3. إرسال 50 رسالة كحد أدنى عبر المنصة +4. حضور جلسة التدريب الأولية + +### الاستثناءات +- الحسابات المجمدة بسبب مخالفات +- الاستخدام لمرة واحدة فقط لكل شركة +- لا يسري على الاشتراكات التجريبية المجانية + +### إجراءات الطلب +1. تقديم طلب الاسترجاع عبر الدعم الفني +2. إشعار استلام خلال 24 ساعة +3. مراجعة الاستخدام خلال 3 أيام عمل +4. القرار خلال 5 أيام عمل +5. التنفيذ خلال 7 أيام عمل من الموافقة + +### طريقة الاسترجاع +- نفس طريقة الدفع الأصلية +- تحويل بنكي إذا تعذرت الطريقة الأصلية diff --git a/salesflow-saas/frontend/public/strategy/legal/terms-of-service-ar.md b/salesflow-saas/frontend/public/strategy/legal/terms-of-service-ar.md new file mode 100644 index 00000000..f11590ca --- /dev/null +++ b/salesflow-saas/frontend/public/strategy/legal/terms-of-service-ar.md @@ -0,0 +1,54 @@ +# شروط الخدمة - Dealix (ديل اي اكس) + +**تاريخ السريان:** 2026-03-31 + +## 1. التعريفات +- **المنصة**: منصة Dealix لأتمتة المبيعات بالذكاء الاصطناعي +- **المشترك**: الشركة أو الفرد المسجل في المنصة +- **الخدمات**: جميع المميزات والأدوات المتاحة حسب الباقة المختارة + +## 2. الخدمات المقدمة +- إدارة العملاء المحتملين والمتابعة التلقائية +- تكامل واتساب بزنس وإيميل ورسائل نصية +- خط أنابيب المبيعات وعروض الأسعار +- تقارير وتحليلات ولوحات تحكم +- وكلاء ذكاء اصطناعي للتواصل التلقائي + +## 3. الباقات والأسعار +- أساسي: 299 ر.س/شهر | احترافي: 699 ر.س/شهر | مؤسسات: 1,499 ر.س/شهر +- جميع الأسعار بالريال السعودي وتشمل ضريبة القيمة المضافة +- الدفع شهري أو سنوي مقدماً + +## 4. التجربة المجانية +- 14 يوم بكل المميزات بدون بطاقة ائتمان +- تنتهي تلقائياً بدون أي التزام + +## 5. حقوق المشترك +- الوصول الكامل لمميزات الباقة المختارة +- الدعم الفني حسب مستوى الباقة +- ملكية كاملة لبياناته وحق تصديرها +- الإلغاء في أي وقت + +## 6. التزامات المشترك +- عدم استخدام المنصة لأغراض غير قانونية +- عدم مشاركة بيانات الدخول +- الالتزام بسياسات التواصل مع العملاء +- عدم إرسال رسائل مزعجة (spam) +- الحفاظ على سرية بيانات عملائه + +## 7. الضمان الذهبي +- 30 يوم استرجاع كامل بشروط محددة في سياسة الاسترجاع +- يسري على جميع الباقات المدفوعة + +## 8. المسؤولية +- Dealix غير مسؤولة عن خسائر ناتجة عن سوء استخدام المنصة +- المسؤولية القصوى لا تتجاوز قيمة الاشتراك الشهري +- لا نضمن نتائج مبيعات محددة + +## 9. الإلغاء +- يحق للمشترك الإلغاء في أي وقت +- الخدمة تستمر حتى نهاية الفترة المدفوعة +- لا توجد رسوم إلغاء + +## 10. القانون الحاكم +تخضع هذه الشروط لأنظمة المملكة العربية السعودية والجهات القضائية المختصة في الرياض. diff --git a/salesflow-saas/frontend/src/app/dashboard/page.tsx b/salesflow-saas/frontend/src/app/dashboard/page.tsx index fe8c7e56..dade5ae5 100644 --- a/salesflow-saas/frontend/src/app/dashboard/page.tsx +++ b/salesflow-saas/frontend/src/app/dashboard/page.tsx @@ -1,7 +1,8 @@ "use client"; -import { useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { useRequireAuth } from "@/contexts/auth-context"; +import { useRouter } from "next/navigation"; import { BarChart3, Users, @@ -28,6 +29,13 @@ import { UserCheck, TrendingUp, Crosshair, + SlidersHorizontal, + Activity, + BookMarked, + Handshake, + Share2, + Rocket, + Megaphone, } from "lucide-react"; import { DashboardView } from "../../components/dealix/dashboard-view"; @@ -51,6 +59,15 @@ import { FullOpsView } from "../../components/dealix/full-ops-view"; import { PipelineKanban } from "../../components/dealix/pipeline-kanban"; import { UnifiedInbox } from "../../components/dealix/unified-inbox"; import { LeadScoreCard } from "../../components/dealix/lead-score-card"; +import { GoLiveReadinessCard } from "../../components/dealix/go-live-readiness-card"; +import { OperatingModelView } from "../../components/dealix/operating-model-view"; +import { PartnershipStudioView } from "../../components/dealix/partnership-studio-view"; +import { GrowthPlaybookView } from "../../components/dealix/growth-playbook-view"; +import { GovernanceMetricsView } from "../../components/dealix/governance-metrics-view"; +import { IdentityGraphView } from "../../components/dealix/identity-graph-view"; +import { VerticalPlaybooksView } from "../../components/dealix/vertical-playbooks-view"; +import { AgentQualityView } from "../../components/dealix/agent-quality-view"; +import { MarketerHubView } from "../../components/dealix/marketer-hub-view"; const dashboardLeadScoreDemo = { score: 82, @@ -63,9 +80,69 @@ const dashboardLeadScoreDemo = { recommendation: "عميل واعد — تابع خلال ٢٤ ساعة", }; +const HUB_ORDER = ["platform", "sales", "partnerships", "growth"] as const; +type HubId = (typeof HUB_ORDER)[number]; + +const HUB_LABELS: Record = { + platform: "المنصة والحوكمة", + sales: "محرك المبيعات", + partnerships: "الشراكات الاستراتيجية", + growth: "النمو والاستراتيجية", +}; + +const NAV_ITEMS = [ + { id: "overview", label: "لوحة القيادة والمراقبة", icon: BarChart3, hub: "platform" as const }, + { id: "go-live", label: "جاهزية الإطلاق", icon: ShieldCheck, hub: "platform" as const }, + { id: "operating-model", label: "نموذج التشغيل OS", icon: SlidersHorizontal, hub: "platform" as const }, + { id: "governance-metrics", label: "الحوكمة والمؤشرات", icon: ShieldCheck, hub: "platform" as const }, + { id: "agent-quality", label: "جودة الوكلاء", icon: Activity, hub: "platform" as const }, + { id: "onboarding", label: "تأهيل المسوق", icon: BookOpen, hub: "platform" as const }, + { id: "agreements", label: "الاتفاقيات واHR", icon: FileSignature, hub: "platform" as const }, + { id: "guarantee", label: "الضمان الذهبي", icon: ShieldCheck, hub: "platform" as const }, + { id: "leads", label: "توليد العملاء — AI", icon: Target, hub: "sales" as const }, + { id: "pipeline", label: "مسار الصفقات", icon: Target, hub: "sales" as const }, + { id: "inbox", label: "صندوق الوارد الموحد", icon: Bell, hub: "sales" as const }, + { id: "scoring", label: "تقييم العملاء AI", icon: Zap, hub: "sales" as const }, + { id: "scripts", label: "سكربتات المبيعات", icon: Phone, hub: "sales" as const }, + { id: "presentations", label: "البرزنتيشنات القطاعية", icon: MonitorPlay, hub: "sales" as const }, + { id: "properties", label: "إدارة المخزون العقاري", icon: Building2, hub: "sales" as const }, + { id: "marketer-hub", label: "مركز المسوق", icon: Megaphone, hub: "sales" as const }, + { id: "affiliates", label: "المسوقين والموظفين", icon: Users, hub: "sales" as const }, + { id: "agents", label: "الوكلاء الأذكياء", icon: BrainCircuit, hub: "sales" as const }, + { id: "revenue", label: "المالية والتحصيل", icon: DollarSign, hub: "sales" as const }, + { id: "sales-os", label: "دفتر العمولة (Sales OS)", icon: Receipt, hub: "sales" as const }, + { id: "full-ops", label: "التشغيل الشامل (Full Ops)", icon: Layers, hub: "sales" as const }, + { id: "analytics", label: "التحليلات ونبض السوق", icon: BarChart3, hub: "sales" as const }, + { id: "knowledge", label: "الذكاء والمعرفة", icon: Brain, hub: "sales" as const }, + { id: "partnership-studio", label: "Partnership Studio", icon: Handshake, hub: "partnerships" as const }, + { id: "identity-graph", label: "طبقة الكيان الموحّد", icon: Share2, hub: "partnerships" as const }, + { id: "vertical-playbooks", label: "Playbooks قطاعية", icon: BookMarked, hub: "partnerships" as const }, + { id: "growth-playbook", label: "نمو واستعداد استحواذ", icon: Rocket, hub: "growth" as const }, + { id: "intelligence", label: "الذكاء المستقل — Manus", icon: BrainCircuit, hub: "growth" as const }, + { id: "business-impact", label: "القيمة للشركات", icon: LineChart, hub: "growth" as const }, + { id: "customer-journey", label: "مسار التشغيل مع العميل", icon: ClipboardList, hub: "growth" as const }, +] as const; +type DashboardTabId = (typeof NAV_ITEMS)[number]["id"]; + export default function DashboardPage() { const auth = useRequireAuth(); - const [activeTab, setActiveTab] = useState("overview"); + const router = useRouter(); + const allowedTabs = useMemo(() => new Set(NAV_ITEMS.map((n) => n.id)), []); + const [activeTab, setActiveTabState] = useState("overview"); + + useEffect(() => { + const requested = new URLSearchParams(window.location.search).get("section") || "overview"; + if (allowedTabs.has(requested as DashboardTabId)) { + setActiveTabState(requested as DashboardTabId); + } + }, [allowedTabs]); + + const setActiveTab = (tab: DashboardTabId) => { + const next = new URLSearchParams(window.location.search); + next.set("section", tab); + setActiveTabState(tab); + router.push(`/dashboard?${next.toString()}`); + }; if (auth.loading) { return ( @@ -78,36 +155,28 @@ export default function DashboardPage() { return null; } - const NAV_ITEMS = [ - { id: "overview", label: "لوحة القيادة والمراقبة", icon: BarChart3 }, - { id: "business-impact", label: "القيمة للشركات", icon: LineChart }, - { id: "customer-journey", label: "مسار التشغيل مع العميل", icon: ClipboardList }, - { id: "intelligence", label: "الذكاء المستقل — Manus", icon: BrainCircuit }, - { id: "leads", label: "توليد العملاء — AI", icon: Target }, - { id: "properties", label: "إدارة المخزون العقاري", icon: Building2 }, - { id: "affiliates", label: "المسوقين والموظفين", icon: Users }, - { id: "agents", label: "الوكلاء الأذكياء", icon: BrainCircuit }, - { id: "revenue", label: "المالية والتحصيل", icon: DollarSign }, - { id: "sales-os", label: "دفتر العمولة (Sales OS)", icon: Receipt }, - { id: "full-ops", label: "التشغيل الشامل (Full Ops)", icon: Layers }, - { id: "analytics", label: "التحليلات ونبض السوق", icon: BarChart3 }, - { id: "knowledge", label: "الذكاء والمعرفة", icon: Brain }, - { id: "presentations", label: "البرزنتيشنات القطاعية", icon: MonitorPlay }, - { id: "scripts", label: "سكربتات المبيعات", icon: Phone }, - { id: "agreements", label: "الاتفاقيات واHR", icon: FileSignature }, - { id: "guarantee", label: "الضمان الذهبي", icon: ShieldCheck }, - { id: "pipeline", label: "مسار الصفقات", icon: Target }, - { id: "inbox", label: "صندوق الوارد الموحد", icon: Bell }, - { id: "scoring", label: "تقييم العملاء AI", icon: Zap }, - { id: "onboarding", label: "تأهيل المسوق", icon: BookOpen }, - ]; - const renderContent = () => { switch (activeTab) { case "overview": return ; case "business-impact": return ; + case "go-live": + return ; + case "operating-model": + return ; + case "governance-metrics": + return ; + case "agent-quality": + return ; + case "partnership-studio": + return ; + case "identity-graph": + return ; + case "vertical-playbooks": + return ; + case "growth-playbook": + return ; case "customer-journey": return ; case "intelligence": @@ -116,6 +185,8 @@ export default function DashboardPage() { return ; case "properties": return ; + case "marketer-hub": + return ; case "affiliates": return ; case "agents": @@ -165,27 +236,35 @@ export default function DashboardPage() { - {item.label} - +
))} +
diff --git a/salesflow-saas/frontend/src/app/globals.css b/salesflow-saas/frontend/src/app/globals.css index e46cde11..e9666162 100644 --- a/salesflow-saas/frontend/src/app/globals.css +++ b/salesflow-saas/frontend/src/app/globals.css @@ -83,6 +83,9 @@ /* Luxury Glassmorphism & High-End Effects */ @layer utilities { + .glass-card { + @apply bg-card/60 backdrop-blur-2xl border border-border/50 rounded-2xl shadow-xl transition-all duration-300 hover:border-primary/30; + } .glass-premium { @apply bg-white/5 dark:bg-black/40 backdrop-blur-2xl border border-white/10 dark:border-white/5 shadow-[0_8px_32px_0_rgba(0,0,0,0.37)]; } @@ -99,6 +102,10 @@ /* Animations & Scrollbar */ @layer utilities { + .dealix-section-header { + @apply border-b border-border/50 pb-4; + } + .animate-float { animation: float 6s ease-in-out infinite; } diff --git a/salesflow-saas/frontend/src/app/landing/page.tsx b/salesflow-saas/frontend/src/app/landing/page.tsx index 8f5d37d4..e3706bc0 100644 --- a/salesflow-saas/frontend/src/app/landing/page.tsx +++ b/salesflow-saas/frontend/src/app/landing/page.tsx @@ -1,5 +1,5 @@ -import HeroLanding from "../../components/dealix/hero-landing"; +import { PremiumLanding } from "../../components/dealix/premium-landing"; export default function LandingPage() { - return ; + return ; } diff --git a/salesflow-saas/frontend/src/app/layout.tsx b/salesflow-saas/frontend/src/app/layout.tsx index 89078ec4..6de7ffaa 100644 --- a/salesflow-saas/frontend/src/app/layout.tsx +++ b/salesflow-saas/frontend/src/app/layout.tsx @@ -13,6 +13,11 @@ export const metadata: Metadata = { title: "Dealix — نظام تشغيل الإيرادات B2B", description: "اكتشاف، تأهيل، قنوات متعددة، وتحليلات — مع حوكمة وذاكرة. سوق سعودي.", + icons: { + icon: "/favicon.svg", + shortcut: "/favicon.svg", + apple: "/favicon.svg", + }, }; export default function RootLayout({ diff --git a/salesflow-saas/frontend/src/app/settings/page.tsx b/salesflow-saas/frontend/src/app/settings/page.tsx index a9eac5a3..50c760bf 100644 --- a/salesflow-saas/frontend/src/app/settings/page.tsx +++ b/salesflow-saas/frontend/src/app/settings/page.tsx @@ -1,8 +1,10 @@ 'use client'; -import { useState, type ReactNode } from 'react'; +import { useState, useEffect, useCallback, type ReactNode } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { useI18n } from '@/i18n'; +import { apiFetch } from '@/lib/api-client'; +import { getAccessToken, getStoredUser } from '@/lib/auth-storage'; /* ------------------------------------------------------------------ */ /* Types */ @@ -355,6 +357,42 @@ function BillingTab({ label }: { label: L }) { +
{}} label={label}> +
+ + + + + + + + + + + + +
+

+ {label( + 'هذه الحقول تمثل ضوابط تسعير على مستوى الشركة ويمكن تعديلها لاحقًا حسب سياسة كل قطاع.', + 'These values are company-level pricing controls and can be tuned per vertical later.' + )} +

+
+ {/* Invoice history */}
@@ -375,34 +413,277 @@ function BillingTab({ label }: { label: L }) { ); } +type CrmStatusPayload = { + salesforce: { + env_refresh_configured: boolean; + tenant_refresh_override: boolean; + domain: string; + }; + hubspot: { + env_token_configured: boolean; + tenant_token_override: boolean; + }; + docs: { integration_master_ar: string; api_map: string }; +}; + +type AiRoutingPayload = { + effective: Record; + available_providers: string[]; + note_ar: string; +}; + function IntegrationsTab({ label }: { label: L }) { - const integrations = [ - { name: 'WhatsApp', icon: '💬', connected: true, descAr: 'متصل — رقم +966 50 XXX XXXX', descEn: 'Connected — +966 50 XXX XXXX' }, - { name: label('البريد SMTP', 'Email SMTP'), icon: '📧', connected: false, descAr: 'غير متصل', descEn: 'Not connected' }, - ]; + const [crm, setCrm] = useState(null); + const [routing, setRouting] = useState(null); + const [leadId, setLeadId] = useState(''); + const [busy, setBusy] = useState(null); + const [lastResult, setLastResult] = useState(null); + const [noToken, setNoToken] = useState(false); + + const user = typeof window !== 'undefined' ? getStoredUser() : null; + const canOps = + user && ['owner', 'manager', 'admin'].includes((user.role || '').toLowerCase()); + + const load = useCallback(async () => { + if (!getAccessToken()) { + setNoToken(true); + setCrm(null); + setRouting(null); + return; + } + setNoToken(false); + const r = await apiFetch('/api/v1/integrations/crm/status'); + if (r.ok) setCrm((await r.json()) as CrmStatusPayload); + const ar = await apiFetch('/api/v1/ai/routing'); + if (ar.ok) setRouting((await ar.json()) as AiRoutingPayload); + }, []); + + useEffect(() => { + void load(); + }, [load]); + + const postAction = async (path: string, body?: object) => { + setBusy(path); + setLastResult(null); + try { + const r = await apiFetch(path, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body ?? {}), + }); + const text = await r.text(); + let msg = text; + try { + msg = JSON.stringify(JSON.parse(text), null, 2); + } catch { + /* raw */ + } + setLastResult(`${r.status} ${r.statusText}\n${msg.slice(0, 4000)}`); + if (r.ok) void load(); + } finally { + setBusy(null); + } + }; + + const pushSf = () => { + const id = leadId.trim(); + if (!/^[0-9a-f-]{36}$/i.test(id)) { + setLastResult(label('معرّف العميل المحتمل غير صالح (UUID)', 'Invalid lead id (UUID)')); + return; + } + void postAction(`/api/v1/integrations/crm/salesforce/push-lead/${id}`); + }; + + const pushHs = () => { + const id = leadId.trim(); + if (!/^[0-9a-f-]{36}$/i.test(id)) { + setLastResult(label('معرّف العميل المحتمل غير صالح (UUID)', 'Invalid lead id (UUID)')); + return; + } + void postAction(`/api/v1/integrations/crm/hubspot/push-lead/${id}`); + }; + + const sfReady = + crm && + (crm.salesforce.env_refresh_configured || crm.salesforce.tenant_refresh_override); + const hsReady = crm && (crm.hubspot.env_token_configured || crm.hubspot.tenant_token_override); return ( <> -
-
- {integrations.map((intg, i) => ( -
-
- {intg.icon} -
-

{intg.name}

-

{label(intg.descAr, intg.descEn)}

-
-
- - {intg.connected ? label('متصل', 'Connected') : label('غير متصل', 'Disconnected')} - -
- ))} +
+
+

+ {label( + 'حالة التهيئة من البيئة وإعدادات المستأجر — دون عرض أسرار.', + 'Configuration hints from env and tenant settings — no secrets shown.', + )} +

+ + {label('دليل التكاملات (Markdown)', 'Integration master (Markdown)')} +
+ + {noToken && ( +

+ {label( + 'سجّل الدخول من صفحة تسجيل الدخول لتحميل حالة التكاملات.', + 'Sign in from the login page to load integration status.', + )} +

+ )} + + {crm && ( +
+
+

Salesforce

+
    +
  • + {label('بيئة:', 'Env:')}{' '} + {crm.salesforce.env_refresh_configured ? label('مهيأ', 'OK') : label('غير مهيأ', 'Missing')} +
  • +
  • + {label('مستأجر:', 'Tenant:')}{' '} + {crm.salesforce.tenant_refresh_override ? label('يوجد override', 'Override') : label('افتراضي', 'Default')} +
  • +
  • domain: {crm.salesforce.domain}
  • +
+

+ {sfReady + ? label('جاهز لمحاولة الاختبار', 'Ready to test') + : label('أضف refresh token وعميل OAuth', 'Add OAuth client + refresh token')} +

+
+
+

HubSpot

+
    +
  • + {label('بيئة:', 'Env:')}{' '} + {crm.hubspot.env_token_configured ? label('مهيأ', 'OK') : label('غير مهيأ', 'Missing')} +
  • +
  • + {label('مستأجر:', 'Tenant:')}{' '} + {crm.hubspot.tenant_token_override ? label('يوجد رمز', 'Token set') : label('افتراضي', 'Default')} +
  • +
+

+ {hsReady + ? label('جاهز لمحاولة الاختبار', 'Ready to test') + : label('أضف مفتاح/رمز HubSpot', 'Add HubSpot token')} +

+
+
+ )} + + {canOps && crm && ( +
+

+ {label('اختبار ومزامنة', 'Test & sync')} +

+
+ + + + +
+ + setLeadId(e.target.value)} + dir="ltr" + placeholder="00000000-0000-0000-0000-000000000000" + className="w-full px-4 py-2.5 rounded-xl bg-white/5 border border-white/10 text-white placeholder-slate-500 text-sm font-mono focus:outline-none focus:ring-2 focus:ring-primary/40" + /> + +
+ + +
+
+ )} + + {!canOps && !noToken && ( +

+ {label( + 'عمليات الاختبار والدفع تتطلب دور مالك أو مدير أو مسؤول.', + 'Test and push operations require owner, manager, or admin role.', + )} +

+ )} + + {lastResult && ( +
+            {lastResult}
+          
+ )}
- {/* API Key */} + {routing && ( +
+

{routing.note_ar}

+

+ {label('المزودون المتاحون حسب مفاتيح الخادم:', 'Available providers (server keys):')}{' '} + {routing.available_providers.join(', ') || '—'} +

+
+            {JSON.stringify(routing.effective, null, 2)}
+          
+
+ )} +
-
diff --git a/salesflow-saas/frontend/src/components/dealix/agent-quality-view.tsx b/salesflow-saas/frontend/src/components/dealix/agent-quality-view.tsx new file mode 100644 index 00000000..0d652ed7 --- /dev/null +++ b/salesflow-saas/frontend/src/components/dealix/agent-quality-view.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { apiFetch } from "@/lib/api-client"; +import { Activity, LineChart } from "lucide-react"; + +type Snap = { + strategic_deals_total: number; + negotiation_rounds_total: number; + avg_negotiation_rounds_per_deal: number; + deals_high_ai_confidence: number; + labels_ar: Record; + loop_hints_ar: string[]; +}; + +export function AgentQualityView() { + const [data, setData] = useState(null); + const [err, setErr] = useState(null); + + useEffect(() => { + let on = true; + void (async () => { + const r = await apiFetch("/api/v1/strategic-deals/agent-quality/snapshot"); + if (!on) return; + if (!r.ok) { + setErr(`تعذر التحميل (${r.status})`); + return; + } + setData(await r.json()); + })(); + return () => { + on = false; + }; + }, []); + + if (err) { + return
{err}
; + } + if (!data) { + return
جارٍ التحميل…
; + } + + return ( +
+
+

حلقة جودة الوكلاء

+

مؤشرات مساعدة من مسار الصفقات الاستراتيجية — توسع لاحقاً بسجلات الرسائل والتحويل.

+
+ +
+
+
+ + {data.labels_ar.negotiation_depth} +
+

{data.negotiation_rounds_total}

+

متوسط {data.avg_negotiation_rounds_per_deal} جولة / صفقة

+
+
+
+ + {data.labels_ar.high_confidence_deals} +
+

{data.deals_high_ai_confidence}

+

من أصل {data.strategic_deals_total} صفقة

+
+
+ +
+

تحسين مستمر

+
    + {data.loop_hints_ar.map((h, i) => ( +
  • {h}
  • + ))} +
+
+
+ ); +} diff --git a/salesflow-saas/frontend/src/components/dealix/dealix-3d-logo.tsx b/salesflow-saas/frontend/src/components/dealix/dealix-3d-logo.tsx index 0e3fbda4..d90f5d9c 100644 --- a/salesflow-saas/frontend/src/components/dealix/dealix-3d-logo.tsx +++ b/salesflow-saas/frontend/src/components/dealix/dealix-3d-logo.tsx @@ -17,6 +17,31 @@ function useIsMobile() { return mobile; } +function usePrefersReducedMotion() { + const [reduced, setReduced] = useState(false); + useEffect(() => { + const media = window.matchMedia("(prefers-reduced-motion: reduce)"); + const update = () => setReduced(media.matches); + update(); + media.addEventListener("change", update); + return () => media.removeEventListener("change", update); + }, []); + return reduced; +} + +function StaticHandshakeMark() { + return ( +
+
+ + + + + +
+ ); +} + function HandShape({ position, rotation, color }: { position: [number, number, number]; rotation: [number, number, number]; @@ -164,27 +189,33 @@ interface DealixLogo3DProps { function DealixLogo3D({ size = 300, className }: DealixLogo3DProps) { const isMobile = useIsMobile(); + const prefersReducedMotion = usePrefersReducedMotion(); + const lowSpec = isMobile || prefersReducedMotion; return (
+ {lowSpec ? ( + + ) : ( }> - + + )} {/* Ambient glow behind the canvas */}
diff --git a/salesflow-saas/frontend/src/components/dealix/go-live-readiness-card.tsx b/salesflow-saas/frontend/src/components/dealix/go-live-readiness-card.tsx new file mode 100644 index 00000000..70d1109a --- /dev/null +++ b/salesflow-saas/frontend/src/components/dealix/go-live-readiness-card.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { apiFetch } from "@/lib/api-client"; + +type GatePayload = { + launch_allowed?: boolean; + readiness_percent_total?: number; + blocked_reasons?: string[]; +}; + +export function GoLiveReadinessCard() { + const [state, setState] = useState<{ + loading: boolean; + error: string | null; + data: GatePayload | null; + }>({ loading: true, error: null, data: null }); + + useEffect(() => { + let active = true; + const load = async () => { + try { + const r = await apiFetch("/api/v1/autonomous-foundation/integrations/go-live-gate", { + cache: "no-store", + }); + const json = await r.json(); + if (!active) return; + setState({ loading: false, error: null, data: json }); + } catch (err) { + if (!active) return; + setState({ + loading: false, + error: err instanceof Error ? err.message : "failed_to_load", + data: null, + }); + } + }; + load(); + return () => { + active = false; + }; + }, []); + + if (state.loading) { + return
جارٍ تحميل حالة الإطلاق…
; + } + if (state.error) { + return
تعذر جلب go-live-gate: {state.error}
; + } + const ok = Boolean(state.data?.launch_allowed); + const reasons = (state.data?.blocked_reasons || []).slice(0, 5); + return ( +
+
+

جاهزية الإطلاق التجاري

+ + {ok ? "جاهز للإطلاق" : "يحتاج استكمال"} + +
+

+ readiness_percent_total: {state.data?.readiness_percent_total ?? "-"}% +

+ {!ok && reasons.length > 0 && ( +
    + {reasons.map((reason) => ( +
  • {reason}
  • + ))} +
+ )} +
+ ); +} + diff --git a/salesflow-saas/frontend/src/components/dealix/governance-metrics-view.tsx b/salesflow-saas/frontend/src/components/dealix/governance-metrics-view.tsx new file mode 100644 index 00000000..e4d01432 --- /dev/null +++ b/salesflow-saas/frontend/src/components/dealix/governance-metrics-view.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { apiFetch } from "@/lib/api-client"; +import { GoLiveReadinessCard } from "./go-live-readiness-card"; +import { Target, Shield, TrendingUp } from "lucide-react"; + +type GovSnapshot = { + operating_mode: { value: number; name: string; label_ar: string }; + north_star_hints_ar: Record; + governance_kpis: { + auto_send_enabled: boolean; + auto_negotiate_enabled: boolean; + max_auto_commitment_sar: number; + strategic_deals_total: number; + deals_with_negotiation_rounds: number; + }; +}; + +export function GovernanceMetricsView() { + const [snap, setSnap] = useState(null); + const [err, setErr] = useState(null); + + useEffect(() => { + let on = true; + void (async () => { + const r = await apiFetch("/api/v1/strategic-deals/governance/snapshot"); + if (!on) return; + if (!r.ok) { + setErr(`تعذر جلب المؤشرات (${r.status})`); + return; + } + setSnap(await r.json()); + })(); + return () => { + on = false; + }; + }, []); + + return ( +
+
+

الحوكمة والمؤشرات

+

Go-live، وضع التشغيل، ومؤشرات موحّدة لثلاثة محاور المنتج.

+
+ + + + {err &&
{err}
} + + {snap && ( + <> +
+
+ +

وضع التشغيل والحدود

+
+

+ {snap.operating_mode.label_ar} + ({snap.operating_mode.name}) +

+
+
+ إرسال تلقائي: {snap.governance_kpis.auto_send_enabled ? "مفعّل" : "غير مفعّل"} +
+
+ تفاوض تلقائي: {snap.governance_kpis.auto_negotiate_enabled ? "مفعّل" : "غير مفعّل"} +
+
+ حد الالتزام التلقائي: {snap.governance_kpis.max_auto_commitment_sar?.toLocaleString("ar-SA")} ر.س +
+
+ صفقات استراتيجية: {snap.governance_kpis.strategic_deals_total} +
+
+ صفقات بجولات تفاوض مسجّلة: {snap.governance_kpis.deals_with_negotiation_rounds} +
+
+
+ +
+
+ +

تلميحات North Star

+
+
    + {Object.entries(snap.north_star_hints_ar).map(([k, v]) => ( +
  • {v}
  • + ))} +
+
+ + )} + +
+ +

+ اربط هذه اللوحة ببيانات القمع والقنوات الفعلية عند تفعيل التكاملات؛ المؤشرات الحالية تعكس طبقة الصفقات الاستراتيجية والحوكمة. +

+
+
+ ); +} diff --git a/salesflow-saas/frontend/src/components/dealix/growth-playbook-view.tsx b/salesflow-saas/frontend/src/components/dealix/growth-playbook-view.tsx new file mode 100644 index 00000000..8a2ad3f1 --- /dev/null +++ b/salesflow-saas/frontend/src/components/dealix/growth-playbook-view.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { apiFetch } from "@/lib/api-client"; +import Link from "next/link"; +import { Rocket, CheckCircle2, ExternalLink } from "lucide-react"; + +type Phase = { + id: string; + title_ar: string; + items_ar: string[]; +}; + +export function GrowthPlaybookView() { + const [phases, setPhases] = useState([]); + const [done, setDone] = useState>({}); + const [disclaimer, setDisclaimer] = useState(""); + + useEffect(() => { + void (async () => { + const r = await apiFetch("/api/v1/strategic-deals/growth/checklist"); + if (!r.ok) return; + const j = await r.json(); + setPhases(j.phases || []); + setDisclaimer(j.disclaimer_ar || ""); + })(); + }, []); + + const toggle = (key: string) => { + setDone((d) => ({ ...d, [key]: !d[key] })); + }; + + return ( +
+
+ +
+

نمو واستعداد استحواذ / توسع

+

+ مساعد قرار وتنظيم مهام — الموافقات والعناية الواجبة الكاملة تبقى بشرية. +

+
+
+ +
+ {disclaimer} +
+ + + + فتح لوحة الذكاء الاستراتيجي (Manus) للسيناريوهات والتقارير + + +
+ {phases.map((ph) => ( +
+

{ph.title_ar}

+
    + {ph.items_ar.map((item, idx) => { + const key = `${ph.id}-${idx}`; + const checked = !!done[key]; + return ( +
  • + + {item} +
  • + ); + })} +
+
+ ))} +
+
+ ); +} diff --git a/salesflow-saas/frontend/src/components/dealix/identity-graph-view.tsx b/salesflow-saas/frontend/src/components/dealix/identity-graph-view.tsx new file mode 100644 index 00000000..e166427a --- /dev/null +++ b/salesflow-saas/frontend/src/components/dealix/identity-graph-view.tsx @@ -0,0 +1,119 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { apiFetch } from "@/lib/api-client"; +import { Share2 } from "lucide-react"; + +type Profile = { + id: string; + company_name: string; + industry?: string | null; +}; + +type GraphPayload = { + profile_id: string; + company_name: string; + suggested_playbook_id: string | null; + counts: { + strategic_deals_as_initiator: number; + strategic_deals_as_target: number; + matches_as_party_a: number; + matches_as_party_b: number; + deals_with_lead_link: number; + deals_with_sales_deal_link: number; + }; +}; + +export function IdentityGraphView() { + const [profiles, setProfiles] = useState([]); + const [pid, setPid] = useState(""); + const [graph, setGraph] = useState(null); + const [err, setErr] = useState(null); + + useEffect(() => { + void (async () => { + const r = await apiFetch("/api/v1/strategic-deals/profiles"); + if (!r.ok) return; + const list = (await r.json()) as Profile[]; + setProfiles(list); + setPid((p) => p || list[0]?.id || ""); + })(); + }, []); + + useEffect(() => { + if (!pid) return; + void (async () => { + setErr(null); + const r = await apiFetch(`/api/v1/strategic-deals/identity/graph?profile_id=${encodeURIComponent(pid)}`); + if (!r.ok) { + setErr("تعذر تحميل الرسم البياني للكيان"); + setGraph(null); + return; + } + setGraph(await r.json()); + })(); + }, [pid]); + + return ( +
+
+ +
+

طبقة الكيان الموحّد

+

+ ملف شركة واحد — صفقات استراتيجية، مطابقات، وروابط CRM (خفيفة). +

+
+
+ +
+ + +
+ + {err &&
{err}
} + + {graph && ( +
+

{graph.company_name}

+ {graph.suggested_playbook_id && ( +

+ Playbook مقترح: {graph.suggested_playbook_id} +

+ )} +
+
+ صفقات كمبادر: {graph.counts.strategic_deals_as_initiator} +
+
+ صفقات كهدف: {graph.counts.strategic_deals_as_target} +
+
+ مطابقات (طرف أ): {graph.counts.matches_as_party_a} +
+
+ مطابقات (طرف ب): {graph.counts.matches_as_party_b} +
+
+ صفقات مربوطة بـ lead: {graph.counts.deals_with_lead_link} +
+
+ صفقات مربوطة بصفقة مبيعات: {graph.counts.deals_with_sales_deal_link} +
+
+
+ )} +
+ ); +} diff --git a/salesflow-saas/frontend/src/components/dealix/intelligence-dashboard.tsx b/salesflow-saas/frontend/src/components/dealix/intelligence-dashboard.tsx index f00ea0f0..04e3d8dd 100644 --- a/salesflow-saas/frontend/src/components/dealix/intelligence-dashboard.tsx +++ b/salesflow-saas/frontend/src/components/dealix/intelligence-dashboard.tsx @@ -2,6 +2,7 @@ import { useState, useEffect } from "react"; import { getApiBaseUrl } from "@/lib/api-base"; +import { apiFetch } from "@/lib/api-client"; interface AgentStatus { role: string; @@ -39,8 +40,7 @@ export function IntelligenceDashboard() { const fetchAgentStatus = async () => { try { - const base = getApiBaseUrl().replace(/\/$/, ""); - const res = await fetch(`${base}/api/v1/agents/status`); + const res = await apiFetch("/api/v1/agents/status", { cache: "no-store" }); if (res.ok) { const data = await res.json(); setAgents(data.agents || []); @@ -50,8 +50,7 @@ export function IntelligenceDashboard() { const fetchHealth = async () => { try { - const base = getApiBaseUrl().replace(/\/$/, ""); - const res = await fetch(`${base}/api/v1/intelligence/health`); + const res = await apiFetch("/api/v1/intelligence/health", { cache: "no-store" }); if (res.ok) setHealth(await res.json()); } catch {} }; @@ -60,8 +59,7 @@ export function IntelligenceDashboard() { if (!leadForm.contact_name || !leadForm.company_name) return; setLoading(true); try { - const base = getApiBaseUrl().replace(/\/$/, ""); - const res = await fetch(`${base}/api/v1/intelligence/run-pipeline`, { + const res = await apiFetch("/api/v1/intelligence/run-pipeline", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: `lead_${Date.now()}`, ...leadForm }), diff --git a/salesflow-saas/frontend/src/components/dealix/lead-generator-view.tsx b/salesflow-saas/frontend/src/components/dealix/lead-generator-view.tsx index 2d00f3f8..d0f77ca7 100644 --- a/salesflow-saas/frontend/src/components/dealix/lead-generator-view.tsx +++ b/salesflow-saas/frontend/src/components/dealix/lead-generator-view.tsx @@ -2,6 +2,7 @@ import { useState } from "react"; import { getApiBaseUrl } from "@/lib/api-base"; +import { apiFetch } from "@/lib/api-client"; export function LeadGeneratorView() { const [sector, setSector] = useState("تقنية المعلومات"); @@ -27,8 +28,7 @@ export function LeadGeneratorView() { setLoading(true); setLeads([]); try { - const base = getApiBaseUrl().replace(/\/$/, ""); - const res = await fetch(`${base}/api/v1/dealix/generate-leads?sector=${encodeURIComponent(sector)}&city=${encodeURIComponent(city)}&count=${count}`, { + const res = await apiFetch(`/api/v1/dealix/generate-leads?sector=${encodeURIComponent(sector)}&city=${encodeURIComponent(city)}&count=${count}`, { method: "POST" }); if (res.ok) { @@ -56,8 +56,7 @@ export function LeadGeneratorView() { const runPipeline = async (lead: any) => { setPipelineRunning(lead.company_name); try { - const base = getApiBaseUrl().replace(/\/$/, ""); - const res = await fetch(`${base}/api/v1/dealix/full-power`, { + const res = await apiFetch("/api/v1/dealix/full-power", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ diff --git a/salesflow-saas/frontend/src/components/dealix/marketer-hub-view.tsx b/salesflow-saas/frontend/src/components/dealix/marketer-hub-view.tsx new file mode 100644 index 00000000..d4eeb017 --- /dev/null +++ b/salesflow-saas/frontend/src/components/dealix/marketer-hub-view.tsx @@ -0,0 +1,171 @@ +"use client"; + +import Link from "next/link"; +import { useCallback, useEffect, useState } from "react"; +import { apiFetch } from "@/lib/api-client"; +import { + BookOpen, + FileText, + Gavel, + Megaphone, + Route, + Sparkles, + Target, + ExternalLink, +} from "lucide-react"; + +type ProgramPayload = { + title_ar?: string; + journey_ar?: { step: number; title: string; detail_ar: string }[]; +}; + +const LEGAL_LINKS = [ + { href: "/strategy/legal/affiliate-rules-ar.md", label: "قواعد المسوقين بالعمولة", summary: "مسموح ومحظور، عقوبات، حقوق المسوق." }, + { href: "/strategy/legal/commission-policy-ar.md", label: "سياسة العمولات", summary: "هيكل العمولات والمستحقات." }, + { href: "/strategy/legal/consent-policy-ar.md", label: "سياسة الموافقة", summary: "التواصل والموافقات المطلوبة." }, +] as const; + +const PLAYBOOK_STEPS = [ + { + phase: "تأهيل", + items: [ + { label: "تعرّف على البرنامج والعمولة", section: "affiliates" as const }, + { label: "اقرأ القواعد والموافقة", section: null }, + ], + }, + { + phase: "محتوى وجاهزية", + items: [ + { label: "البرزنتيشنات القطاعية", section: "presentations" as const }, + { label: "سكربتات المكالمات والواتساب", section: "scripts" as const }, + ], + }, + { + phase: "توليد ومتابعة", + items: [ + { label: "توليد عملاء محتملين", section: "leads" as const }, + { label: "صندوق الوارد الموحّد", section: "inbox" as const }, + ], + }, + { + phase: "شراكات متقدمة", + items: [{ label: "Partnership Studio", section: "partnership-studio" as const }], + }, +]; + +function dash(section: string) { + return `/dashboard?section=${section}`; +} + +export function MarketerHubView() { + const [program, setProgram] = useState(null); + + const load = useCallback(async () => { + const r = await apiFetch("/api/v1/affiliates/program"); + if (r.ok) setProgram(await r.json()); + }, []); + + useEffect(() => { + void load(); + }, [load]); + + return ( +
+
+
+ +
+

مركز المسوق

+

+ مسار واحد: قواعد البرنامج، الأدوات في Dealix، والوثائق الرسمية — بدون تشتيت بين الشاشات. +

+
+
+
+ +
+
+ +

برنامج العمولة (مختصر مباشر من API)

+
+ {program?.title_ar &&

{program.title_ar}

} +
    + {(program?.journey_ar || []).slice(0, 4).map((s) => ( +
  • + {s.step} + + {s.title} — {s.detail_ar} + +
  • + ))} + {!program?.journey_ar?.length &&
  • جارٍ التحميل أو لا توجد بيانات برنامج بعد.
  • } +
+ + + فتح لوحة المسوقين والترتيب الكامل + +
+ +
+
+ +

Playbook المسار

+
+
+ {PLAYBOOK_STEPS.map((block) => ( +
+

{block.phase}

+
    + {block.items.map((it) => ( +
  • + {it.section ? ( + + {it.label} + + + ) : ( + {it.label} + )} +
  • + ))} +
+
+ ))} +
+
+ +
+
+ +

الوثائق القانونية (ملخص + الملف الكامل)

+
+
+ {LEGAL_LINKS.map((doc) => ( + +
+ +
+

{doc.label}

+

{doc.summary}

+
+
+
+ ))} +
+

+ + للربط الشامل بالبيئة والتكاملات راجع وثيقة INTEGRATION_MASTER في المستودع أو مسار الاستراتيجية العامة. +

+
+
+ ); +} diff --git a/salesflow-saas/frontend/src/components/dealix/operating-model-view.tsx b/salesflow-saas/frontend/src/components/dealix/operating-model-view.tsx new file mode 100644 index 00000000..173035cd --- /dev/null +++ b/salesflow-saas/frontend/src/components/dealix/operating-model-view.tsx @@ -0,0 +1,161 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { apiFetch } from "@/lib/api-client"; +import { Shield, Users, Clock, Gavel } from "lucide-react"; + +type ModeRow = { + mode: number; + name: string; + label_ar: string; + description_ar: string; + auto_send: boolean; + auto_negotiate: boolean; + max_auto_commitment_sar: number; + allowed_channels: string[]; +}; + +type OperatingPayload = { + current: ModeRow & { name: string }; + modes: ModeRow[]; + roles_ar: { id: string; label: string; scope: string }[]; + sla_hints_ar: Record; +}; + +export function OperatingModelView() { + const [data, setData] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + const load = useCallback(async () => { + setLoading(true); + setError(null); + const r = await apiFetch("/api/v1/strategic-deals/operating-model"); + if (!r.ok) { + setError(`تعذر التحميل (${r.status})`); + setLoading(false); + return; + } + setData(await r.json()); + setLoading(false); + }, []); + + useEffect(() => { + void load(); + }, [load]); + + const setMode = async (mode: number) => { + setSaving(true); + setError(null); + const r = await apiFetch("/api/v1/strategic-deals/operating-model", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ mode }), + }); + setSaving(false); + if (!r.ok) { + const j = await r.json().catch(() => ({})); + setError((j as { detail?: string }).detail || "فشل الحفظ"); + return; + } + await load(); + }; + + if (loading) { + return
جارٍ تحميل نموذج التشغيل…
; + } + if (error && !data) { + return
{error}
; + } + if (!data) return null; + + return ( +
+
+

نموذج تشغيل Dealix OS

+

+ أدوار، حلقات قرار، وحدود أتمتة الذكاء الاصطناعي — مربوطة بوضع التشغيل في الخادم. +

+
+ + {error &&
{error}
} + +
+
+ +

الوضع الحالي

+
+

{data.current.label_ar}

+

{data.current.description_ar}

+
+ + إرسال تلقائي: {data.current.auto_send ? "نعم" : "لا"} + + + تفاوض تلقائي: {data.current.auto_negotiate ? "نعم" : "لا"} + + + حد الالتزام التلقائي: {data.current.max_auto_commitment_sar?.toLocaleString("ar-SA")} ر.س + +
+
+ +
+

تغيير وضع التشغيل (0–4)

+
+ {data.modes.map((m) => ( + + ))} +
+
+ +
+
+ +

أدوار مقترحة

+
+
    + {data.roles_ar.map((r) => ( +
  • + {r.label} + — {r.scope} +
  • + ))} +
+
+ +
+
+ +

إرشادات SLA داخلية

+
+
    + {Object.values(data.sla_hints_ar).map((t, i) => ( +
  • {t}
  • + ))} +
+
+ +
+ +

+ الالتزامات القانونية والمالية النهائية تبقى بشرية؛ الوضع الاستراتيجي يسمح بأتمتة أوسع مع تصعيد إلزامي عند الحدود. +

+
+
+ ); +} diff --git a/salesflow-saas/frontend/src/components/dealix/partnership-studio-view.tsx b/salesflow-saas/frontend/src/components/dealix/partnership-studio-view.tsx new file mode 100644 index 00000000..71f5f176 --- /dev/null +++ b/salesflow-saas/frontend/src/components/dealix/partnership-studio-view.tsx @@ -0,0 +1,540 @@ +"use client"; + +import { useCallback, useEffect, useState } from "react"; +import { apiFetch } from "@/lib/api-client"; +import { Handshake, Plus, RefreshCw } from "lucide-react"; + +type Profile = { + id: string; + company_name: string; + industry?: string | null; + capabilities?: string[]; + needs?: string[]; +}; + +type StrategicDeal = { + id: string; + deal_title: string; + deal_type: string; + status: string; + estimated_value_sar?: number | null; + lead_id?: string | null; + sales_deal_id?: string | null; +}; + +type MatchRow = { + id: string; + match_score: number; + company_b_name?: string | null; + status: string; + deal_type_suggested?: string | null; +}; + +const DEAL_TYPES = [ + "partnership", + "distribution", + "franchise", + "jv", + "referral", + "acquisition", + "barter", +]; + +export function PartnershipStudioView() { + const [tab, setTab] = useState<"profiles" | "deals" | "matches" | "policy" | "archetypes">("profiles"); + const [profiles, setProfiles] = useState([]); + const [deals, setDeals] = useState([]); + const [matches, setMatches] = useState([]); + const [selProfile, setSelProfile] = useState(""); + const [loading, setLoading] = useState(false); + const [msg, setMsg] = useState(null); + + const [newProfile, setNewProfile] = useState({ + company_name: "", + industry: "", + capabilities: "", + needs: "", + }); + + const [newDeal, setNewDeal] = useState({ + deal_title: "", + deal_type: "partnership", + target_company_name: "", + estimated_value_sar: "", + channel: "whatsapp", + lead_id: "", + sales_deal_id: "", + }); + + const [policy, setPolicy] = useState({ + channel: "whatsapp", + action: "send_custom_message", + deal_value_sar: "0", + industry: "", + }); + const [policyResult, setPolicyResult] = useState | null>(null); + + const [archetypes, setArchetypes] = useState[]>([]); + + const loadProfiles = useCallback(async () => { + const r = await apiFetch("/api/v1/strategic-deals/profiles"); + if (r.ok) { + const list = (await r.json()) as Profile[]; + setProfiles(list); + setSelProfile((prev) => prev || list[0]?.id || ""); + } + }, []); + + const loadDeals = useCallback(async () => { + if (!selProfile) return; + const r = await apiFetch( + `/api/v1/strategic-deals?profile_id=${encodeURIComponent(selProfile)}&per_page=50`, + ); + if (r.ok) setDeals(await r.json()); + }, [selProfile]); + + const loadMatches = useCallback(async () => { + if (!selProfile) return; + const r = await apiFetch( + `/api/v1/strategic-deals/matches?profile_id=${encodeURIComponent(selProfile)}&per_page=50`, + ); + if (r.ok) setMatches(await r.json()); + }, [selProfile]); + + useEffect(() => { + void loadProfiles(); + }, [loadProfiles]); + + useEffect(() => { + void loadDeals(); + void loadMatches(); + }, [loadDeals, loadMatches]); + + useEffect(() => { + void (async () => { + const r = await apiFetch("/api/v1/strategic-deals/partner-archetypes"); + if (r.ok) { + const j = await r.json(); + setArchetypes(j.archetypes || []); + } + })(); + }, []); + + const refresh = async () => { + setLoading(true); + setMsg(null); + await loadProfiles(); + await loadDeals(); + await loadMatches(); + setLoading(false); + }; + + const createProfile = async () => { + setMsg(null); + const caps = newProfile.capabilities.split(",").map((s) => s.trim()).filter(Boolean); + const needs = newProfile.needs.split(",").map((s) => s.trim()).filter(Boolean); + const r = await apiFetch("/api/v1/strategic-deals/profiles", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + company_name: newProfile.company_name, + industry: newProfile.industry || undefined, + capabilities: caps, + needs, + }), + }); + if (!r.ok) { + const e = await r.json().catch(() => ({})); + setMsg((e as { detail?: string }).detail || "فشل إنشاء الملف"); + return; + } + setNewProfile({ company_name: "", industry: "", capabilities: "", needs: "" }); + await loadProfiles(); + setMsg("تم إنشاء ملف الشركة."); + }; + + const createDeal = async () => { + if (!selProfile) { + setMsg("اختر ملف شركة مبادر."); + return; + } + setMsg(null); + const body: Record = { + initiator_profile_id: selProfile, + deal_title: newDeal.deal_title, + deal_type: newDeal.deal_type, + channel: newDeal.channel, + target_company_name: newDeal.target_company_name || undefined, + estimated_value_sar: newDeal.estimated_value_sar + ? Number(newDeal.estimated_value_sar) + : undefined, + }; + if (newDeal.lead_id.trim()) body.lead_id = newDeal.lead_id.trim(); + if (newDeal.sales_deal_id.trim()) body.sales_deal_id = newDeal.sales_deal_id.trim(); + + const r = await apiFetch("/api/v1/strategic-deals", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (!r.ok) { + const e = await r.json().catch(() => ({})); + setMsg((e as { detail?: string }).detail || "فشل إنشاء الصفقة"); + return; + } + setNewDeal((d) => ({ + ...d, + deal_title: "", + target_company_name: "", + estimated_value_sar: "", + lead_id: "", + sales_deal_id: "", + })); + await loadDeals(); + setMsg("تم إنشاء الصفقة الاستراتيجية."); + }; + + const evaluatePolicy = async () => { + setMsg(null); + const r = await apiFetch("/api/v1/strategic-deals/policy/evaluate", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + channel: policy.channel, + action: policy.action, + deal_value_sar: Number(policy.deal_value_sar) || 0, + industry: policy.industry || undefined, + }), + }); + if (!r.ok) { + setMsg("فشل تقييم السياسة"); + return; + } + setPolicyResult(await r.json()); + }; + + const patchDealLinks = async (dealId: string, leadId: string, salesId: string) => { + const body: Record = {}; + if (leadId.trim()) body.lead_id = leadId.trim(); + if (salesId.trim()) body.sales_deal_id = salesId.trim(); + if (!Object.keys(body).length) return; + const r = await apiFetch(`/api/v1/strategic-deals/${dealId}/links`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }); + if (r.ok) await loadDeals(); + }; + + return ( +
+
+
+ +
+

Partnership Studio

+

ملفات، صفقات، مطابقات، سياسات، وأنماط شراكة — عبر REST الموحّد.

+
+
+ +
+ + {msg &&
{msg}
} + +
+ {( + [ + ["profiles", "الملفات"], + ["deals", "الصفقات"], + ["matches", "المطابقات"], + ["policy", "محرك السياسات"], + ["archetypes", "أنماط الشراكة"], + ] as const + ).map(([id, label]) => ( + + ))} +
+ +
+ ملف المبادر النشط: + +
+ + {tab === "profiles" && ( +
+
+

+ ملف شركة جديد +

+ setNewProfile({ ...newProfile, company_name: e.target.value })} + /> + setNewProfile({ ...newProfile, industry: e.target.value })} + /> + setNewProfile({ ...newProfile, capabilities: e.target.value })} + /> + setNewProfile({ ...newProfile, needs: e.target.value })} + /> + +
+
+

الملفات الحالية

+ {profiles.map((p) => ( +
+
{p.company_name}
+
{p.industry || "—"}
+
+ ))} + {!profiles.length &&

لا توجد ملفات بعد.

} +
+
+ )} + + {tab === "deals" && ( +
+
+

صفقة استراتيجية جديدة

+ setNewDeal({ ...newDeal, deal_title: e.target.value })} + /> +
+ + +
+ setNewDeal({ ...newDeal, target_company_name: e.target.value })} + /> + setNewDeal({ ...newDeal, estimated_value_sar: e.target.value })} + /> +
+ setNewDeal({ ...newDeal, lead_id: e.target.value })} + /> + setNewDeal({ ...newDeal, sales_deal_id: e.target.value })} + /> +
+ +
+ +
+

الصفقات

+ + + + + + + + + + + {deals.map((d) => ( + + + + + + + ))} + +
العنوانالنوعالحالةربط CRM
{d.deal_title}{d.deal_type}{d.status} +
+ + lead: {d.lead_id || "—"} | sales: {d.sales_deal_id || "—"} + +
+ + + +
+
+
+ {!deals.length &&

لا صفقات لهذا الملف.

} +
+
+ )} + + {tab === "matches" && ( +
+

مطابقات مقترحة

+ {matches.map((m) => ( +
+
+
{m.company_b_name || "طرف ب"}
+
+ score {m.match_score.toFixed(2)} · {m.deal_type_suggested || "—"} · {m.status} +
+
+
+ ))} + {!matches.length &&

لا مطابقات بعد — جرّب فحص الاكتشاف من الـ API.

} +
+ )} + + {tab === "policy" && ( +
+

تقييم سياسة الإجراء

+ + setPolicy({ ...policy, action: e.target.value })} + /> + setPolicy({ ...policy, deal_value_sar: e.target.value })} + /> + setPolicy({ ...policy, industry: e.target.value })} + /> + + {policyResult && ( +
+              {JSON.stringify(policyResult, null, 2)}
+            
+ )} +
+ )} + + {tab === "archetypes" && ( +
+

من نوع الصفقة إلى النمط التشغيلي

+ {archetypes.map((a, i) => ( +
+
{(a as { deal_type?: string }).deal_type}
+
{(a as { label_ar?: string }).label_ar}
+
{(a as { description_ar?: string }).description_ar}
+
+ ))} +
+ )} +
+ ); +} diff --git a/salesflow-saas/frontend/src/components/dealix/premium-landing.tsx b/salesflow-saas/frontend/src/components/dealix/premium-landing.tsx index 600e5b69..a3292615 100644 --- a/salesflow-saas/frontend/src/components/dealix/premium-landing.tsx +++ b/salesflow-saas/frontend/src/components/dealix/premium-landing.tsx @@ -2,6 +2,8 @@ import { useRef } from "react"; import { motion, useInView } from "framer-motion"; +import Link from "next/link"; +import { DealixLogo3D } from "./dealix-3d-logo"; import { Zap, MessageSquare, @@ -85,6 +87,35 @@ const steps = [ { num: "٣", title: "ابدأ البيع", desc: "دع الذكاء الاصطناعي يساعدك في إتمام المزيد من الصفقات" }, ]; +/** فروقات يمكن التحقق منها في المستودع والوثائق — بدون أرقام سوق غير مثبتة */ +const provenDifferentiators = [ + { + icon: FileText, + title: "مسارات API موثقة", + desc: "خريطة API في المستودع ومطابقة OpenAPI عبر سكربت التحقق من الفرونت.", + }, + { + icon: ShieldCheck, + title: "حوكمة وإطلاق منضبط", + desc: "بوابة go-live وفحوص تكامل موثقة في قائمة الإطلاق وليس مجرد وعود في الواجهة.", + }, + { + icon: MessageSquare, + title: "قنوات محلية أولاً", + desc: "تجربة عربية RTL وواتساب ضمن مسار المنتج، وليس قالباً أمريكياً مترجماً فقط.", + }, + { + icon: Building2, + title: "شراكات B2B في نفس المنصة", + desc: "مسارات الصفقات الاستراتيجية وPartnership Studio بجانب محرك المبيعات.", + }, + { + icon: BrainCircuit, + title: "توجيه نماذج حسب المهمة", + desc: "سياسة LLM لكل نوع عمل دون إرسال مفاتيح إلى المتصفح.", + }, +] as const; + const pricingPlans = [ { name: "Starter", @@ -116,25 +147,6 @@ const pricingPlans = [ }, ]; -/* ───────────── 3D Logo placeholder ───────────── */ -function DealixLogo3D() { - return ( - -
-
-
- - DEALIX -
-
- - ); -} - /* ───────────── main component ───────────── */ export function PremiumLanding() { return ( @@ -156,11 +168,12 @@ export function PremiumLanding() { {/* ═══════════ NAV ═══════════ */}
@@ -215,18 +229,18 @@ export function PremiumLanding() { {[ - { label: "شركة سعودية", value: "+٥٠٠" }, - { label: "رضا العملاء", value: "٩٥٪" }, - { label: "صفقة مغلقة", value: "+١٠٠٠" }, + { label: "اللغة والسياق", value: "عربي · سعودي أولاً" }, + { label: "تكامل CRM", value: "Salesforce · HubSpot عبر API" }, + { label: "الشفافية", value: "وثائق + مصفوفة تنافسية" }, ].map((s, i) => (
-

{s.value}

+

{s.value}

{s.label}

))} @@ -286,6 +300,40 @@ export function PremiumLanding() {
+ {/* ═══════════ PROVEN DIFFERENTIATORS ═══════════ */} +
+ + فروقات يمكن إثباتها — لا أرقام وهمية + + + ما يلي مربوط بمسارات الكود والوثائق في المستودع؛ راجع المصفوفة التفصيلية للمقارنة مع فئات الأدوات العالمية. + + + + فتح مصفوفة تنافسية (Markdown) + + +
+ {provenDifferentiators.map((d, i) => ( + +
+ +
+

{d.title}

+

{d.desc}

+
+ ))} +
+
+ {/* ═══════════ HOW IT WORKS ═══════════ */}
@@ -342,7 +390,7 @@ export function PremiumLanding() { ))}

- “Dealix غيّر طريقة عمل فريق المبيعات عندنا بالكامل. من أول شهر زادت مبيعاتنا ٤٠٪ وصار عندنا رؤية واضحة لكل صفقة.” + “Dealix جمع لنا المسار والقنوات في مكان واحد؛ صار عندنا رؤية أوضح لكل صفقة وتقليل تشتيت بين الأدوات.”

عبدالله الشمري

diff --git a/salesflow-saas/frontend/src/components/dealix/vertical-playbooks-view.tsx b/salesflow-saas/frontend/src/components/dealix/vertical-playbooks-view.tsx new file mode 100644 index 00000000..6303594b --- /dev/null +++ b/salesflow-saas/frontend/src/components/dealix/vertical-playbooks-view.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { apiFetch } from "@/lib/api-client"; +import { BookOpen } from "lucide-react"; + +type Playbook = { + id: string; + label_ar: string; + label_en: string; + primary_channels: string[]; + approval_value_threshold_sar: number; + compliance_notes_ar: string[]; +}; + +export function VerticalPlaybooksView() { + const [items, setItems] = useState([]); + const [sel, setSel] = useState(null); + const [detail, setDetail] = useState(null); + const [err, setErr] = useState(null); + + useEffect(() => { + void (async () => { + const r = await apiFetch("/api/v1/strategic-deals/playbooks"); + if (!r.ok) { + setErr("تعذر تحميل القوالب القطاعية"); + return; + } + const j = (await r.json()) as { items: Playbook[] }; + setItems(j.items || []); + })(); + }, []); + + useEffect(() => { + if (!sel) { + setDetail(null); + return; + } + void (async () => { + const r = await apiFetch(`/api/v1/strategic-deals/playbooks/${sel}`); + if (r.ok) setDetail(await r.json()); + })(); + }, [sel]); + + return ( +
+
+ +
+

Playbooks قطاعية

+

+ قنوات أولى، عتبات موافقة، وملاحظات امتثال — تغذي الوكلاء والسياسات دون تخصيص كود لكل عميل. +

+
+
+ + {err &&
{err}
} + +
+
+ {items.map((p) => ( + + ))} +
+
+ {!detail ? ( +

اختر قطاعاً لعرض التفاصيل.

+ ) : ( + <> +

{detail.label_ar}

+

عتبة موافقة مقترحة: {detail.approval_value_threshold_sar?.toLocaleString("ar-SA")} ر.س

+
+

قنوات أولى

+
+ {detail.primary_channels?.map((c) => ( + + {c} + + ))} +
+
+
+

امتثال

+
    + {detail.compliance_notes_ar?.map((n, i) => ( +
  • {n}
  • + ))} +
+
+ + )} +
+
+
+ ); +} diff --git a/salesflow-saas/scripts/check_go_live_gate.py b/salesflow-saas/scripts/check_go_live_gate.py index 38287659..18e9d940 100644 --- a/salesflow-saas/scripts/check_go_live_gate.py +++ b/salesflow-saas/scripts/check_go_live_gate.py @@ -21,6 +21,12 @@ from pathlib import Path def main() -> int: + if hasattr(sys.stdout, "reconfigure"): + try: + sys.stdout.reconfigure(encoding="utf-8") + except Exception: + pass + saas = Path(__file__).resolve().parent.parent backend = saas / "backend" os.environ.setdefault("DATABASE_URL", "sqlite+aiosqlite:///./go_live_gate_cli.db") diff --git a/salesflow-saas/scripts/sync-marketing-to-public.cjs b/salesflow-saas/scripts/sync-marketing-to-public.cjs index 069e4973..0bab15a3 100644 --- a/salesflow-saas/scripts/sync-marketing-to-public.cjs +++ b/salesflow-saas/scripts/sync-marketing-to-public.cjs @@ -67,6 +67,29 @@ if (fs.existsSync(SRC_INTEGRATION)) { console.warn("SKIP INTEGRATION_MASTER doc (missing):", SRC_INTEGRATION); } +const SRC_LEGAL = path.join(ROOT, "docs", "legal"); +const DEST_LEGAL = path.join(DEST_STRATEGY_DIR, "legal"); +if (fs.existsSync(SRC_LEGAL)) { + fs.mkdirSync(DEST_LEGAL, { recursive: true }); + for (const f of fs.readdirSync(SRC_LEGAL)) { + if (f.endsWith(".md")) { + fs.copyFileSync(path.join(SRC_LEGAL, f), path.join(DEST_LEGAL, f)); + } + } + console.log("OK:", DEST_LEGAL); +} else { + console.warn("SKIP legal docs (missing):", SRC_LEGAL); +} + +const SRC_COMPETITIVE = path.join(ROOT, "docs", "COMPETITIVE_MATRIX_AR.md"); +if (fs.existsSync(SRC_COMPETITIVE)) { + fs.mkdirSync(DEST_STRATEGY_DIR, { recursive: true }); + fs.copyFileSync(SRC_COMPETITIVE, path.join(DEST_STRATEGY_DIR, "COMPETITIVE_MATRIX_AR.md")); + console.log("OK:", path.join(DEST_STRATEGY_DIR, "COMPETITIVE_MATRIX_AR.md")); +} else { + console.warn("SKIP competitive matrix (missing):", SRC_COMPETITIVE); +} + const readme = path.join(DEST_MARKETING, "LOCAL-ONLY-NEXT.txt"); fs.writeFileSync( readme, @@ -82,6 +105,8 @@ fs.writeFileSync( " http://localhost:3000/dealix-presentations/", " http://localhost:3000/resources", " http://localhost:3000/strategy", + " http://localhost:3000/strategy/legal/ (وثائق قانونية بعد المزامنة)", + " http://localhost:3000/strategy/COMPETITIVE_MATRIX_AR.md", "", "لتحديث النسخ بعد تعديل الملفات الأصلية:", " node scripts/sync-marketing-to-public.cjs",