From 6227d59eaeb09b85b1be5fd424ce4920401447ed Mon Sep 17 00:00:00 2001 From: Will Estes Date: Thu, 23 Jul 2026 22:13:13 -0400 Subject: [PATCH] Track altbody in change-detection so existing campaigns actually re-sync Campaign/parseCampaign never fetched altbody, so campaignUnchanged() compared everything except it -- a campaign synced before v0.1.3 (altbody always null) whose content otherwise hasn't changed would be judged unchanged forever and never get updated with the new altbody, even though every subsequent sync now intends to send one. Co-Authored-By: Claude Sonnet 5 --- internal/campaign/sync.go | 4 ++++ internal/campaign/sync_test.go | 6 ++++-- internal/listmonk/listmonk.go | 3 +++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/campaign/sync.go b/internal/campaign/sync.go index 7fbdc78..cf6ab95 100644 --- a/internal/campaign/sync.go +++ b/internal/campaign/sync.go @@ -217,6 +217,10 @@ func attachmentFilename(slug, relPath string, content []byte) string { func campaignUnchanged(existing *listmonk.Campaign, input listmonk.CampaignInput) bool { return existing.Subject == input.Subject && existing.Body == input.Body && + // altbody always mirrors body (see CampaignInput.payload) -- comparing + // it here is what catches a campaign synced before that started, whose + // stored altbody is still null even though its body hasn't changed. + existing.AltBody == input.Body && existing.FromEmail == input.FromEmail && existing.TemplateID == input.TemplateID && existing.Type == input.Type && diff --git a/internal/campaign/sync_test.go b/internal/campaign/sync_test.go index 3dfdd78..0e19063 100644 --- a/internal/campaign/sync_test.go +++ b/internal/campaign/sync_test.go @@ -31,6 +31,7 @@ type fakeCampaign struct { Status string Subject string Body string + AltBody string FromEmail string TemplateID int Type string @@ -138,7 +139,7 @@ func (c fakeCampaign) toJSON() map[string]any { } return map[string]any{ "id": c.ID, "name": c.Name, "status": c.Status, "subject": c.Subject, - "body": c.Body, "from_email": c.FromEmail, "template_id": c.TemplateID, + "body": c.Body, "altbody": c.AltBody, "from_email": c.FromEmail, "template_id": c.TemplateID, "type": c.Type, "tags": c.Tags, "lists": lists, "media": media, } } @@ -155,7 +156,7 @@ func (f *fakeListmonk) createCampaign(w http.ResponseWriter, r *http.Request) { body := decodeBody(r) c := fakeCampaign{ ID: f.id(), Name: str(body["name"]), Status: "draft", - Subject: str(body["subject"]), Body: str(body["body"]), + Subject: str(body["subject"]), Body: str(body["body"]), AltBody: str(body["altbody"]), FromEmail: str(body["from_email"]), TemplateID: toInt(body["template_id"]), Type: str(body["type"]), Tags: toStrings(body["tags"]), ListIDs: toInts(body["lists"]), MediaIDs: toInts(body["media"]), @@ -171,6 +172,7 @@ func (f *fakeListmonk) updateCampaign(w http.ResponseWriter, r *http.Request) { if f.campaigns[i].ID == id { f.campaigns[i].Subject = str(body["subject"]) f.campaigns[i].Body = str(body["body"]) + f.campaigns[i].AltBody = str(body["altbody"]) f.campaigns[i].FromEmail = str(body["from_email"]) f.campaigns[i].TemplateID = toInt(body["template_id"]) f.campaigns[i].Type = str(body["type"]) diff --git a/internal/listmonk/listmonk.go b/internal/listmonk/listmonk.go index 9dae237..c4b0454 100644 --- a/internal/listmonk/listmonk.go +++ b/internal/listmonk/listmonk.go @@ -289,6 +289,7 @@ type Campaign struct { Status string Subject string Body string + AltBody string FromEmail string TemplateID int Type string @@ -381,6 +382,7 @@ func parseCampaign(data []byte) (*Campaign, error) { Status string `json:"status"` Subject string `json:"subject"` Body string `json:"body"` + AltBody string `json:"altbody"` FromEmail string `json:"from_email"` TemplateID int `json:"template_id"` Type string `json:"type"` @@ -409,6 +411,7 @@ func parseCampaign(data []byte) (*Campaign, error) { Status: parsed.Status, Subject: parsed.Subject, Body: parsed.Body, + AltBody: parsed.AltBody, FromEmail: parsed.FromEmail, TemplateID: parsed.TemplateID, Type: parsed.Type,