"message_type": MSG_TYPES, "attach_mode": {"none", "pdf", "file", "image"}, "rotation_strategy": {"sequential", "weighted"}, "subject_rotation": {"round_robin", "random", "fixed_first"}, "template_sequence_mode": {"repeat", "stop", "random"}, "phone_selection": {"round_robin", "random", "fixed"}, } def _ser(c: Campaign) -> dict: return { "id": c.id, "name": c.name, "status": c.status, "recipient_list_id": c.recipient_list_id, "subject_set_ids": c.subject_set_ids or [], "template_ids": c.template_ids or [], "message_type": c.message_type, "attach_mode": c.attach_mode, "attach_pdf": bool(c.attach_pdf), "attach_pdf_template_id": c.attach_pdf_template_id, "attachment_id": c.attachment_id, "variable_config": c.variable_config or {}, "account_ids": c.account_ids or [], "rotation_strategy": c.rotation_strategy, "rotation_weights": c.rotation_weights or {}, "subject_rotation": c.subject_rotation, "template_sequence_mode": c.template_sequence_mode, "phone_selection": c.phone_selection, "max_per_account": c.max_per_account, "send_interval_seconds": c.send_interval_seconds, "rate_per_minute": c.rate_per_minute, "max_retries": c.max_retries, "retry_delay_seconds": c.retry_delay_seconds, "unsubscribe_enabled": bool(c.unsubscribe_enabled), "preview_sample_size": c.preview_sample_size, "total_recipients": c.total_recipients, "sent_count": c.sent_count, "failed_count": c.failed_count, "suppressed_count": c.suppressed_count, "skipped_count": c.skipped_count, "queued_count": c.queued_count, "started_at": c.started_at.isoformat() if c.started_at else None, "finished_at": c.finished_at.isoformat() if c.finished_at else None, "created_at": c.created_at.isoformat() if c.created_at else None, } async def _get_own(db: AsyncSession, user: User, cid: int) -> Campaign: c = await db.get(Campaign, cid) if not c or c.user_id != user.id: raise HTTPException(status_code=404, detail="Campaign not found") return c async def _validate_refs(db: AsyncSession, user: User, data: CampaignIn): """Wizard-level validation: all referenced entities must exist & belong to the user.""" errors = [] if data.message_type not in MSG_TYPES: errors.append("message_type must be html|plain|both") if data.attach_mode not in VALID_ENUMS["attach_mode"]: errors.append("attach_mode must be none|pdf|file|image") if data.rotation_strategy not in VALID_ENUMS["rotation_strategy"]: errors.append("rotation_strategy must be sequential|weighted") if data.subject_rotation not in VALID_ENUMS["subject_rotation"]: errors.append("subject_rotation must be round_robin|random|fixed_first") if data.template_sequence_mode not in VALID_ENUMS["template_sequence_mode"]: errors.append("template_sequence_mode must be repeat|stop|random") if data.phone_selection not in VALID_ENUMS["phone_selection"]: errors.append("phone_selection must be round_robin|random|fixed") if data.recipient_list_id: