system-prompts-and-models-o.../salesflow-saas/backend/app/api/v1/sales_os.py
2026-04-04 18:04:21 +03:00

162 lines
5.9 KiB
Python

"""Sales OS: commission ledger (شفافية عمولة)، مهام، حصص، تأهيل مندوب."""
from __future__ import annotations
from typing import Any, Dict, Optional
from fastapi import APIRouter, Depends
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.api.deps import get_current_user, get_optional_user, require_role, get_current_tenant
from app.models.user import User
from app.models.tenant import Tenant
from app.services.sales_os_service import (
build_commission_ledger,
build_daily_digest,
build_deal_health,
build_manager_team_summary,
demo_commission_ledger,
merge_quota_view,
pipeline_value_open_deals,
pipeline_value_open_deals_scoped,
rep_onboarding_playbook,
tasks_inbox_today,
)
router = APIRouter(prefix="/sales-os", tags=["Sales OS"])
class QuotaUpdate(BaseModel):
default_monthly_quota_sar: Optional[float] = None
rep_quotas: Optional[Dict[str, float]] = Field(default=None, description="user_id -> monthly SAR target")
@router.get("/commission-ledger")
async def commission_ledger(
db: AsyncSession = Depends(get_db),
user: Optional[User] = Depends(get_optional_user),
):
"""
صفقة → عمولة → دفعة. بدون توكن: بيانات توضيحية. مع توكن: بيانات المستأجر (أو توضيحي إن فارغ).
"""
if user:
data = await build_commission_ledger(db, user.tenant_id)
if not data["items"]:
return demo_commission_ledger()
return data
return demo_commission_ledger()
@router.get("/tasks-inbox")
async def tasks_inbox(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""أنشطة مرتبطة بالمستخدم — بداية صندوق مهام."""
items = await tasks_inbox_today(db, user.tenant_id, user.id)
return {"items": items, "count": len(items)}
@router.get("/quota")
async def quota_overview(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
tenant: Tenant = Depends(get_current_tenant),
):
pipeline = await pipeline_value_open_deals_scoped(
db, user.tenant_id, user_id=user.id, role=user.role or "agent"
)
settings = tenant.settings if isinstance(tenant.settings, dict) else {}
return merge_quota_view(settings, user.id, pipeline)
@router.put("/quota")
async def quota_update(
body: QuotaUpdate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_role("owner", "admin", "manager")),
tenant: Tenant = Depends(get_current_tenant),
):
base = tenant.settings if isinstance(tenant.settings, dict) else {}
sales_os = dict(base.get("sales_os") or {})
if body.default_monthly_quota_sar is not None:
sales_os["default_monthly_quota_sar"] = body.default_monthly_quota_sar
if body.rep_quotas is not None:
sales_os["rep_quotas"] = {str(k): float(v) for k, v in body.rep_quotas.items()}
tenant.settings = {**base, "sales_os": sales_os}
db.add(tenant)
await db.flush()
pipeline = await pipeline_value_open_deals_scoped(
db, user.tenant_id, user_id=user.id, role=user.role or "agent"
)
return merge_quota_view(tenant.settings or {}, user.id, pipeline)
@router.get("/rep-onboarding")
async def rep_onboarding_playbook_endpoint():
"""مسار 7/14/30 يوم — محتوى ثابت يُربط لاحقاً بتتبع DB."""
return rep_onboarding_playbook()
@router.get("/overview")
async def sales_os_overview(
db: AsyncSession = Depends(get_db),
user: Optional[User] = Depends(get_optional_user),
):
"""لقطة واحدة للواجهة: عمولة + حصة + مهام + تأهيل."""
out: Dict[str, Any] = {"rep_onboarding": rep_onboarding_playbook()}
if user:
t_result = await db.execute(select(Tenant).where(Tenant.id == user.tenant_id))
tenant = t_result.scalar_one_or_none()
settings = tenant.settings if tenant and isinstance(tenant.settings, dict) else {}
pipeline = await pipeline_value_open_deals_scoped(
db, user.tenant_id, user_id=user.id, role=user.role or "agent"
)
ledger = await build_commission_ledger(db, user.tenant_id)
if not ledger["items"]:
ledger = demo_commission_ledger()
out["commission_ledger"] = ledger
out["quota"] = merge_quota_view(settings, user.id, pipeline)
out["tasks"] = await tasks_inbox_today(db, user.tenant_id, user.id)
out["daily_digest"] = await build_daily_digest(
db, user.tenant_id, user.id, user.role or "agent", settings
)
else:
out["commission_ledger"] = demo_commission_ledger()
out["quota"] = None
out["tasks"] = []
out["daily_digest"] = None
return out
@router.get("/daily-digest")
async def daily_digest(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
tenant: Tenant = Depends(get_current_tenant),
):
"""ملخّص يومي: مهام، حصة، إغلاقات قريبة، اقتراحات."""
settings = tenant.settings if isinstance(tenant.settings, dict) else {}
return await build_daily_digest(db, user.tenant_id, user.id, user.role or "agent", settings)
@router.get("/manager-summary")
async def manager_summary(
db: AsyncSession = Depends(get_db),
user: User = Depends(require_role("owner", "admin", "manager")),
):
"""أنبوب الفريق حسب المندوب — للمدير."""
return await build_manager_team_summary(db, user.tenant_id)
@router.get("/deal-health")
async def deal_health(
db: AsyncSession = Depends(get_db),
user: User = Depends(get_current_user),
):
"""صحة الصفقات المفتوحة — إشارات أولية."""
return await build_deal_health(db, user.tenant_id, user.id, user.role or "agent")