feat(leads): personalised re-engagement campaign for captured PDF leads - #118
Merged
Merged
Conversation
Adds an admin-only endpoint that emails people who ran a scan and asked for
the PDF, driving them to /hire.
Not a broadcast. Each email is built from the recipient's OWN most recent
report — their host, their score, their top three failing findings — with a
/hire link prefilled with the site and address we already have. That is the
thing they asked us for, so it converts better than a newsletter and stays
tied to the request that captured the address.
Why not /api/admin/email-broadcast: it selects from `profiles`, so it
structurally cannot reach a lead who never registered (51 of the 108 we
have). It also sends raw HTML through sendBulk with no unsubscribe footer
and no List-Unsubscribe headers, and applies no unsubscribed_at filter at
all — nobody has unsubscribed yet so no harm has occurred, but the first
person who does would keep receiving mail. This route goes through
sendMarketingEmail, which adds both, and filters the suppression list twice:
once when selecting the audience and again immediately before each send, in
case someone opts out mid-run.
Audience selection is a pure module with its own tests, because choosing
wrongly doesn't throw — it silently mails someone who opted out. Excluded:
unsubscribed (which beats a later consent record), our own domains, role
accounts, and anyone with no report to talk about. Segmentable into existing
customers vs cold leads, deduped per address.
DRY RUN BY DEFAULT — sending requires an explicit {"dryRun": false}, and a
testTo override sends the whole thing to one inbox first. Paced at 400ms
between sends.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vu1nz Security Review0 finding(s) in PR #? No security issues found. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Emails the people who ran a scan and asked for the PDF, driving them to
/hire.Not a broadcast. Each email is built from the recipient's own most recent report — their host, their score, their top three failing findings — with a
/hirelink prefilled with the site and address we already hold. It's about the thing they asked us for.Why not
/api/admin/email-broadcastThat route selects from
profiles, so it structurally cannot reach a lead who never registered — 51 of our 108. It also sends raw HTML viasendBulk, bypassingsendMarketingEmail, so it adds no unsubscribe footer and noList-Unsubscribeheaders, and applies nounsubscribed_atfilter. Nobody has unsubscribed yet so no harm has occurred, but the first person who does would keep receiving mail. Worth fixing separately.This route uses
sendMarketingEmail(footer + one-click headers) and checks suppression twice — when selecting the audience, and again immediately before each send in case someone opts out mid-run.Safety
{"dryRun": false}; an accidental POST mails nobody.testTosends the entire campaign to one inbox for a real-world preview.postmaster@,no-reply@…), and anyone with no report to talk about.users(existing relationship) vsleads(cold) vsall.Audience selection is a pure module (
lib/leadCampaign.ts) with 15 tests, because choosing wrongly doesn't throw an exception — it silently mails someone who opted out.Usage
Verification
npm run typecheckclean, 549 tests pass (15 new),npm run buildexits 0. No email has been sent.🤖 Generated with Claude Code