system-prompts-and-models-o.../salesflow-saas/backend/app/api/v1/strategic_deals.py
Claude d7a5af9156
feat: Add Strategic Deals Engine — autonomous B2B deal-making system
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
2026-04-11 09:15:29 +00:00

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 "لم يتم العثور على فرص مقايضة. حاول إضافة المزيد من القدرات والاحتياجات في ملفك."
),
}