from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks, Query from sqlalchemy.orm import Session from sqlalchemy import func from typing import List from datetime import datetime from app.db.base import get_db from app.core.deps import get_current_user from app.models.user import User from app.models.campaign import Campaign, CampaignRecipient, CampaignStatus, RecipientStatus from app.models.template import Template from app.models.list import List as ContactList, ListMember from app.models.contact import Contact, DNDList from app.models.job import Job from app.schemas.campaign import ( CampaignCreate, CampaignUpdate, CampaignResponse, RecipientResponse, CampaignPreview ) router = APIRouter() @router.post("", response_model=CampaignResponse, status_code=status.HTTP_201_CREATED) def create_campaign( campaign: CampaignCreate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): # Verify template exists template = db.query(Template).filter( Template.id == campaign.template_id, Template.user_id == current_user.id ).first() if not template: raise HTTPException(status_code=404, detail="Template not found") # Verify list exists contact_list = db.query(ContactList).filter( ContactList.id == campaign.list_id, ContactList.user_id == current_user.id ).first() if not contact_list: raise HTTPException(status_code=404, detail="List not found") # Create campaign db_campaign = Campaign( user_id=current_user.id, name=campaign.name, template_id=campaign.template_id, list_id=campaign.list_id, status=CampaignStatus.DRAFT, scheduled_at=campaign.scheduled_at ) db.add(db_campaign) db.commit() db.refresh(db_campaign) # Create recipients members = db.query(ListMember).filter(ListMember.list_id == campaign.list_id).all() for member in members: recipient = CampaignRecipient( campaign_id=db_campaign.id, contact_id=member.contact_id, status=RecipientStatus.PENDING ) db.add(recipient) db.commit() return db_campaign @router.get("", response_model=List[CampaignResponse]) def list_campaigns( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): return db.query(Campaign).filter( Campaign.user_id == current_user.id ).order_by(Campaign.created_at.desc()).offset(skip).limit(limit).all() @router.get("/{campaign_id}", response_model=CampaignResponse) def get_campaign( campaign_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): campaign = db.query(Campaign).filter( Campaign.id == campaign_id, Campaign.user_id == current_user.id ).first() if not campaign: raise HTTPException(status_code=404, detail="Campaign not found") return campaign @router.put("/{campaign_id}", response_model=CampaignResponse) def update_campaign( campaign_id: int, campaign_update: CampaignUpdate, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): campaign = db.query(Campaign).filter( Campaign.id == campaign_id, Campaign.user_id == current_user.id ).first() if not campaign: raise HTTPException(status_code=404, detail="Campaign not found") update_data = campaign_update.dict(exclude_unset=True) for field, value in update_data.items(): setattr(campaign, field, value) db.commit() db.refresh(campaign) return campaign @router.delete("/{campaign_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_campaign( campaign_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): campaign = db.query(Campaign).filter( Campaign.id == campaign_id, Campaign.user_id == current_user.id ).first() if not campaign: raise HTTPException(status_code=404, detail="Campaign not found") db.delete(campaign) db.commit() return None @router.get("/{campaign_id}/preview", response_model=CampaignPreview) def preview_campaign( campaign_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Preview campaign recipients and compliance stats""" campaign = db.query(Campaign).filter( Campaign.id == campaign_id, Campaign.user_id == current_user.id ).first() if not campaign: raise HTTPException(status_code=404, detail="Campaign not found") # Get all recipients recipients = db.query(CampaignRecipient).filter( CampaignRecipient.campaign_id == campaign_id ).all() total_count = len(recipients) opted_in_count = 0 dnd_count = 0 sample_contacts = [] for recipient in recipients[:5]: # Sample first 5 contact = db.query(Contact).filter(Contact.id == recipient.contact_id).first() if not contact: continue # Check if opted in if contact.opted_in: opted_in_count += 1 # Check DND dnd_entry = db.query(DNDList).filter( DNDList.user_id == current_user.id, DNDList.phone_e164 == contact.phone_e164 ).first() if dnd_entry: dnd_count += 1 sample_contacts.append({ "phone": contact.phone_e164, "name": f"{contact.first_name or ''} {contact.last_name or ''}".strip(), "opted_in": contact.opted_in, "in_dnd": dnd_entry is not None }) # Count all opted in all_contact_ids = [r.contact_id for r in recipients] opted_in_count = db.query(Contact).filter( Contact.id.in_(all_contact_ids), Contact.opted_in == True ).count() # Count all DND all_phones = [ c.phone_e164 for c in db.query(Contact.phone_e164).filter( Contact.id.in_(all_contact_ids) ).all() ] dnd_count = db.query(DNDList).filter( DNDList.user_id == current_user.id, DNDList.phone_e164.in_(all_phones) ).count() eligible_count = total_count - dnd_count return CampaignPreview( total_recipients=total_count, opted_in_count=opted_in_count, dnd_count=dnd_count, eligible_count=eligible_count, sample_contacts=sample_contacts ) @router.post("/{campaign_id}/send") def send_campaign( campaign_id: int, background_tasks: BackgroundTasks, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Start sending campaign (creates a job)""" campaign = db.query(Campaign).filter( Campaign.id == campaign_id, Campaign.user_id == current_user.id ).first() if not campaign: raise HTTPException(status_code=404, detail="Campaign not found") if campaign.status not in [CampaignStatus.DRAFT, CampaignStatus.SCHEDULED]: raise HTTPException(status_code=400, detail="Campaign already sent or in progress") # Update status campaign.status = CampaignStatus.SENDING db.commit() # Create job for background processing job = Job( user_id=current_user.id, type="send_campaign", payload_json={"campaign_id": campaign_id}, status="pending", attempts=0 ) db.add(job) db.commit() return { "status": "started", "campaign_id": campaign_id, "job_id": job.id } @router.post("/{campaign_id}/reset") def reset_campaign( campaign_id: int, current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Reset campaign to draft and all recipients to pending""" campaign = db.query(Campaign).filter( Campaign.id == campaign_id, Campaign.user_id == current_user.id ).first() if not campaign: raise HTTPException(status_code=404, detail="Campaign not found") # Reset campaign status campaign.status = CampaignStatus.DRAFT # Reset all recipients to pending db.query(CampaignRecipient).filter( CampaignRecipient.campaign_id == campaign_id ).update({ "status": RecipientStatus.PENDING, "provider_message_id": None, "last_error": None }) # Delete any pending jobs for this campaign from sqlalchemy import cast, String db.query(Job).filter( Job.type == "send_campaign", cast(Job.payload_json["campaign_id"], String) == str(campaign_id), Job.status == "pending" ).delete(synchronize_session=False) db.commit() return { "status": "reset", "campaign_id": campaign_id, "message": "Campaign reset to draft. All recipients set to pending." } @router.get("/{campaign_id}/recipients", response_model=List[RecipientResponse]) def get_campaign_recipients( campaign_id: int, status: str = Query(None), skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), current_user: User = Depends(get_current_user), db: Session = Depends(get_db) ): """Get campaign recipients with optional status filter""" campaign = db.query(Campaign).filter( Campaign.id == campaign_id, Campaign.user_id == current_user.id ).first() if not campaign: raise HTTPException(status_code=404, detail="Campaign not found") query = db.query(CampaignRecipient).filter( CampaignRecipient.campaign_id == campaign_id ) if status: query = query.filter(CampaignRecipient.status == status) recipients = query.offset(skip).limit(limit).all() # Enrich with contact data result = [] for recipient in recipients: contact = db.query(Contact).filter(Contact.id == recipient.contact_id).first() recipient_dict = { "id": recipient.id, "campaign_id": recipient.campaign_id, "contact_id": recipient.contact_id, "status": recipient.status, "provider_message_id": recipient.provider_message_id, "last_error": recipient.last_error, "updated_at": recipient.updated_at, "contact_phone": contact.phone_e164 if contact else None, "contact_name": f"{contact.first_name or ''} {contact.last_name or ''}".strip() if contact else None } result.append(recipient_dict) return result