592:    const payload = payloadFromC();
593-    try {
594-      let saved;
595-      if (editingId) { saved = await POST(`/campaigns/${editingId}/patch`, payload); toast("Campaign updated", "ok"); }
596-      else { saved = await POST("/campaigns/create", payload); toast(`Campaign #${saved.id} created`, "ok"); }
597-      closeModal();
598-      loadList();
599-      openMonitor(saved.id);
600-    } catch (e) {
601-      const errs = e.data?.detail?.errors;
602-      toast(errs && errs.length ? errs.join(" · ") : e.message, "err", 7000);
603-    }
604-  }
605-
606-  function snapshotFor(s) {
607-    return { name: C.name, recipient_list_id: C.recipient_list_id,
608-             subject_set_ids: C.subject_set_ids, subject_rotation: C.subject_rotation,
609-             template_ids: C.template_ids, template_sequence_mode: C.template_sequence_mode,
610-             attach_mode: C.attach_mode, attach_pdf_template_id: C.attach_pdf_template_id,
611-             attachment_id: C.attachment_id, variable_config: cleanVC(),
612-             account_ids: C.account_ids, rotation_strategy: C.rotation_strategy,
613-             rotation_weights: C.rotation_weights, max_per_account: C.max_per_account,
614-             send_interval_seconds: C.send_interval_seconds,
615-             phone_selection: C.phone_selection, unsubscribe_enabled: C.unsubscribe_enabled };
616-  }
617-  function cleanVC() {
618-    const out = {};
619-    for (const [k, v] of Object.entries(C.variable_config || {})) {
620-      if (v === undefined || v === null || v === "") continue;
621-      out[k] = v;
622-    }
--
625:  function payloadFromC() {
626-    const vc = cleanVC();
627-    const payload = {
628-      name: C.name, recipient_list_id: C.recipient_list_id,
---PDF-TEST---
            assert acc.last_send_date is not None
            assert acc.last_success_at is not None
        assert rec.calls == []  # no reschedules on success

    def test_pdf_attachment_generated_and_sent(self, db_engine, send_mod, monkeypatch):
        """attach_mode=pdf renders the PDF template with variables via real
        WeasyPrint and attaches a real PDF to the MIME message."""
        from sqlalchemy.orm import Session
        from app.models import EmailTemplate
        st, _ = send_mod
        ok_client = _OkClient()
        monkeypatch.setattr(st, "build_gmail_client_from_account",
                            lambda account, token, client_config=None: ok_client)
        with Session(db_engine, expire_on_commit=False) as db:
            f = _seed_sendable(db, attach_mode="pdf", attach_pdf=True)
            pdf_tpl = EmailTemplate(
                user_id=f["user"].id, name=f"pdftpl-{uuid.uuid4().hex[:6]}",
                subject_hint="att",
                body_html="<h1>Invoice %RandomInvoiceNumber%</h1>"
                          "<p>Customer: %FirstName%<br>Amount: %RandomTotal%</p>")
            db.add(pdf_tpl)
            db.commit()
            f["campaign"].attach_pdf_template_id = pdf_tpl.id
            db.commit()
            cid, crid = f["campaign"].id, f["cr"].id
        out = _call(st, cid, crid)
        assert out["ok"] is True and out["status"] == "sent"
        mime = ok_client.sent[0]
        raw = mime.as_string()
        assert "Invoice_INV-TEST-0001.pdf" in raw
        # extract the attachment payload and verify it is a real PDF with the
        # substituted content inside (streams are Flate-compressed, so extract
        # text with a real PDF text extractor — poppler's pdftotext)
        import base64
        import subprocess
        import tempfile
        from email import policy
        msg = __import__("email").message_from_string(raw, policy=policy.default)
        pdf_part = next(p for p in msg.iter_attachments()
                        if p.get_filename() == "Invoice_INV-TEST-0001.pdf")
        data = pdf_part.get_payload(decode=True)
