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="

Invoice %RandomInvoiceNumber%

" "

Customer: %FirstName%
Amount: %RandomTotal%

") 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)