diff --git a/scripts/build_trucking_campaigns.py b/scripts/build_trucking_campaigns.py index a78c997..716a585 100644 --- a/scripts/build_trucking_campaigns.py +++ b/scripts/build_trucking_campaigns.py @@ -859,7 +859,21 @@ def listmonk_sendable(email: str) -> tuple[bool, str]: def get_base_campaign(campaign_id: int) -> dict: - return lm_api(f"/campaigns/{campaign_id}")["data"] + data = lm_api(f"/campaigns/{campaign_id}")["data"] + # Defensively strip Listmonk's @TrackLink marker from cloned bodies. Our CTAs + # are PER-SUBSCRIBER (`{{ lp_link }}`, `?dot=...`), and @TrackLink registers a + # SINGLE static URL per tracked link: it both 404s (the registered URL is + # captured with the `{{ lp_link }}` token unrendered -> `/order/slug&utm...` + # with no `?`) and collapses every recipient onto one carrier's redirect. + # Real human clicks are already attributed via Umami's campaign-click event, + # so dropping the marker loses nothing. This guard means a stale source + # campaign re-edited with @TrackLink can never reach recipients broken again. + body = data.get("body") + if body and "@TrackLink" in body: + data["body"] = body.replace("@TrackLink", "") + LOG.warning("[%s] stripped @TrackLink from cloned source body id=%s", + "get_base_campaign", campaign_id) + return data def create_list(name: str) -> int: @@ -1001,13 +1015,15 @@ def send_test(base: dict, campaign_id: int, sample_row: tuple, label: str, tz: s body = body.replace("{{ .Subscriber.Attribs.company }}", name or "Sample Carrier LLC") body = body.replace("{{ .Subscriber.Attribs.dot_number }}", dot or "0000000") body = body.replace("{{ .Subscriber.Attribs.state }}", state or "TX") - # Real subscribers get a populated lp_link attrib; the test send must mirror - # that or the CTA button (e.g. "Check My Emissions Status") renders as a bare - # "?dot=..." that links to nowhere. Build the same link the audience gets, - # using the target_state (the state the offer applies to, which for per-state - # programs comes from the deficiency flag, not the base state). + # Real subscribers get a populated lp_link attrib that already carries a + # leading `?` query (the carrier's `?dot=`, plus `?code=` when a coupon is + # on). The test send MUST mirror that: a bare path here would render the CTA + # as `/order/slug&utm_source=...` (the template appends `&utm...`) which has + # no `?` and 404s — exactly the broken owner-spot-check link. Use the same + # lp_link_with_coupon() the audience gets so the `?` is present. body = body.replace("{{ .Subscriber.Attribs.lp_link }}", - build_lp_link(campaign_type, target_state)) + lp_link_with_coupon(campaign_type, target_state, None, + dot=dot or "0000000")) # NOTE: leave {{ UnsubscribeURL }} alone — Listmonk renders it into a real, # working per-subscriber unsubscribe link (even on test sends). Overwriting it # produced a dead /unsubscribe link with no subscriber identity.