Surfaces the WHY of the gift in the reminder body. The original
campaign was sent ~10 days before the reminder fires — recipients
may have forgotten the loyalty/gratitude context, leading to a
"what is this?" reaction when they see the reminder cold.
Adding two words ("pour te remercier" / "as a thank-you") cheaply
reconnects with the original messaging and reinforces TARGO's
relationship framing.
FR: "La carte-cadeau qu'on t'a envoyée pour te remercier peut
s'utiliser chez des centaines de marques canadiennes..."
EN: "The gift card we sent you as a thank-you can be redeemed at
hundreds of Canadian brands..."
Greeting kept as "Petit rappel pour {{firstname}}," — first-name
personalization beats generic "toi" on engagement metrics, and the
firstname auto-clean covers ~99% of recipients.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Some readers (and several modern style guides) read em-dashes as
"AI-written" feel — the user preferred a mix of period (for full
clauses) and comma (for asides) to keep the copy conversational
without the long pause em-dashes impose.
Period when both sides are independent clauses:
- about fiber. They're about people too. (EN main + reminder)
- on Giftbit. Just click your X. (EN main + reminder)
- pas manqué. La carte-cadeau qu'on t'a envoyée… (FR reminder)
- didn't miss it. The gift card… (EN reminder)
Comma when the second half is an aside or starts with "and":
- something special, for a limited time. (EN main)
- right next door, and we genuinely love… (EN main)
- aucun souci, pas besoin… (FR reminder)
- no worries, no need to reply… (EN reminder)
gift-email-fr unchanged — its user-visible text never had em-dashes
(the 3 detected were inside HTML comments).
No hub restart needed: the send worker reads templates fresh from
disk on every campaign run, so the new copy applies on the very next
"Lancer l'envoi" click.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The reminder copy read as pushy on test sends ("Hâte-toi! ... Tu n'as
encore rien fait, et le délai approche"). Toned down to factual and
friendly: state availability + offer the no-pressure path.
FR before / after:
⏰ Hâte-toi! Ton cadeau de 60 $ expire le ___. (red bold)
→ 🎁 Ton cadeau de 60 $ reste disponible jusqu'au 1 juillet 2026.
(brand dark green)
Tu n'as encore rien fait, et le délai approche. Si tu n'utilises
pas ton cadeau d'ici là, il ne pourra plus être réclamé.
→ On voulait juste s'assurer que tu ne l'as pas manqué — la carte-
cadeau qu'on t'a envoyée peut s'utiliser chez des centaines de
marques canadiennes, en quelques clics.
Si tu préfères ne pas l'utiliser, aucun souci — pas besoin de
répondre à ce courriel.
EN copy mirrored.
Also: {{expires_at_date}} was rendering empty in test sends and
previews because neither the test-send endpoint, the preview
endpoint, nor the editor's testSendForm.vars seeded it. Three fixes:
- Hub preview endpoint: compute now+30d as default sample date.
- Hub test-send endpoint: same default + expose view_url='' so the
Mustache section block collapses cleanly in internal tests.
- Editor test-send dialog: pre-fill expires_at_date (and expires_in_
days) with the same now+30d value, plus expose both fields as
editable inputs so the operator can override per-test.
Verified live on prod: the preview endpoint with no vars now renders
"Ton cadeau de 60 $ reste disponible jusqu'au 1 juillet 2026."
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a "Créer une relance" button on the campaign detail page that
clones the parent campaign into a new draft, targeting only the
recipients who haven't clicked the Giftbit gift link yet.
Backend (POST /campaigns/:id/reminder):
- Filters parent recipients: status sent/opened, not excluded, not
revoked, wrapper not yet expired, has a gift_url.
- Builds a fresh recipients array — same gift_url (Giftbit shortlink),
same name/email/language/amount, but cleared gift_token so the worker
generates a brand-new wrapper at send time. Each campaign owns its
own click metrics.
- New campaign starts as 'draft' so the operator can review, tweak
subject/template, and click "Lancer l'envoi" when ready.
- Tracks parent_campaign_id + parent_row_index on each reminder row
for traceability in CSV reports and debugging.
Templates (gift-email-reminder-fr / gift-email-reminder-en):
- Header swap: "Petit rappel pour {firstname}" / "Quick reminder, X"
- Bold orange urgency line: "⏰ Hâte-toi! Ton cadeau de X expire le Y"
using the existing {{expires_at_date}} and {{amount}} merge vars
- Body shortened — drops the manifesto, focuses on "you have a gift,
redeem before it's gone"
- Same CTA button + prorata disclaimer + signature + footer as the
main templates so brand stays consistent.
UI:
- Button visible when campaign is sending/completed AND it's not
itself a reminder AND there's ≥ 1 eligible non-clicker.
- Confirmation dialog spells out the mechanics: same Giftbit URLs,
new wrapper tokens, reminder template, sample expiry date pulled
from the campaign's first recipient with a gift_expires_at.
- On OK, redirects to the new campaign's detail page.
Click stats on the existing campaign (cmp-20260522-2d4605) verified
intact before+after deploy (109 opens, 15 generic clicks, 27 gift CTA
clicks) — saveCampaign persists per-event so the hub restart was a
no-op for accumulated data.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>