Commit Graph

7 Commits

Author SHA1 Message Date
louispaulb
d694d889a1 feat(campaigns/templates): replace placehold.co with real Mailjet logos for rows 2-3
User pasted the full HTML block from their Mailjet Passport editor —
extracted the 8 missing CDN URLs for the merchant grid bottom rows and
swapped them into both FR and EN templates.

Final 12-logo grid is now 100% real Mailjet-hosted assets matching the
user's brand-approved visuals (no more placehold.co rectangles):

  Row 1: Amazon, IGA, Tim Hortons, $1 Plus           (already real)
  Row 2: Pizza Pizza, Home Depot, Best Buy, Walmart  (NEW)
  Row 3: Petro-Canada, Esso, Home Hardware, Sobeys   (NEW)

URL pattern: https://xqy3m.mjt.lu/img2/xqy3m/<UUID>/content
Width normalized to 95px (consistent with row 1) instead of the source
template's 300px since our 600px-wide email card means each 25% column
is ~140px effective — 95px image fits with proper margins.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 21:36:51 -04:00
louispaulb
d6096fe1f8 feat(campaigns): apply real TARGO brand + auto-route FR/EN by Customer.language
Brand audit against the official guide (Feb 2026 v1.0) caught several
inconsistencies in the email template:

- Wrong primary green: was #019547, should be #00C853 (Targo Green from
  brand palette). Globally replaced.
- Wrong gradient: was #019547→#06a04d, should be 135deg #00C853→#005026
  (the official Gradient Targo from the brand). Now using Outlook-safe
  background-image + bgcolor fallback for solid green on Outlook desktop.
- Wrong contact info: facturation@targointernet.com / 514 242-1500 →
  support@targo.ca / 514 448-0773 / 1 855 888-2746 (per §11 of guide).
- Wrong website: targointernet.com + gigafibre.ca → www.targo.ca.
- Missing slogan + green dot: footer now ends with the trademark
  tagline "Services de confiance, tout-en-un, près de chez vous." with
  the obligatory green period (always FR — it's the trademark, not a
  marketing line, so stays untranslated in EN template too).
- Missing brand fonts: added Space Grotesk (display) + Plus Jakarta
  Sans (body) via Google Fonts. Wrapped in MSO conditional comments so
  Outlook desktop skips the request and falls back to Helvetica via
  the explicit font-family stack on every element.
- Wrong body bg / text colors: now #F5FAF7 (Muted) / #1B2E24
  (Foreground) per brand semantic palette.
- Wrong info-pill bg: was #f3f4f3 → #F5FAF7 (Muted).
- Added official dark footer band #1C1E26 (Targo Dark) with white
  inverted wordmark, slogan, address, copyright.

Multilang routing (FR/EN):

- lib/campaigns.js matchCustomer now fetches Customer.language
  (14k FR / 1k EN distribution confirmed on prod). Default 'fr' for
  unmatched contacts.
- New templateForLanguage(lang) helper picks gift-email-<lang>.html,
  falls back to FR. Resolves 'fr-CA' → 'fr' etc.
- sendCampaignAsync pre-loads templates per recipient with an in-memory
  cache to avoid re-reading from disk on every send.
- gift-email-en.html created — English translation of the full FR
  template, keeping the slogan in French (it's the trademark tagline).
- year variable now injected (replaces hardcoded © year).

UI (CampaignNewPage):

- New "Langue" column in the Step 2 recipient table. Shows a clickable
  chip (FR primary green / EN blue-grey) that toggles language inline,
  so a campaign manager can override the ERPNext-resolved language
  per recipient.
- Step 3 recap now shows "Répartition par langue: 145 × FR, 12 × EN"
  before confirming the send.

Spell-check:

- TemplateEditorPage HTML mode now has spellcheck="true" + dynamic
  lang attribute on the textarea, picked from the template name suffix
  (gift-email-fr → fr, gift-email-en → en). Browser's native dictionary
  flags typos in real time. AI-grade rewrites deferred to the future
  /campaigns/ai/rewrite endpoint discussed previously.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 20:50:56 -04:00
louispaulb
9f2b37939d feat(campaigns): TARGO rebrand + Mustache sections + Mailjet webhook setup
- Template gift-email-fr.html: switch from Gigafibre indigo to TARGO green
  (#019547), use real Mailjet-hosted TARGO logo, adopt retention-offer
  layout from the latest mockup (tutoiement, Option 1/Option 2 split,
  prorata-refund disclaimer, "L'équipe TARGO" signature). Row 1 of the
  merchant grid uses real Mailjet logos (Amazon, IGA, Tim Hortons, $1
  Plus); rows 2-3 are placehold.co until URLs are shared.

- send_gift_campaign.js: add {{#var}}...{{/var}} Mustache section support
  to the renderer so the optional expiry block disappears cleanly when
  --expiry is omitted (was rendering literal tags before). Add new
  --commitment-months CLI flag (default 3) for the "Rester encore X mois
  ou +" wording.

- setup_mailjet_webhook.js (new): one-shot Node script to register the
  Hub callback URL with Mailjet's /v3/REST/eventcallbackurl. Defaults
  to a safe event subset (open/click/spam/unsub) that doesn't conflict
  with the WP-Mail-SMTP integration already owning sent/bounce/blocked.
  --all forces full takeover with a conflict guard requiring
  --force-takeover to overwrite existing records. Supports --list and
  --delete for inspection / rollback.

- package.json (new): nodemailer dependency for SMTP send.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-21 19:07:20 -04:00
louispaulb
9b06e2df30 fix(docs/campaigns): support@targointernet.com is the validated sender, not support@targo.ca
Previous commit (380f3bc) incorrectly claimed Mailjet verified targo.ca
at the domain level. It doesn't — Mailjet validates senders ONE BY ONE,
even when SPF/DKIM/DMARC are correctly published at the domain. The
mistake: SMTP returned `250 OK` on a send-test from support@targo.ca,
but the message was silently dropped on Mailjet's side because that
specific mailbox hadn't been approved.

Validated senders on this Mailjet account:
  ✓ noreply@targo.ca           — hub transactional (invoices etc.)
  ✓ support@targointernet.com  — gift campaigns

`support@targo.ca` is NOT validated, despite being on a domain whose
sibling (`noreply@targo.ca`) is.

Updated:
  - README default --from value
  - The "sender" section now explains per-sender validation (not
    domain-level) and the SMTP-250-but-not-delivered gotcha
  - Listed both validated senders explicitly with usage intent

The script itself (send_gift_campaign.js) was already
sender-agnostic — only README guidance changed. New senders are added
in Mailjet console → Sender Domain Verification → Add sender, with
the verification link mailed to the new address.
2026-05-21 16:42:18 -04:00
louispaulb
380f3bc0e7 docs(campaigns): document support@targo.ca as the default gift-campaign sender
Validated live with Mailjet: targo.ca is verified at the DOMAIN level
(SPF + DKIM + DMARC published in Cloudflare), so any *@targo.ca sender
works without per-mailbox approval. Tested 1 send from
support@targo.ca → accepted, delivered.

Why support@ rather than noreply@ for campaigns:
  - Campaigns INVITE a reply (questions about the gift, "I didn't get
    mine", "the link doesn't work", etc.)
  - noreply@ is for transactional system mail where there's nothing
    useful for a human to reply to
  - Different intent → different sender

The hub's transactional emails (invoices, magic links) continue to
use noreply@targo.ca; campaigns specifically use support@targo.ca.
README updated accordingly with the rationale.

Note for future: if we ever want a @gigafibre.ca sender, that's
~30 min of Mailjet setup (add domain, publish SPF/DKIM CNAMEs in
Cloudflare). Not done today because all customer-facing email
flows through targo.ca and support@ is the right mailbox for this
campaign intent.
2026-05-21 16:36:06 -04:00
louispaulb
e1283f30e8 feat(campaigns): add Giftbit API client + validate end-to-end with sandbox
Adds create_giftbit_campaign.js — Node CLI that POSTs to the Giftbit
API (testbed or production), creates a campaign with
delivery_type=SHORTLINK so Giftbit does NOT send their own English
template emails, polls /gifts?campaign_uuid=... until the redemption
shortlinks are generated, then writes a gifts CSV ready to feed into
send_gift_campaign.js.

Two non-obvious things learned while wiring it up:

1. The right endpoint to get the shortlinks is /gifts (not /links).
   /links/{uuid} returned 0 rows on our sandbox account; /gifts has
   a `shortlink` field on each gift once delivery_status transitions
   from QUEUED → LINKCREATED. Polled with 2s interval, up to 20 tries.

2. delivery_type=SHORTLINK is mandatory. Default is GIFTBIT_EMAIL,
   which fires their English template immediately — defeating the
   whole point of bridging through our French Mailjet template.
   Confirmed in the campaign GET response that delivery_type echoes
   back correctly when we send "SHORTLINK".

Validated end-to-end (entirely synthetic data — Alice/Bob/Charlie at
@example.com, no real customer info in the sandbox):
  ✓ Auth probe via /ping returns 200
  ✓ POST /campaign returns campaign UUID
  ✓ After ~12s, /gifts returns 3 gifts each with a working shortlink
  ✓ send_gift_campaign.js consumes the gifts CSV + the contacts CSV
  ✓ FR template renders: "Bonjour Alice", http://gtbt.co/7TKGFDBNVZq
    embedded in the CTA button href, address in the footer line

The --sandbox flag does double duty: routes the API to
api-testbed.giftbit.com AND replaces every recipient email with
louis@targo.ca so we can't accidentally hit real customer inboxes
with the non-redeemable test gifts.

README updated with the two-stage pipeline (create → send), explicit
warnings about the customer-matching gap (only 25% of source rows
resolve via legacy_delivery_id — the rest use a different ID space
from the source Map tool), and the sandbox-quirk where Giftbit
collapses recipient_name when emails are duplicated.

Token NOT committed — pulled from GIFTBIT_TOKEN env var per the
script's contract. In production we'll store it in the hub's
.env alongside SMTP_USER / SMTP_PASS.
2026-05-21 16:20:28 -04:00
louispaulb
37896421c3 feat(campaigns): MVP gift campaign sender (Node CLI + FR email template)
User context: needs to send Giftbit gift cards to 203 customers with a
branded French email instead of Giftbit's English-only default delivery.
Giftbit's own UI/API can issue the gifts but its email is English; this
MVP bridges the gap by taking the gift URLs back from Giftbit, pairing
them with our contact CSV, and sending personalized FR emails through
the Mailjet SMTP that's already wired up for ERPNext invoice mail.

Three files in scripts/campaigns/:

1. send_gift_campaign.js — Node CLI. Two CSV inputs (gifts + contacts),
   matches by row order (default) or email key, renders the HTML
   template with mustache-style {{firstname}} / {{gift_url}} / etc.,
   sends via nodemailer with configurable SMTP + throttle.
   --dry-run writes per-recipient previews to disk for visual review
   before flipping to live mode. Results CSV with per-row status
   (sent / failed / dry-run) + error message + timestamp is written
   next to the script for follow-up on failures.

2. templates/gift-email-fr.html — branded French email. Table-based
   layout (the only thing that renders consistently in Gmail / Outlook /
   iOS Mail / Apple Mail / Bell Sympatico). Indigo gradient header,
   centered CTA button, contextual {{description}} line citing the
   service address, support contact in the footer, no inline images
   (defers to text + colour blocks to dodge image-blocking).

3. contacts_from_legacy.py — replaces the ad-hoc /tmp Python I ran
   earlier with a proper repo'd version. Same multi-email handling
   options (first / split / skip) as I offered the user; defaults to
   "first" = 1 gift per household, which is what they chose. Title-
   cases the address with French article rules (de / du / la / aux
   stay lowercase, 1re / 2e ordinals stay lowercase too).

4. README.md — end-to-end usage with the actual SMTP env vars from
   /opt/targo-hub/.env and the matching strategy decision matrix.

Validated end-to-end with a 5-row dry run: matching works, accents
preserved (Amélie, Geneviève, Marc-André), {{firstname}} interpolates,
gift URLs land in the rendered button href, address shows in the
contextual footer line. Previews written to disk for visual QA.

NOT in this MVP (out of scope, can come next if we end up running
gift campaigns regularly):
  - No persistence to ERPNext doctype (no Gift Campaign / Recipient
    records — pure CLI, results CSV is the audit trail)
  - No click-tracking redirect (the gift_url goes verbatim to the
    recipient; Giftbit's own API/dashboard reports redemption status,
    which is the more relevant signal than "clicked the link")
  - No ops UI page (CLI is fine for one-shot; if this becomes regular
    we wrap it in services/targo-hub/lib/gift-campaign.js + a Vue page)
2026-05-21 15:51:01 -04:00