system-prompts-and-models-o.../salesflow-saas/backend/app/api/v1/followups.py

163 lines
5.1 KiB
Python

"""Follow-up Scheduler — generates follow-up drafts for unreplied outreach.
Checks sent drafts that haven't received replies after 2/5/10 days
and creates new follow-up drafts linked to the original.
"""
from __future__ import annotations
import logging
from datetime import datetime, timedelta, timezone
from typing import Any, Dict, List
from uuid import uuid4
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel
from sqlalchemy import select, and_
from sqlalchemy.ext.asyncio import AsyncSession
logger = logging.getLogger("dealix.followups")
router = APIRouter(prefix="/followups", tags=["Follow-ups"])
async def _get_db():
from app.database import get_db
async for session in get_db():
yield session
@router.get("/due")
async def list_due_followups(
days_since_sent: int = Query(2, ge=1, le=30),
limit: int = Query(50, ge=1, le=200),
db: AsyncSession = Depends(_get_db),
) -> Dict[str, Any]:
from app.models.outreach_draft import OutreachDraft
cutoff = datetime.now(timezone.utc) - timedelta(days=days_since_sent)
stmt = (
select(OutreachDraft)
.where(
and_(
OutreachDraft.status == "sent",
OutreachDraft.sent_at <= cutoff,
OutreachDraft.reply_text.is_(None),
)
)
.order_by(OutreachDraft.sent_at.asc())
.limit(limit)
)
result = await db.execute(stmt)
rows = list(result.scalars().all())
due = []
for row in rows:
days_elapsed = (datetime.now(timezone.utc) - row.sent_at).days if row.sent_at else 0
followup_text = ""
followup_type = ""
if days_elapsed >= 10:
followup_text = row.followup_5d or "آخر متابعة — لو مو الوقت المناسب أفهم تماماً. شكراً."
followup_type = "day_10_breakup"
elif days_elapsed >= 5:
followup_text = row.followup_5d or row.followup_2d or ""
followup_type = "day_5_value"
elif days_elapsed >= 2:
followup_text = row.followup_2d or ""
followup_type = "day_2_reminder"
if followup_text:
due.append({
"original_draft_id": str(row.id),
"company": row.company,
"channel": row.channel,
"contact_email": row.contact_email,
"contact_phone": row.contact_phone,
"days_since_sent": days_elapsed,
"followup_type": followup_type,
"followup_text": followup_text,
"sector": row.sector,
})
return {
"due_count": len(due),
"cutoff_days": days_since_sent,
"followups": due,
}
class GenerateFollowupsRequest(BaseModel):
days_since_sent: int = 2
max_followups: int = 20
@router.post("/generate")
async def generate_followup_drafts(
req: GenerateFollowupsRequest,
db: AsyncSession = Depends(_get_db),
) -> Dict[str, Any]:
from app.models.outreach_draft import OutreachDraft
cutoff = datetime.now(timezone.utc) - timedelta(days=req.days_since_sent)
stmt = (
select(OutreachDraft)
.where(
and_(
OutreachDraft.status == "sent",
OutreachDraft.sent_at <= cutoff,
OutreachDraft.reply_text.is_(None),
)
)
.order_by(OutreachDraft.sent_at.asc())
.limit(req.max_followups)
)
result = await db.execute(stmt)
originals = list(result.scalars().all())
created = 0
batch_id = f"followup_{datetime.now(timezone.utc).strftime('%Y%m%d')}_{str(uuid4())[:6]}"
for orig in originals:
days_elapsed = (datetime.now(timezone.utc) - orig.sent_at).days if orig.sent_at else 0
if days_elapsed >= 10:
body = orig.followup_5d or "آخر متابعة — لو مناسب نتكلم، أنا موجود. لو لا، شكراً على وقتكم."
subject = f"متابعة أخيرة: {orig.company}"
elif days_elapsed >= 5:
body = orig.followup_5d or orig.followup_2d or ""
subject = f"متابعة: {orig.company}"
else:
body = orig.followup_2d or ""
subject = f"متابعة سريعة: {orig.company}"
if not body:
continue
followup = OutreachDraft(
batch_id=batch_id,
company=orig.company,
contact_name=orig.contact_name,
contact_email=orig.contact_email,
contact_phone=orig.contact_phone,
channel=orig.channel,
subject=subject,
body=body,
sector=orig.sector,
city=orig.city,
fit_score=orig.fit_score,
risk_score=orig.risk_score,
status="draft",
approval_required=True,
source=f"followup_day_{days_elapsed}_of_{str(orig.id)[:8]}",
)
db.add(followup)
created += 1
if created:
await db.commit()
return {
"batch_id": batch_id,
"followups_created": created,
"originals_checked": len(originals),
}