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.
229 lines
8.6 KiB
Markdown
229 lines
8.6 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
|
|
|
|
## 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 Gigafibre" \
|
|
--amount "50 $" \
|
|
--expiry "31 décembre 2026" \
|
|
--from "Gigafibre <noreply@gigafibre.ca>" \
|
|
--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 Gigafibre" \
|
|
--amount "50 $" \
|
|
--expiry "31 décembre 2026" \
|
|
--from "Gigafibre <noreply@gigafibre.ca>" \
|
|
--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"`) |
|
|
|
|
The template uses a vintage `{{#expiry}} ... {{/expiry}}` block for the
|
|
optional expiry line — currently rendered as plain text (the script's
|
|
simple `{{var}}` renderer doesn't strip the tags). If you don't want the
|
|
expiry sentence, edit the template directly to remove that block.
|
|
|
|
## 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.
|