From ddedd603207169dbaa3e711079b6cfd6ee67a3f8 Mon Sep 17 00:00:00 2001 From: louispaulb Date: Mon, 1 Jun 2026 14:58:54 -0400 Subject: [PATCH] fix(campaigns/expiry): format dates in America/Montreal, not container UTC MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The targo-hub container runs with TZ=UTC (no override set). Calls to toLocaleDateString without an explicit timeZone option were rendering dates in UTC, which meant a wrapper expiring at 23:59 EDT (= 03:59 UTC next day) showed "22 juin 2026" to the recipient instead of the intended "21 juin". All 4 date-formatting sites in lib/campaigns.js now pass timeZone: 'America/Montreal' explicitly: - worker (sendCampaignAsync) — main send path - /campaigns/:id/recipients/:i/view — web fallback render - POST /templates/:name/test-send sample defaults - POST /templates/:name/preview sample defaults Verified on prod: stored UTC "2026-06-22T03:59:59Z" now formats "21 juin 2026" / "June 21, 2026" with the timeZone option, matching the operator's intent ("expiration en fin de journée le 21 juin EDT"). Also re-patched the relance draft cmp-20260601-f857cd-rem from 2026-06-21T23:59:59Z (= 19:59 EDT, the early-evening cutoff) to 2026-06-22T03:59:59Z (= 23:59:59 EDT, true end of day). Bonus: this aligns with the original campaign's recipients which expire around ~11:57-12:04 EDT on June 21, so the reminder always works at least as long as the original — never the inverse confusion. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 --- services/targo-hub/lib/campaigns.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/services/targo-hub/lib/campaigns.js b/services/targo-hub/lib/campaigns.js index 503c7de..94a892c 100644 --- a/services/targo-hub/lib/campaigns.js +++ b/services/targo-hub/lib/campaigns.js @@ -1132,7 +1132,7 @@ async function sendCampaignAsync (id) { let expiresInDays = '' if (r.gift_expires_at) { const exp = new Date(r.gift_expires_at) - expiresAtDate = exp.toLocaleDateString(expiryLocale, { day: 'numeric', month: 'long', year: 'numeric' }) + expiresAtDate = exp.toLocaleDateString(expiryLocale, { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'America/Montreal' }) const days = Math.max(0, Math.ceil((exp - new Date()) / 86400000)) expiresInDays = String(days) } @@ -1830,7 +1830,7 @@ async function handle (req, res, method, path) { // 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' }) + .toLocaleDateString('fr-CA', { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'America/Montreal' }) const vars = { firstname: 'Louis', lastname: 'Test', @@ -1878,7 +1878,7 @@ async function handle (req, res, method, path) { const body = await parseBody(req) 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' }) + .toLocaleDateString('fr-CA', { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'America/Montreal' }) const vars = { firstname: 'Louis', lastname: 'Paul', email: 'louis@targo.ca', description: '123 Rue de Test', gift_url: 'http://gtbt.co/PREVIEW', @@ -1998,7 +1998,7 @@ async function handle (req, res, method, path) { let expiresInDays = '' if (r.gift_expires_at) { const exp = new Date(r.gift_expires_at) - expiresAtDate = exp.toLocaleDateString(expiryLocale, { day: 'numeric', month: 'long', year: 'numeric' }) + expiresAtDate = exp.toLocaleDateString(expiryLocale, { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'America/Montreal' }) const days = Math.max(0, Math.ceil((exp - new Date()) / 86400000)) expiresInDays = String(days) }