from typing import Dict, Any, Optional, List from datetime import datetime, timedelta from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, and_ from app.models.customer import Customer, Conversation, Message import logging logger = logging.getLogger(__name__) class CustomerService: def __init__(self, db: AsyncSession): self.db = db async def list_customers(self, user_id: str, status: Optional[str] = None, page: int = 1, size: int = 20) -> Dict[str, Any]: query = select(Customer).where(Customer.user_id == user_id) count_query = select(func.count()).select_from(Customer).where(Customer.user_id == user_id) if status: query = query.where(Customer.status == status) count_query = count_query.where(Customer.status == status) query = query.order_by(Customer.updated_at.desc()).offset((page - 1) * size).limit(size) total = await self.db.execute(count_query) result = await self.db.execute(query) customers = result.scalars().all() return { "items": [self._to_dict(c) for c in customers], "total": total.scalar(), "page": page, "size": size, } async def get_customer(self, user_id: str, customer_id: str) -> Optional[Dict]: result = await self.db.execute( select(Customer).where( and_(Customer.id == customer_id, Customer.user_id == user_id) ) ) c = result.scalar_one_or_none() return self._to_dict(c) if c else None async def create_customer(self, user_id: str, data: Dict[str, Any]) -> Dict: c = Customer(user_id=user_id, **data) self.db.add(c) await self.db.flush() return self._to_dict(c) async def update_customer(self, user_id: str, customer_id: str, data: Dict[str, Any]) -> Optional[Dict]: result = await self.db.execute( select(Customer).where( and_(Customer.id == customer_id, Customer.user_id == user_id) ) ) c = result.scalar_one_or_none() if not c: return None for k, v in data.items(): if hasattr(c, k): setattr(c, k, v) await self.db.flush() return self._to_dict(c) async def delete_customer(self, user_id: str, customer_id: str) -> bool: result = await self.db.execute( select(Customer).where( and_(Customer.id == customer_id, Customer.user_id == user_id) ) ) c = result.scalar_one_or_none() if not c: return False await self.db.delete(c) return True async def get_silent_customers(self, user_id: str, days: int = 3) -> List[Dict]: cutoff = datetime.utcnow() - timedelta(days=days) result = await self.db.execute( select(Customer) .where( and_( Customer.user_id == user_id, Customer.status.in_(["lead", "negotiating"]), Customer.last_contact_at.isnot(None), Customer.last_contact_at < cutoff, ) ) .order_by(Customer.last_contact_at.asc()) ) return [self._to_dict(c) for c in result.scalars().all()] async def record_contact(self, user_id: str, customer_id: str): now = datetime.utcnow() result = await self.db.execute( select(Customer).where( and_(Customer.id == customer_id, Customer.user_id == user_id) ) ) c = result.scalar_one_or_none() if c: c.last_contact_at = now c.silence_started_at = None c.next_followup_at = now + timedelta(days=3) await self.db.flush() async def get_conversation(self, user_id: str, customer_id: str, page: int = 1, size: int = 50) -> Dict[str, Any]: conv_query = select(Conversation).where( and_(Conversation.user_id == user_id, Conversation.customer_id == customer_id) ).order_by(Conversation.created_at.desc()).limit(1) conv_result = await self.db.execute(conv_query) conv = conv_result.scalar_one_or_none() if not conv: return {"messages": [], "total": 0, "conversation_id": None} msg_query = ( select(Message) .where(Message.conversation_id == conv.id) .order_by(Message.created_at.asc()) .offset((page - 1) * size) .limit(size) ) msg_result = await self.db.execute(msg_query) messages = msg_result.scalars().all() return { "conversation_id": str(conv.id), "messages": [ { "id": str(m.id), "direction": m.direction, "content": m.content, "content_translated": m.content_translated, "ai_suggestions": m.ai_suggestions, "selected_suggestion": m.selected_suggestion, "created_at": m.created_at.isoformat() if m.created_at else None, } for m in messages ], "total": conv.message_count, } async def save_message( self, user_id: str, customer_id: str, direction: str, content: str, translation: Optional[str] = None, suggestions: Optional[List] = None, ) -> Dict: conv_result = await self.db.execute( select(Conversation).where( and_(Conversation.user_id == user_id, Conversation.customer_id == customer_id) ).order_by(Conversation.created_at.desc()).limit(1) ) conv = conv_result.scalar_one_or_none() if not conv: conv = Conversation( user_id=user_id, customer_id=customer_id, channel="whatsapp", status="active", ) self.db.add(conv) await self.db.flush() msg = Message( conversation_id=conv.id, direction=direction, content=content, content_translated=translation, ai_suggestions=suggestions, ) self.db.add(msg) conv.message_count = (conv.message_count or 0) + 1 conv.last_message_at = datetime.utcnow() await self.db.flush() await self.record_contact(user_id, customer_id) return { "message_id": str(msg.id), "conversation_id": str(conv.id), "direction": direction, "content": content, } def _to_dict(self, c: Customer) -> Dict: if not c: return {} return { "id": str(c.id), "name": c.name, "company": c.company, "country": c.country, "phone": c.phone, "email": c.email, "whatsapp_id": c.whatsapp_id, "source": c.source, "tags": c.tags, "status": c.status, "last_contact_at": c.last_contact_at.isoformat() if c.last_contact_at else None, "silence_days": (datetime.utcnow() - c.last_contact_at).days if c.last_contact_at else 0, "created_at": c.created_at.isoformat() if c.created_at else None, }