fix(campaigns/reminder): softer tone + render expiry in tests

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>
This commit is contained in:
louispaulb 2026-06-01 11:55:20 -04:00
parent e64e1e6a1f
commit 73c42d6997
10 changed files with 47 additions and 20 deletions

View File

@ -160,6 +160,8 @@
<q-input v-model="testSendForm.vars.gift_url" label="gift_url" outlined dense class="col-12" /> <q-input v-model="testSendForm.vars.gift_url" label="gift_url" outlined dense class="col-12" />
<q-input v-model="testSendForm.vars.description" label="description" outlined dense class="col-12" /> <q-input v-model="testSendForm.vars.description" label="description" outlined dense class="col-12" />
<q-input v-model="testSendForm.vars.expiry" label="expiry" outlined dense class="col-12" /> <q-input v-model="testSendForm.vars.expiry" label="expiry" outlined dense class="col-12" />
<q-input v-model="testSendForm.vars.expires_at_date" label="expires_at_date (auto, date du wrapper)" outlined dense class="col-6" />
<q-input v-model="testSendForm.vars.expires_in_days" label="expires_in_days" outlined dense class="col-6" />
</div> </div>
</q-card-section> </q-card-section>
<q-card-section class="bg-grey-2"> <q-card-section class="bg-grey-2">
@ -529,6 +531,13 @@ async function openPreview () {
// Test-send dialog // Test-send dialog
const testSendOpen = ref(false) const testSendOpen = ref(false)
const testSending = ref(false) const testSending = ref(false)
// Sample expiry 30 days from now, locale-formatted FR. The Mustache
// section {{#expires_at_date}}{{/expires_at_date}} guards visible
// blocks, so leaving this blank would hide them in tests. We compute
// a realistic value so test sends and previews show the real layout.
const sampleExpiryDate = new Date(Date.now() + 30 * 86400 * 1000)
.toLocaleDateString('fr-CA', { day: 'numeric', month: 'long', year: 'numeric' })
const testSendForm = ref({ const testSendForm = ref({
to: 'louis@targo.ca', to: 'louis@targo.ca',
subject: '[TEST] Aperçu du courriel TARGO', subject: '[TEST] Aperçu du courriel TARGO',
@ -538,6 +547,8 @@ const testSendForm = ref({
gift_url: 'https://gft.link/TEST123', gift_url: 'https://gft.link/TEST123',
description: '123 Rue de Test, Ste-Clotilde', description: '123 Rue de Test, Ste-Clotilde',
expiry: '31 décembre 2026', expiry: '31 décembre 2026',
expires_at_date: sampleExpiryDate,
expires_in_days: '30',
}, },
}) })

View File

@ -228,8 +228,8 @@ table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: under
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:14px;word-break:break-word;" align="left" style="font-size:0px;padding:10px 25px;padding-bottom:14px;word-break:break-word;"
> >
<div <div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:18px;font-weight:700;line-height:1.4;text-align:left;color:#D03A0A;" style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:17px;font-weight:600;line-height:1.4;text-align:left;color:#1B2E24;"
>⏰ Hurry — your {{amount}} gift expires on {{expires_at_date}}.</div> >🎁 Your {{amount}} gift is still available until {{expires_at_date}}.</div>
</td> </td>
</tr> </tr>
@ -240,8 +240,8 @@ table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: under
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0;word-break:break-word;" align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0;word-break:break-word;"
> >
<div <div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:justify;color:#374151;">We sent you a gift card you can redeem at hundreds of Canadian brands — one click is all it takes.<br /> style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:justify;color:#374151;">We just wanted to make sure you didn't miss it — the gift card we sent you can be redeemed at hundreds of Canadian brands in just a few clicks.<br />
The window is closing soon. If you don't claim your gift before then, it'll no longer be available.</div> If you'd rather not use it, no worries — no need to reply to this email.</div>
</td> </td>
</tr> </tr>

File diff suppressed because one or more lines are too long

View File

@ -232,8 +232,8 @@ table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: under
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:14px;word-break:break-word;" align="left" style="font-size:0px;padding:10px 25px;padding-bottom:14px;word-break:break-word;"
> >
<div <div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:18px;font-weight:700;line-height:1.4;text-align:left;color:#D03A0A;" style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:17px;font-weight:600;line-height:1.4;text-align:left;color:#1B2E24;"
>⏰ Hâte-toi&nbsp;! Ton cadeau de {{amount}} expire le {{expires_at_date}}.</div> >🎁 Ton cadeau de {{amount}} reste disponible jusqu'au {{expires_at_date}}.</div>
</td> </td>
</tr> </tr>
@ -244,8 +244,8 @@ table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: under
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0;word-break:break-word;" align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0;word-break:break-word;"
> >
<div <div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:justify;color:#374151;">On t'a envoyé une carte-cadeau à utiliser chez des centaines de marques canadiennes — il te suffit d'un clic pour la réclamer.<br /> style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:justify;color:#374151;">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.<br />
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é.</div> Si tu préfères ne pas l'utiliser, aucun souci — pas besoin de répondre à ce courriel.</div>
</td> </td>
</tr> </tr>

File diff suppressed because one or more lines are too long

View File

@ -1815,6 +1815,12 @@ async function handle (req, res, method, path) {
} catch (e) { } catch (e) {
return json(res, 404, { error: 'template not found', detail: e.message }) return json(res, 404, { error: 'template not found', detail: e.message })
} }
// Sample wrapper expiry — 30 days out, locale-formatted FR. This is
// critical for the reminder template which uses {{expires_at_date}}
// as its main urgency line; without it the test email shows an
// empty space where the date should be.
const sampleExpAt = new Date(Date.now() + 30 * 86400 * 1000)
.toLocaleDateString('fr-CA', { day: 'numeric', month: 'long', year: 'numeric' })
const vars = { const vars = {
firstname: 'Louis', firstname: 'Louis',
lastname: 'Test', lastname: 'Test',
@ -1823,8 +1829,14 @@ async function handle (req, res, method, path) {
gift_url: 'https://gft.link/TEST123', gift_url: 'https://gft.link/TEST123',
amount: '60 $', amount: '60 $',
expiry: '31 décembre 2026', expiry: '31 décembre 2026',
expires_at_date: sampleExpAt,
expires_in_days: '30',
commitment_months: '3', commitment_months: '3',
year: new Date().getFullYear(), year: new Date().getFullYear(),
// view_url left empty so the {{#view_url}} section collapses —
// test emails go to internal addresses and don't need the web
// fallback link.
view_url: '',
...(body.vars || {}), ...(body.vars || {}),
} }
const rendered = renderTemplate(html, vars) const rendered = renderTemplate(html, vars)
@ -1855,10 +1867,14 @@ async function handle (req, res, method, path) {
if (tplPreview && method === 'POST') { if (tplPreview && method === 'POST') {
const body = await parseBody(req) const body = await parseBody(req)
const html = body.html || fs.readFileSync(templatePath(tplPreview[1]), 'utf8') const html = body.html || fs.readFileSync(templatePath(tplPreview[1]), 'utf8')
const sampleExpAt = new Date(Date.now() + 30 * 86400 * 1000)
.toLocaleDateString('fr-CA', { day: 'numeric', month: 'long', year: 'numeric' })
const vars = { const vars = {
firstname: 'Louis', lastname: 'Paul', email: 'louis@targo.ca', firstname: 'Louis', lastname: 'Paul', email: 'louis@targo.ca',
description: '123 Rue de Test', gift_url: 'http://gtbt.co/PREVIEW', description: '123 Rue de Test', gift_url: 'http://gtbt.co/PREVIEW',
amount: '60 $', expiry: '31 décembre 2026', commitment_months: '3', amount: '60 $', expiry: '31 décembre 2026', commitment_months: '3',
expires_at_date: sampleExpAt, expires_in_days: '30',
view_url: '',
...(body.vars || {}), ...(body.vars || {}),
} }
return json(res, 200, { rendered: renderTemplate(html, vars) }) return json(res, 200, { rendered: renderTemplate(html, vars) })

View File

@ -228,8 +228,8 @@ table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: under
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:14px;word-break:break-word;" align="left" style="font-size:0px;padding:10px 25px;padding-bottom:14px;word-break:break-word;"
> >
<div <div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:18px;font-weight:700;line-height:1.4;text-align:left;color:#D03A0A;" style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:17px;font-weight:600;line-height:1.4;text-align:left;color:#1B2E24;"
>⏰ Hurry — your {{amount}} gift expires on {{expires_at_date}}.</div> >🎁 Your {{amount}} gift is still available until {{expires_at_date}}.</div>
</td> </td>
</tr> </tr>
@ -240,8 +240,8 @@ table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: under
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0;word-break:break-word;" align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0;word-break:break-word;"
> >
<div <div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:justify;color:#374151;">We sent you a gift card you can redeem at hundreds of Canadian brands — one click is all it takes.<br /> style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:justify;color:#374151;">We just wanted to make sure you didn't miss it — the gift card we sent you can be redeemed at hundreds of Canadian brands in just a few clicks.<br />
The window is closing soon. If you don't claim your gift before then, it'll no longer be available.</div> If you'd rather not use it, no worries — no need to reply to this email.</div>
</td> </td>
</tr> </tr>

File diff suppressed because one or more lines are too long

View File

@ -232,8 +232,8 @@ table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: under
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:14px;word-break:break-word;" align="left" style="font-size:0px;padding:10px 25px;padding-bottom:14px;word-break:break-word;"
> >
<div <div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:18px;font-weight:700;line-height:1.4;text-align:left;color:#D03A0A;" style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:17px;font-weight:600;line-height:1.4;text-align:left;color:#1B2E24;"
>⏰ Hâte-toi&nbsp;! Ton cadeau de {{amount}} expire le {{expires_at_date}}.</div> >🎁 Ton cadeau de {{amount}} reste disponible jusqu'au {{expires_at_date}}.</div>
</td> </td>
</tr> </tr>
@ -244,8 +244,8 @@ table, td { color: #1B2E24; } #u_body a { color: #00C853; text-decoration: under
align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0;word-break:break-word;" align="left" style="font-size:0px;padding:10px 25px;padding-bottom:0;word-break:break-word;"
> >
<div <div
style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:justify;color:#374151;">On t'a envoyé une carte-cadeau à utiliser chez des centaines de marques canadiennes — il te suffit d'un clic pour la réclamer.<br /> style="font-family:Plus Jakarta Sans, Helvetica, Arial, sans-serif;font-size:16px;line-height:1.5;text-align:justify;color:#374151;">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.<br />
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é.</div> Si tu préfères ne pas l'utiliser, aucun souci — pas besoin de répondre à ce courriel.</div>
</td> </td>
</tr> </tr>

File diff suppressed because one or more lines are too long