mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-17 23:09:35 +00:00
Revolutionary AI system for autonomous B2B partnerships, negotiations, and deals: Models (strategic_deal.py - 238 lines): - CompanyProfile: Rich Saudi company profiles with CR, capabilities, needs - StrategicDeal: Full deal lifecycle (discovery → negotiation → close) - DealMatch: AI-generated company matches with scoring Services (4 files, ~2,060 lines): - company_profiler.py: Profile creation, AI enrichment, needs/capability analysis - deal_matcher.py: 6-dimension scoring, semantic matching, barter chain discovery - deal_negotiator.py: Multi-round Arabic negotiation with cultural awareness - deal_agent.py: Autonomous outreach via WhatsApp/LinkedIn/Email API (strategic_deals.py - 681 lines, 16 endpoints): - Profile management + AI enrichment - Match discovery + approval - Deal lifecycle (create → negotiate → proposal → term sheet → close) - Barter chain scanning - Analytics dashboard Deal types: partnership, distribution, franchise, JV, referral, acquisition, barter Channels: WhatsApp (primary), LinkedIn, Email Languages: Arabic (Saudi dialect) + English Cultural: Saudi negotiation norms, relationship-first, face-saving https://claude.ai/code/session_01LsnvBa7HwF5hs99VZbgLGj
682 lines
25 KiB
Python
682 lines
25 KiB
Python
"""
|
|
Strategic Deals API — B2B deal discovery, matching, negotiation, and outreach.
|
|
واجهة الصفقات الاستراتيجية: اكتشاف وتوفيق وتفاوض وتواصل الشراكات
|
|
"""
|
|
|
|
from datetime import datetime, timezone
|
|
from decimal import Decimal
|
|
from typing import Optional
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from pydantic import BaseModel as Schema, Field
|
|
from sqlalchemy import select, func
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.database import get_db
|
|
from app.api.deps import get_current_user
|
|
from app.models.user import User
|
|
from app.models.strategic_deal import (
|
|
CompanyProfile, StrategicDeal, DealMatch,
|
|
DealStatus, DealType, DealChannel, MatchStatus,
|
|
)
|
|
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
|
|
|
|
router = APIRouter(prefix="/strategic-deals", tags=["Strategic Deals"])
|
|
|
|
|
|
# ── Pydantic Schemas ─────────────────────────────────────────────────────────
|
|
|
|
|
|
class ProfileCreate(Schema):
|
|
company_name: str
|
|
company_name_ar: Optional[str] = None
|
|
industry: Optional[str] = None
|
|
sub_industry: Optional[str] = None
|
|
cr_number: Optional[str] = None
|
|
city: Optional[str] = None
|
|
region: Optional[str] = None
|
|
employee_count: Optional[int] = None
|
|
annual_revenue_sar: Optional[float] = None
|
|
capabilities: list[str] = []
|
|
needs: list[str] = []
|
|
deal_preferences: dict = {}
|
|
website: Optional[str] = None
|
|
linkedin_url: Optional[str] = None
|
|
whatsapp_number: Optional[str] = None
|
|
|
|
|
|
class ProfileResponse(Schema):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
company_name: str
|
|
company_name_ar: Optional[str] = None
|
|
industry: Optional[str] = None
|
|
sub_industry: Optional[str] = None
|
|
cr_number: Optional[str] = None
|
|
city: Optional[str] = None
|
|
region: Optional[str] = None
|
|
employee_count: Optional[float] = None
|
|
annual_revenue_sar: Optional[float] = None
|
|
capabilities: list = []
|
|
needs: list = []
|
|
deal_preferences: dict = {}
|
|
website: Optional[str] = None
|
|
linkedin_url: Optional[str] = None
|
|
whatsapp_number: Optional[str] = None
|
|
trust_score: float = 0.0
|
|
is_verified: bool = False
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class NeedsAnalysisRequest(Schema):
|
|
description: str = Field(..., description="وصف الاحتياجات بالعربي أو الإنجليزي")
|
|
|
|
|
|
class DealCreate(Schema):
|
|
initiator_profile_id: UUID
|
|
target_profile_id: Optional[UUID] = None
|
|
target_company_name: Optional[str] = None
|
|
target_contact_phone: Optional[str] = None
|
|
target_contact_email: Optional[str] = None
|
|
deal_type: str = "partnership"
|
|
deal_title: str
|
|
deal_title_ar: Optional[str] = None
|
|
our_offer: Optional[str] = None
|
|
our_need: Optional[str] = None
|
|
proposed_terms: dict = {}
|
|
estimated_value_sar: Optional[float] = None
|
|
channel: str = "whatsapp"
|
|
|
|
|
|
class DealResponse(Schema):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
initiator_profile_id: UUID
|
|
target_profile_id: Optional[UUID] = None
|
|
target_company_name: Optional[str] = None
|
|
target_contact_phone: Optional[str] = None
|
|
target_contact_email: Optional[str] = None
|
|
deal_type: str
|
|
deal_title: str
|
|
deal_title_ar: Optional[str] = None
|
|
our_offer: Optional[str] = None
|
|
our_need: Optional[str] = None
|
|
proposed_terms: dict = {}
|
|
agreed_terms: dict = {}
|
|
estimated_value_sar: Optional[float] = None
|
|
status: str
|
|
channel: str
|
|
ai_confidence: float = 0.0
|
|
negotiation_history: list = []
|
|
notes: Optional[str] = None
|
|
notes_ar: Optional[str] = None
|
|
created_at: datetime
|
|
updated_at: Optional[datetime] = None
|
|
closed_at: Optional[datetime] = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class MatchResponse(Schema):
|
|
id: UUID
|
|
tenant_id: UUID
|
|
company_a_id: UUID
|
|
company_b_id: Optional[UUID] = None
|
|
company_b_name: Optional[str] = None
|
|
company_b_data: dict = {}
|
|
match_score: float = 0.0
|
|
match_reasons: list = []
|
|
deal_type_suggested: Optional[str] = None
|
|
terms_suggested: dict = {}
|
|
status: str
|
|
created_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class NegotiateRequest(Schema):
|
|
their_terms: Optional[dict] = None
|
|
message: Optional[str] = None
|
|
strategy: Optional[NegotiationStrategy] = None
|
|
|
|
|
|
class OutreachRequest(Schema):
|
|
channel: str = "whatsapp"
|
|
style: str = "as_company"
|
|
|
|
|
|
class DiscoveryScanRequest(Schema):
|
|
profile_id: UUID
|
|
deal_type: Optional[str] = None
|
|
|
|
|
|
class BarterScanRequest(Schema):
|
|
profile_id: UUID
|
|
|
|
|
|
# ── Profile Endpoints ────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.post("/profiles", response_model=ProfileResponse, status_code=201)
|
|
async def create_profile(
|
|
data: ProfileCreate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Create a company profile for B2B matching. | إنشاء ملف شركة للمطابقة"""
|
|
profiler = CompanyProfiler()
|
|
profile = await profiler.create_profile(
|
|
company_data=data.model_dump(),
|
|
tenant_id=current_user.tenant_id,
|
|
db=db,
|
|
)
|
|
return ProfileResponse.model_validate(profile)
|
|
|
|
|
|
@router.put("/profiles/{profile_id}/enrich", response_model=ProfileResponse)
|
|
async def enrich_profile(
|
|
profile_id: UUID,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""AI-enrich a company profile. | إثراء ملف الشركة بالذكاء الاصطناعي"""
|
|
result = await db.execute(
|
|
select(CompanyProfile).where(
|
|
CompanyProfile.id == profile_id,
|
|
CompanyProfile.tenant_id == current_user.tenant_id,
|
|
)
|
|
)
|
|
if not result.scalar_one_or_none():
|
|
raise HTTPException(status_code=404, detail="الملف غير موجود | Profile not found")
|
|
|
|
profiler = CompanyProfiler()
|
|
profile = await profiler.enrich_profile(profile_id, db)
|
|
return ProfileResponse.model_validate(profile)
|
|
|
|
|
|
@router.post("/profiles/{profile_id}/analyze-needs")
|
|
async def analyze_needs(
|
|
profile_id: UUID,
|
|
data: NeedsAnalysisRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Analyze what a company needs (Arabic input). | تحليل احتياجات الشركة"""
|
|
result = await db.execute(
|
|
select(CompanyProfile).where(
|
|
CompanyProfile.id == profile_id,
|
|
CompanyProfile.tenant_id == current_user.tenant_id,
|
|
)
|
|
)
|
|
if not result.scalar_one_or_none():
|
|
raise HTTPException(status_code=404, detail="الملف غير موجود | Profile not found")
|
|
|
|
profiler = CompanyProfiler()
|
|
analysis = await profiler.analyze_needs(profile_id, data.description, db)
|
|
return {"status": "ok", "analysis": analysis}
|
|
|
|
|
|
# ── Match Endpoints ──────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.get("/matches", response_model=list[MatchResponse])
|
|
async def list_matches(
|
|
profile_id: UUID = Query(None, description="Filter by company profile"),
|
|
status: str = Query(None, description="Filter by match status"),
|
|
min_score: float = Query(None, ge=0, le=1, description="Minimum match score"),
|
|
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),
|
|
):
|
|
"""Get AI-suggested matches. | عرض المطابقات المقترحة بالذكاء الاصطناعي"""
|
|
query = select(DealMatch).where(DealMatch.tenant_id == current_user.tenant_id)
|
|
if profile_id:
|
|
query = query.where(
|
|
(DealMatch.company_a_id == profile_id) | (DealMatch.company_b_id == profile_id)
|
|
)
|
|
if status:
|
|
query = query.where(DealMatch.status == status)
|
|
if min_score is not None:
|
|
query = query.where(DealMatch.match_score >= min_score)
|
|
|
|
query = query.order_by(DealMatch.match_score.desc()).offset((page - 1) * per_page).limit(per_page)
|
|
result = await db.execute(query)
|
|
return [MatchResponse.model_validate(m) for m in result.scalars().all()]
|
|
|
|
|
|
@router.post("/matches/{match_id}/approve", response_model=MatchResponse)
|
|
async def approve_match(
|
|
match_id: UUID,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Approve a match for outreach. | الموافقة على مطابقة للتواصل"""
|
|
result = await db.execute(
|
|
select(DealMatch).where(
|
|
DealMatch.id == match_id,
|
|
DealMatch.tenant_id == current_user.tenant_id,
|
|
)
|
|
)
|
|
match = result.scalar_one_or_none()
|
|
if not match:
|
|
raise HTTPException(status_code=404, detail="المطابقة غير موجودة | Match not found")
|
|
if match.status != MatchStatus.SUGGESTED.value:
|
|
raise HTTPException(status_code=400, detail="المطابقة تمت الموافقة عليها مسبقاً | Match already processed")
|
|
|
|
match.status = MatchStatus.APPROVED.value
|
|
await db.flush()
|
|
await db.refresh(match)
|
|
return MatchResponse.model_validate(match)
|
|
|
|
|
|
# ── Discovery Scan ───────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.post("/scan", response_model=list[MatchResponse])
|
|
async def run_discovery_scan(
|
|
data: DiscoveryScanRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Run a full AI discovery scan for partners. | تشغيل فحص اكتشاف شامل"""
|
|
result = await db.execute(
|
|
select(CompanyProfile).where(
|
|
CompanyProfile.id == data.profile_id,
|
|
CompanyProfile.tenant_id == current_user.tenant_id,
|
|
)
|
|
)
|
|
if not result.scalar_one_or_none():
|
|
raise HTTPException(status_code=404, detail="الملف غير موجود | Profile not found")
|
|
|
|
agent = DealAgent()
|
|
matches = await agent.run_discovery_scan(data.profile_id, data.deal_type, db)
|
|
return [MatchResponse.model_validate(m) for m in matches]
|
|
|
|
|
|
# ── Deal CRUD ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.post("", response_model=DealResponse, status_code=201)
|
|
async def create_deal(
|
|
data: DealCreate,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Create a strategic deal. | إنشاء صفقة استراتيجية"""
|
|
# Verify initiator profile belongs to tenant
|
|
init_result = await db.execute(
|
|
select(CompanyProfile).where(
|
|
CompanyProfile.id == data.initiator_profile_id,
|
|
CompanyProfile.tenant_id == current_user.tenant_id,
|
|
)
|
|
)
|
|
if not init_result.scalar_one_or_none():
|
|
raise HTTPException(status_code=404, detail="ملف المبادر غير موجود | Initiator profile not found")
|
|
|
|
deal = StrategicDeal(
|
|
tenant_id=current_user.tenant_id,
|
|
initiator_profile_id=data.initiator_profile_id,
|
|
target_profile_id=data.target_profile_id,
|
|
target_company_name=data.target_company_name,
|
|
target_contact_phone=data.target_contact_phone,
|
|
target_contact_email=data.target_contact_email,
|
|
deal_type=data.deal_type,
|
|
deal_title=data.deal_title,
|
|
deal_title_ar=data.deal_title_ar,
|
|
our_offer=data.our_offer,
|
|
our_need=data.our_need,
|
|
proposed_terms=data.proposed_terms,
|
|
estimated_value_sar=Decimal(str(data.estimated_value_sar)) if data.estimated_value_sar else None,
|
|
status=DealStatus.DISCOVERY.value,
|
|
channel=data.channel,
|
|
ai_confidence=0.0,
|
|
negotiation_history=[],
|
|
)
|
|
db.add(deal)
|
|
await db.flush()
|
|
await db.refresh(deal)
|
|
return DealResponse.model_validate(deal)
|
|
|
|
|
|
@router.get("", response_model=list[DealResponse])
|
|
async def list_deals(
|
|
status: str = Query(None),
|
|
deal_type: str = Query(None),
|
|
profile_id: UUID = Query(None, description="Filter by initiator or target profile"),
|
|
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 strategic deals with filters. | عرض الصفقات الاستراتيجية"""
|
|
query = select(StrategicDeal).where(StrategicDeal.tenant_id == current_user.tenant_id)
|
|
if status:
|
|
query = query.where(StrategicDeal.status == status)
|
|
if deal_type:
|
|
query = query.where(StrategicDeal.deal_type == deal_type)
|
|
if profile_id:
|
|
query = query.where(
|
|
(StrategicDeal.initiator_profile_id == profile_id)
|
|
| (StrategicDeal.target_profile_id == profile_id)
|
|
)
|
|
|
|
query = query.order_by(StrategicDeal.created_at.desc()).offset((page - 1) * per_page).limit(per_page)
|
|
result = await db.execute(query)
|
|
return [DealResponse.model_validate(d) for d in result.scalars().all()]
|
|
|
|
|
|
@router.get("/{deal_id}", response_model=DealResponse)
|
|
async def get_deal(
|
|
deal_id: UUID,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Get deal details with negotiation history. | تفاصيل الصفقة مع سجل التفاوض"""
|
|
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")
|
|
return DealResponse.model_validate(deal)
|
|
|
|
|
|
# ── Negotiation ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.put("/{deal_id}/negotiate")
|
|
async def negotiate_deal(
|
|
deal_id: UUID,
|
|
data: NegotiateRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Submit terms, counter-offer, or free-text message. | تقديم شروط أو عرض مضاد"""
|
|
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")
|
|
|
|
negotiator = DealNegotiator()
|
|
|
|
# Check if we should escalate
|
|
should_escalate = await negotiator.should_escalate(deal_id, db)
|
|
if should_escalate:
|
|
return {
|
|
"status": "escalation_required",
|
|
"message_ar": "هذه الصفقة تحتاج تدخل بشري. يرجى التواصل مع مدير الحساب.",
|
|
"message_en": "This deal requires human intervention. Please contact the account manager.",
|
|
}
|
|
|
|
# Start new negotiation
|
|
if data.strategy and not deal.negotiation_history:
|
|
round_data = await negotiator.start_negotiation(deal_id, data.strategy, db)
|
|
return {
|
|
"status": "negotiation_started",
|
|
"round": round_data.round_number,
|
|
"action": round_data.action,
|
|
"our_terms": round_data.our_terms,
|
|
"message_ar": round_data.message_ar,
|
|
"message_en": round_data.message_en,
|
|
"confidence": round_data.confidence,
|
|
}
|
|
|
|
# Handle counter-offer
|
|
if data.their_terms:
|
|
round_data = await negotiator.handle_counter_offer(deal_id, data.their_terms, db)
|
|
return {
|
|
"status": "counter_processed",
|
|
"round": round_data.round_number,
|
|
"action": round_data.action,
|
|
"our_terms": round_data.our_terms,
|
|
"message_ar": round_data.message_ar,
|
|
"message_en": round_data.message_en,
|
|
"within_range": round_data.within_range,
|
|
"confidence": round_data.confidence,
|
|
}
|
|
|
|
# Handle free-text message
|
|
if data.message:
|
|
response = await negotiator.generate_response(deal_id, data.message, db)
|
|
return {
|
|
"status": "response_generated",
|
|
"response": response,
|
|
}
|
|
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail="يرجى تقديم شروط أو رسالة أو استراتيجية | Provide terms, message, or strategy",
|
|
)
|
|
|
|
|
|
# ── Outreach ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.post("/{deal_id}/outreach")
|
|
async def send_outreach(
|
|
deal_id: UUID,
|
|
data: OutreachRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Send outreach via channel. | إرسال تواصل عبر قناة"""
|
|
# Find the deal and verify ownership
|
|
deal_result = await db.execute(
|
|
select(StrategicDeal).where(
|
|
StrategicDeal.id == deal_id,
|
|
StrategicDeal.tenant_id == current_user.tenant_id,
|
|
)
|
|
)
|
|
deal = deal_result.scalar_one_or_none()
|
|
if not deal:
|
|
raise HTTPException(status_code=404, detail="الصفقة غير موجودة | Deal not found")
|
|
|
|
# Find or create a match for this deal to run outreach
|
|
match_result = await db.execute(
|
|
select(DealMatch).where(
|
|
DealMatch.company_a_id == deal.initiator_profile_id,
|
|
DealMatch.tenant_id == current_user.tenant_id,
|
|
).order_by(DealMatch.match_score.desc()).limit(1)
|
|
)
|
|
match = match_result.scalar_one_or_none()
|
|
|
|
if not match:
|
|
# Create a placeholder match for outreach
|
|
match = DealMatch(
|
|
tenant_id=current_user.tenant_id,
|
|
company_a_id=deal.initiator_profile_id,
|
|
company_b_id=deal.target_profile_id,
|
|
company_b_name=deal.target_company_name,
|
|
match_score=deal.ai_confidence or 0.5,
|
|
match_reasons=["تواصل مباشر من المستخدم"],
|
|
deal_type_suggested=deal.deal_type,
|
|
status=MatchStatus.APPROVED.value,
|
|
)
|
|
db.add(match)
|
|
await db.flush()
|
|
|
|
agent = DealAgent()
|
|
result = await agent.run_outreach_campaign(match.id, data.channel, db)
|
|
|
|
return {
|
|
"status": "sent" if result.success else "failed",
|
|
"channel": result.channel,
|
|
"message_sent": result.message_sent,
|
|
"next_action_ar": result.next_action_ar,
|
|
"error": result.error,
|
|
}
|
|
|
|
|
|
# ── Proposal & Term Sheet ───────────────────────────────────────────────────
|
|
|
|
|
|
@router.post("/{deal_id}/proposal")
|
|
async def generate_proposal(
|
|
deal_id: UUID,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Generate an Arabic business proposal. | إنشاء مقترح أعمال بالعربي"""
|
|
result = await db.execute(
|
|
select(StrategicDeal).where(
|
|
StrategicDeal.id == deal_id,
|
|
StrategicDeal.tenant_id == current_user.tenant_id,
|
|
)
|
|
)
|
|
if not result.scalar_one_or_none():
|
|
raise HTTPException(status_code=404, detail="الصفقة غير موجودة | Deal not found")
|
|
|
|
agent = DealAgent()
|
|
proposal = await agent.generate_proposal(deal_id, db)
|
|
return {"status": "ok", "proposal": proposal}
|
|
|
|
|
|
@router.post("/{deal_id}/term-sheet")
|
|
async def generate_term_sheet(
|
|
deal_id: UUID,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Generate an Arabic term sheet. | إنشاء ورقة شروط بالعربي"""
|
|
result = await db.execute(
|
|
select(StrategicDeal).where(
|
|
StrategicDeal.id == deal_id,
|
|
StrategicDeal.tenant_id == current_user.tenant_id,
|
|
)
|
|
)
|
|
if not result.scalar_one_or_none():
|
|
raise HTTPException(status_code=404, detail="الصفقة غير موجودة | Deal not found")
|
|
|
|
negotiator = DealNegotiator()
|
|
term_sheet = await negotiator.generate_term_sheet(deal_id, db)
|
|
return {"status": "ok", "term_sheet": term_sheet}
|
|
|
|
|
|
# ── Analytics ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.get("/analytics/overview")
|
|
async def deal_analytics(
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Deal flow analytics: match rate, close rate, avg deal value. | تحليلات الصفقات"""
|
|
tenant_id = current_user.tenant_id
|
|
|
|
# Total deals
|
|
total_q = select(func.count()).select_from(StrategicDeal).where(
|
|
StrategicDeal.tenant_id == tenant_id,
|
|
)
|
|
total_deals = (await db.execute(total_q)).scalar() or 0
|
|
|
|
# By status
|
|
status_q = select(
|
|
StrategicDeal.status, func.count()
|
|
).where(
|
|
StrategicDeal.tenant_id == tenant_id,
|
|
).group_by(StrategicDeal.status)
|
|
status_rows = (await db.execute(status_q)).all()
|
|
by_status = {row[0]: row[1] for row in status_rows}
|
|
|
|
won = by_status.get(DealStatus.CLOSED_WON.value, 0)
|
|
lost = by_status.get(DealStatus.CLOSED_LOST.value, 0)
|
|
closed = won + lost
|
|
close_rate = (won / closed * 100) if closed > 0 else 0.0
|
|
|
|
# Average deal value (closed won)
|
|
avg_val_q = select(func.avg(StrategicDeal.estimated_value_sar)).where(
|
|
StrategicDeal.tenant_id == tenant_id,
|
|
StrategicDeal.status == DealStatus.CLOSED_WON.value,
|
|
)
|
|
avg_value = (await db.execute(avg_val_q)).scalar()
|
|
avg_value_float = float(avg_value) if avg_value else 0.0
|
|
|
|
# Total matches and conversion
|
|
total_matches_q = select(func.count()).select_from(DealMatch).where(
|
|
DealMatch.tenant_id == tenant_id,
|
|
)
|
|
total_matches = (await db.execute(total_matches_q)).scalar() or 0
|
|
|
|
converted_q = select(func.count()).select_from(DealMatch).where(
|
|
DealMatch.tenant_id == tenant_id,
|
|
DealMatch.status == MatchStatus.CONVERTED.value,
|
|
)
|
|
converted_matches = (await db.execute(converted_q)).scalar() or 0
|
|
match_rate = (converted_matches / total_matches * 100) if total_matches > 0 else 0.0
|
|
|
|
# By deal type
|
|
type_q = select(
|
|
StrategicDeal.deal_type, func.count()
|
|
).where(
|
|
StrategicDeal.tenant_id == tenant_id,
|
|
).group_by(StrategicDeal.deal_type)
|
|
type_rows = (await db.execute(type_q)).all()
|
|
by_type = {row[0]: row[1] for row in type_rows}
|
|
|
|
return {
|
|
"total_deals": total_deals,
|
|
"by_status": by_status,
|
|
"close_rate_percent": round(close_rate, 1),
|
|
"avg_deal_value_sar": round(avg_value_float, 2),
|
|
"total_matches": total_matches,
|
|
"converted_matches": converted_matches,
|
|
"match_conversion_rate_percent": round(match_rate, 1),
|
|
"by_deal_type": by_type,
|
|
"labels_ar": {
|
|
"total_deals": "إجمالي الصفقات",
|
|
"close_rate": "نسبة الإغلاق",
|
|
"avg_value": "متوسط قيمة الصفقة",
|
|
"match_rate": "نسبة تحول المطابقات",
|
|
},
|
|
}
|
|
|
|
|
|
# ── Barter Scan ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
@router.post("/barter-scan")
|
|
async def barter_scan(
|
|
data: BarterScanRequest,
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
"""Find multi-party barter opportunities. | اكتشاف فرص المقايضة المتعددة"""
|
|
result = await db.execute(
|
|
select(CompanyProfile).where(
|
|
CompanyProfile.id == data.profile_id,
|
|
CompanyProfile.tenant_id == current_user.tenant_id,
|
|
)
|
|
)
|
|
if not result.scalar_one_or_none():
|
|
raise HTTPException(status_code=404, detail="الملف غير موجود | Profile not found")
|
|
|
|
matcher = DealMatcher()
|
|
chains = await matcher.find_barter_chains(data.profile_id, db)
|
|
|
|
return {
|
|
"status": "ok",
|
|
"chains_found": len(chains),
|
|
"chains": chains,
|
|
"summary_ar": (
|
|
f"تم العثور على {len(chains)} سلسلة مقايضة محتملة"
|
|
if chains
|
|
else "لم يتم العثور على فرص مقايضة. حاول إضافة المزيد من القدرات والاحتياجات في ملفك."
|
|
),
|
|
}
|