size = sample_size or c.preview_sample_size or 5 size = max(1, min(size, 20)) if not c.recipient_list_id: raise HTTPException(status_code=400, detail="Campaign has no recipient list") rl = await db.get(RecipientList, c.recipient_list_id) if not rl or rl.user_id != user.id: raise HTTPException(status_code=400, detail="Recipient list not found") recipient_rows = (await db.execute( select(Recipient).where(Recipient.list_id == rl.id, Recipient.is_valid == True) # noqa: E712 .order_by(Recipient.id).limit(size))).scalars().all() if not recipient_rows: raise HTTPException(status_code=400, detail="Recipient list has no valid recipients") # materialize plan rows WITHOUT persisting CampaignRecipient entities result = await materialize_campaign(db, c, recipient_rows, preview=True, preview_size=size) plan = result.get("plan", []) # render actual message content per sample engine = VariableEngine(c.variable_config or {}) out = [] for item in plan: rec = next((r for r in recipient_rows if r.id == item["recipient_id"]), None) if not rec: continue tpl = await db.get(EmailTemplate, item["template_id"]) if item.get("template_id") else None subj = await db.get(SubjectLine, item["subject_id"]) if item.get("subject_id") else None g = GeneratedValues() g.invoice_number = item.get("invoice_number", "") g.amount = item.get("amount") g.shipping = item.get("shipping") g.invoice_date = item.get("invoice_date") g.due_date = item.get("due_date") phone = item.get("phone", "") unsub = f"{cfg.UNSUBSCRIBE_BASE or cfg.BASE_URL}/unsubscribe/{rec.unsub_token}" if c.unsubscribe_enabled else "" extra = {"unsubscribe_link": unsub, "company_phone": phone} body_html = "" subject = "" if tpl: res = engine.substitute(tpl.body_html, rec, g, custom_fields=rec.custom_fields or {}, extra=extra) body_html = res.text subject = subj.subject if subj else (tpl.subject_hint or tpl.name) if subj: res_s = engine.substitute(subj.subject, rec, g, custom_fields=rec.custom_fields or {}, extra=extra) subject = res_s.text out.append({ "recipient": {"id": rec.id, "email": rec.email, "first_name": rec.first_name, "last_name": rec.last_name}, "from_account": item.get("account_email", ""), "subject": subject, "body_html": sanitize_preview(body_html), "raw_variables": {"invoice_number": g.invoice_number, "amount": g.amount, "shipping": g.shipping, "phone": phone}, "warnings": item.get("warnings", []), }) return {"campaign_id": c.id, "sample_size": len(out), "messages": out, "plan_summary": result.get("summary", {})} # --------------------------------------------------------------------------- # Control: start / pause / resume / cancel # --------------------------------------------------------------------------- @router.post("/{cid}/start")