# app/modules/prospecting/services/campaign_service.py """ Campaign management service. Handles campaign template CRUD, rendering with prospect data, and send tracking. """ import json import logging from datetime import UTC, datetime from sqlalchemy.orm import Session from app.modules.prospecting.exceptions import ( CampaignRenderException, CampaignTemplateNotFoundException, ) from app.modules.prospecting.models import ( CampaignSend, CampaignSendStatus, CampaignTemplate, Prospect, ) from app.modules.prospecting.services.prospect_service import prospect_service logger = logging.getLogger(__name__) class CampaignService: """Service for campaign template management and sending.""" # --- Template CRUD --- def get_templates( self, db: Session, *, lead_type: str | None = None, active_only: bool = False, ) -> list[CampaignTemplate]: query = db.query(CampaignTemplate) if lead_type: query = query.filter(CampaignTemplate.lead_type == lead_type) if active_only: query = query.filter(CampaignTemplate.is_active.is_(True)) return query.order_by(CampaignTemplate.lead_type, CampaignTemplate.name).all() def get_template_by_id(self, db: Session, template_id: int) -> CampaignTemplate: template = db.query(CampaignTemplate).filter(CampaignTemplate.id == template_id).first() if not template: raise CampaignTemplateNotFoundException(str(template_id)) return template def create_template(self, db: Session, data: dict) -> CampaignTemplate: template = CampaignTemplate( name=data["name"], lead_type=data["lead_type"], channel=data.get("channel", "email"), language=data.get("language", "fr"), subject_template=data.get("subject_template"), body_template=data["body_template"], is_active=data.get("is_active", True), ) db.add(template) db.commit() db.refresh(template) return template def update_template(self, db: Session, template_id: int, data: dict) -> CampaignTemplate: template = self.get_template_by_id(db, template_id) for field in ["name", "lead_type", "channel", "language", "subject_template", "body_template", "is_active"]: if field in data and data[field] is not None: setattr(template, field, data[field]) db.commit() db.refresh(template) return template def delete_template(self, db: Session, template_id: int) -> bool: template = self.get_template_by_id(db, template_id) db.delete(template) db.commit() return True # --- Rendering --- def render_campaign(self, db: Session, template_id: int, prospect_id: int) -> dict: """Render a campaign template with prospect data.""" template = self.get_template_by_id(db, template_id) prospect = prospect_service.get_by_id(db, prospect_id) placeholders = self._build_placeholders(prospect) try: rendered_subject = None if template.subject_template: rendered_subject = template.subject_template.format(**placeholders) rendered_body = template.body_template.format(**placeholders) except KeyError as e: raise CampaignRenderException(template_id, f"Missing placeholder: {e}") return { "subject": rendered_subject, "body": rendered_body, } # --- Sending --- def send_campaign( self, db: Session, template_id: int, prospect_ids: list[int], sent_by_user_id: int, ) -> list[CampaignSend]: """Create campaign send records for prospects.""" template = self.get_template_by_id(db, template_id) sends = [] for pid in prospect_ids: prospect = prospect_service.get_by_id(db, pid) placeholders = self._build_placeholders(prospect) try: rendered_subject = None if template.subject_template: rendered_subject = template.subject_template.format(**placeholders) rendered_body = template.body_template.format(**placeholders) except KeyError: rendered_body = template.body_template rendered_subject = template.subject_template send = CampaignSend( template_id=template_id, prospect_id=pid, channel=template.channel, rendered_subject=rendered_subject, rendered_body=rendered_body, status=CampaignSendStatus.SENT, sent_at=datetime.now(UTC), sent_by_user_id=sent_by_user_id, ) db.add(send) sends.append(send) db.commit() logger.info("Sent campaign %d to %d prospects", template_id, len(prospect_ids)) return sends def get_sends( self, db: Session, *, prospect_id: int | None = None, template_id: int | None = None, ) -> list[CampaignSend]: query = db.query(CampaignSend) if prospect_id: query = query.filter(CampaignSend.prospect_id == prospect_id) if template_id: query = query.filter(CampaignSend.template_id == template_id) return query.order_by(CampaignSend.created_at.desc()).all() def _build_placeholders(self, prospect: Prospect) -> dict: """Build template placeholder values from prospect data.""" contacts = prospect.contacts or [] primary_email = next((c.value for c in contacts if c.contact_type == "email"), "") primary_phone = next((c.value for c in contacts if c.contact_type == "phone"), "") reason_flags = [] if prospect.score and prospect.score.reason_flags: try: reason_flags = json.loads(prospect.score.reason_flags) except (json.JSONDecodeError, TypeError): pass issues_text = ", ".join(f.replace("_", " ") for f in reason_flags) return { "business_name": prospect.business_name or prospect.domain_name or "", "domain": prospect.domain_name or "", "score": str(prospect.score.score) if prospect.score else "—", "issues": issues_text, "primary_email": primary_email, "primary_phone": primary_phone, "city": prospect.city or "Luxembourg", } campaign_service = CampaignService()