gigafibre-fsm/scripts/campaigns/README.md
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

269 lines
10 KiB
Markdown

# Gift Campaign — Personalized French email sender
One-shot tool to send Giftbit gift cards to a list of contacts with a branded
French email, bypassing Giftbit's English-only built-in delivery.
## How it works (two-stage pipeline)
The campaign is split into two scripts you run in sequence:
```
contacts_from_legacy.py # (one-time) extract clean contacts from legacy CSV
contacts.csv
create_giftbit_campaign.js # POST to Giftbit API → SHORTLINK gifts back
gifts.csv + contacts.csv
send_gift_campaign.js # personalized FR emails via Mailjet
results-<timestamp>.csv # per-row status for follow-up
```
**Critical**: the create script passes `delivery_type=SHORTLINK` to Giftbit
so they generate the redemption links but DO NOT send their own English
emails. We then deliver French personalized mail through Mailjet, the
same SMTP wired up for ERPNext invoices.
The Giftbit redemption landing page (where the recipient picks a brand)
is controlled by Giftbit — when creating the campaign through their
dashboard for the first time, set the language to `fr-CA` so the page
shows in French. The API exposes a `language` field too but it wasn't
fully exposed on our sandbox account; verify with the campaign you
created in the Giftbit dashboard.
## Setup
```bash
cd scripts/campaigns
npm init -y # one-time, creates package.json
npm install nodemailer # only dependency (create_giftbit_campaign.js
# uses Node built-ins, no http library needed)
```
## Stage 1 — create the Giftbit campaign
```bash
# Sandbox test (all recipients are rerouted to louis@targo.ca for safety):
export GIFTBIT_TOKEN="<your testbed token>"
node create_giftbit_campaign.js \
--contacts ./test-contacts.csv \
--amount-cents 5000 \
--brand-codes amazonca,timhortonsca,walmart \
--expiry 2026-12-31 \
--subject "Cadeau Gigafibre" \
--message "Merci d'être client" \
--sandbox \
--id "test-q4-2026"
# Production (real recipient emails, real gifts charged from your balance):
export GIFTBIT_TOKEN="<your prod token>"
node create_giftbit_campaign.js \
--contacts ./contacts.csv \
--amount-cents 5000 \
--brand-codes amazonca,timhortonsca,walmart \
--expiry 2026-12-31 \
--subject "Cadeau Gigafibre" \
--message "Merci d'être client" \
--id "q4-2026-loyalty"
```
Output: `giftbit-gifts-<id>.csv` with columns:
```
firstname,lastname,email,gift_url,giftbit_uuid,gift_value_cents,internal_id
Alice,Tremblay,louis@targo.ca,http://gtbt.co/7TKGFDBNVZq,bdb28566...,500,TEST-001
```
`internal_id` is your contact's `account_id` column passed through to
join the response back to ERPNext customer records.
The `--sandbox` flag does TWO things:
- Points the API at `api-testbed.giftbit.com` instead of `api.giftbit.com`
- Replaces every recipient email with `louis@targo.ca` as a safety net
so the test gifts (non-redeemable in sandbox) don't actually land in
any real customer inbox
### `--from` sender
Mailjet validates senders **individually**, not at the domain level —
each mailbox (`noreply@`, `support@`, etc.) must be approved separately
in the Mailjet console even when SPF/DKIM/DMARC are published at the
domain level. The two known-validated senders on this account are:
| Sender | Used by | Status |
|---|---|---|
| `noreply@targo.ca` | hub transactional (invoices, magic links) | ✓ validated |
| `support@targointernet.com` | gift campaigns | ✓ validated |
The default for gift campaigns:
```
--from "TARGO <support@targointernet.com>"
```
Reasoning for `support@` over `noreply@`: campaigns INVITE a reply
(questions about the gift, "I didn't get mine", "the link doesn't
work"). `noreply@` is for transactional system mail where there's
nothing useful for a human to reply to. Different intent → different
sender.
**Common gotcha**: SMTP returns `250 OK` even when Mailjet later refuses
to deliver because the sender isn't validated. So the script will say
"sent" but the recipient never sees the message. Always verify a single
test arrives in inbox before doing a bulk send with a new sender. We
hit this with `support@targo.ca` (caught after the fact, fixed by
switching to `support@targointernet.com`).
If you need a new sender (e.g. `recompenses@gigafibre.ca`), add it in
the Mailjet console → Sender Domain Verification → Add a sender, then
click the verification link mailed to that address. Per-sender approval
takes minutes once you control the inbox.
## Stage 2 — send the personalized French emails
## Dry run (no emails sent, HTML written for preview)
```bash
node send_gift_campaign.js \
--gifts /path/to/giftbit-gifts.csv \
--contacts /path/to/giftbit-contacts-A-first-email.csv \
--template ./templates/gift-email-fr.html \
--subject "🎁 Un cadeau pour vous, de la part de TARGO" \
--amount "60 $" \
--expiry "31 décembre 2026" \
--commitment-months 3 \
--from "TARGO <support@targointernet.com>" \
--dry-run
```
A `preview-YYYY-MM-DD-HH-MM/` directory will be created with one HTML
file per recipient (numbered + email-suffixed). Open a few in a browser
to validate the rendering on real data, then drop the `--dry-run` flag
to actually send.
## Live send
```bash
# Pull SMTP creds from the hub env (same Mailjet account as ERPNext)
source <(ssh root@96.125.196.67 'grep -E "^SMTP_" /opt/targo-hub/.env' | sed 's/^/export /')
node send_gift_campaign.js \
--gifts /path/to/giftbit-gifts.csv \
--contacts /path/to/giftbit-contacts-A-first-email.csv \
--template ./templates/gift-email-fr.html \
--subject "🎁 Un cadeau pour vous, de la part de TARGO" \
--amount "60 $" \
--expiry "31 décembre 2026" \
--commitment-months 3 \
--from "TARGO <support@targointernet.com>" \
--smtp-host in-v3.mailjet.com --smtp-port 587 \
--smtp-user "$SMTP_USER" --smtp-pass "$SMTP_PASS" \
--throttle-ms 600
```
`--throttle-ms 600` = roughly 100 emails/minute, safely below the Mailjet
free-plan ceiling of ~120/min. Adjust upward to 250 ms if you're on a
paid Mailjet plan.
## Matching strategies
| `--match-by` | Behaviour |
| --- | --- |
| `row` (default) | Line N of the gifts CSV pairs with line N of the contacts CSV. Use when Giftbit issued the gifts in the same order as your contacts. |
| `email` | Join by `email` column present in both CSVs. Use when Giftbit included emails in their export (more robust to ordering mistakes). |
## Template variables
The HTML template at `templates/gift-email-fr.html` uses `{{var}}` syntax.
Variables resolved at send time:
| Variable | Source |
| --- | --- |
| `{{firstname}}` | contacts CSV `firstname` column (falls back to "cher client") |
| `{{lastname}}` | contacts CSV `lastname` |
| `{{email}}` | contacts CSV `email` |
| `{{description}}` | contacts CSV `description` (we put the service address there) |
| `{{gift_url}}` | matched from the gifts CSV |
| `{{amount}}` | `--amount` CLI flag (e.g. `"50 $"`) |
| `{{expiry}}` | `--expiry` CLI flag (e.g. `"31 décembre 2026"`) |
| `{{commitment_months}}` | `--commitment-months` CLI flag (default `3`) — used in the "Condition" pill and prorata-refund disclaimer of the retention offer |
The template uses a Mustache-style `{{#expiry}} ... {{/expiry}}` block for
the optional expiry line. The renderer keeps the contents when the
matching variable is truthy and drops them entirely otherwise — so if you
omit `--expiry` from the CLI, the "Le lien expire le …" sentence
disappears cleanly with no orphan tags showing.
## Source data — the two CSVs
### Contacts (what we send to)
Generated from a service-address selection by `scripts/campaigns/contacts_from_legacy.py`
(or by hand). One row per recipient:
```csv
firstname,lastname,email,description
Marc-André,Boileau,boileau.marcandre@gmail.com,15 Rue des Hirondelles
Maryse,Roy,roy.maryse@hotmail.com,32 Rue des Hirondelles
```
### Gifts (output from Giftbit)
Whatever shape Giftbit gives you. The script auto-detects the URL column
from the common naming conventions. Typically:
```csv
gift_id,gift_url,amount
gb_abc123,https://app.giftbit.com/g/x7K2N9...,5000
gb_def456,https://app.giftbit.com/g/p2H8M4...,5000
```
## After sending
Check `results-<timestamp>.csv`:
- `status=sent` rows landed in Mailjet's outbound queue (delivery to the
recipient's mailbox is not guaranteed — see Mailjet console for bounces).
- `status=failed` rows have the SMTP error in the `error` column. Common
causes: malformed email address, hard bounce from a stale legacy email.
- Re-run only the failed rows by filtering the results CSV and feeding it
back through the script.
## What's NOT in this script (intentional MVP scope)
- No persistence to ERPNext doctype (no `Gift Campaign` records created)
- No click tracking — the `gift_url` is included verbatim. Giftbit gives
you redemption status via their own API/dashboard.
- No ops UI — pure CLI. If we end up running gift campaigns regularly,
wrap this in a `services/targo-hub/lib/gift-campaign.js` endpoint and
add a page in ops. For now, one-shot CLI is sufficient.
## Known issues to resolve before production
1. **Customer matching from the source CSV is only 25%** — the `id
emplacement` column in `selectionAdressesMap*.csv` is NOT a
`legacy_delivery_id`. Of 216 source rows, only 54 resolve to a
Service Location via that column. The other 162 use a different ID
space (50000+ range, while migrated SLs are 1-17307). Before going
to production, we need to either:
- Match by address (street + civic + postal_code) to find the
correct Service Location → Customer
- Or have the Map tool include the actual Service Location `name`
(`LOC-XXXXX`) in its export
The current `account_id` column in our contacts CSV is approximated;
for accurate Customer audit-trail we need this fixed.
2. **The Giftbit testbed token** in our hub `.env` is sandbox-only.
Production access requires Giftbit-side KYC + account funding +
API approval. While waiting, all testing happens with the testbed
token and the `--sandbox` flag — gift URLs work in their test
webapp but represent no real money.
3. **`recipient_name` collapses when emails are duplicated.** In
sandbox we send all 3 test gifts to `louis@targo.ca`, and Giftbit's
API returns the same `recipient_name` for all of them (apparently
they dedup by email). In production with distinct emails per
contact, each gift has the right name. This is a sandbox-only
quirk, not a script bug.