Skip to content

Commit 21c85b2

Browse files
committed
docs(integrations): address Copilot review — 8 factual corrections
Copilot review on PR #121 caught eight factual inaccuracies in the runbook (most because I wrote from inferred knowledge rather than verified code paths). Each fix below is grounded in a re-read of the relevant source file. - Integration table (Line 19): the demo/live switch lives in the integration *clients* (lib/integrations/google-maps.client.ts, lib/integrations/smtp.client.ts), not in the services (geocoding.service / route-optimizer.service / email.service). Rewrote the row "where the demo path is implemented" to point at the correct file. Also clarified that webhook signature verification on /api/webhooks/whatsapp uses HMAC SHA256 against WHATSAPP_APP_SECRET. - Health-check baseline (Line 27 + Line 298 expected JSON): n8n reports up/unreachable (not configured/unconfigured); the response has six check groups (not the five I'd implied earlier). Documented the exact shape per check group, the rules that produce the top-level status (healthy / degraded / unhealthy), and the example "expected" JSON now matches what /api/health actually returns. - Mapbox reference (Line 81): repo doesn't use Mapbox for the map fallback; it renders a static placeholder when NEXT_PUBLIC_GOOGLE_MAPS_BROWSER_KEY is unset. Reworded. - Geocode verification (Line 107): /api/route-planning/geocode persists coordinates to the Yard record but returns only { success, message }. Updated the expected response and added a follow-up GET /api/yards/:id step to confirm lat/long stored. - Template wording (Line 175 + Line 180): reminders today use whatsappService.sendTextMessage (free text from reminder.service.ts), not sendTemplateMessage. Replaced the misleading "all flows use templates" claim with a clear table: appointment confirmations need template approval today; reminders + stock-replies will need it for outside-window sends. Template body strings live in lib/services/{reminder,stock-reply}.service.ts build* functions, NOT in messages/{en,fr}.json — pointed operators at the actual source so they don't waste time grepping the i18n catalogs. - WhatsApp outbound smoke test (Line 202): the original example POSTed to /api/demo/simulate-whatsapp which is an INBOUND webhook simulator (and is blocked when DEMO_MODE=false), so it could never exercise a real send. Replaced with two safe operator paths: (a) trigger /api/reminders/check on a backdated due date, or (b) book a real test appointment through the operator UI. Both exercise sendTemplateMessage / sendTextMessage against the live Meta endpoint when DEMO_MODE=false on Production. - Stop-gate count (Line 327): said "five health-check items" but there are six (database + environment + n8n + whatsapp + smtp + googleMaps). Fixed the count and reworded the smoke-test list to reference real operator UI paths instead of imagined endpoints. No code changes; pure documentation correctness. https://claude.ai/code/session_01JBMfnqc8QXZRqCG3Tm7hHx
1 parent 896ca45 commit 21c85b2

1 file changed

Lines changed: 80 additions & 35 deletions

File tree

docs/INTEGRATIONS_SETUP.md

Lines changed: 80 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,32 @@ This document is for the operator (Richard / Freddie) wiring the production cred
1010

1111
Code is in place for these integrations on `main`:
1212

13-
| Integration | Service file | Demo path | Live path | Webhook |
14-
|---|---|---|---|---|
15-
| Google Geocoding | `lib/services/geocoding.service.ts` | `lib/demo/maps-simulator.ts` | `https://maps.googleapis.com/maps/api/geocode/json` | n/a |
16-
| Google Route Optimization | `lib/services/route-optimizer.service.ts` | local nearest-neighbour algorithm | `https://routeoptimization.googleapis.com/v1/projects/{id}:optimizeTours` | n/a |
17-
| WhatsApp Business (Cloud API) | `lib/services/whatsapp.service.ts` | `lib/demo/whatsapp-simulator.ts` | `https://graph.facebook.com/v21.0/{phone_id}/messages` | `app/api/webhooks/whatsapp/route.ts` |
18-
| n8n | `lib/integrations/n8n.client.ts` | rejects with `unconfigured` health check | `${N8N_PROTOCOL}://${N8N_HOST}:${N8N_PORT}` | bidirectional |
19-
| SMTP / email | `lib/services/email.service.ts` | console-log simulator | nodemailer over SMTPS | inbound n8n→/api/webhooks/email |
13+
| Integration | Demo / live switch | Live path | Webhook |
14+
|---|---|---|---|
15+
| Google Geocoding + Route Optimization | `lib/integrations/google-maps.client.ts` (routes to `lib/demo/maps-simulator.ts` in demo mode, calls real Geocoding / Route Optimization APIs in live mode) | `https://maps.googleapis.com/maps/api/geocode/json`, `https://routeoptimization.googleapis.com/v1/projects/{id}:optimizeTours` | n/a |
16+
| WhatsApp Business (Cloud API) | `lib/services/whatsapp.service.ts` (routes to `lib/demo/whatsapp-simulator.ts` in demo mode) | `https://graph.facebook.com/v21.0/{phone_id}/messages` | `app/api/webhooks/whatsapp/route.ts` (HMAC SHA256 signature verified against `WHATSAPP_APP_SECRET`) |
17+
| n8n | health-check pings `${N8N_HOST}` and reports `up`/`unreachable`; webhook handlers fail-closed when `N8N_API_KEY` is unset in production | `${N8N_PROTOCOL}://${N8N_HOST}:${N8N_PORT}` | bidirectional |
18+
| SMTP / email | `lib/integrations/smtp.client.ts` (routes to `lib/demo/email-simulator.ts` in demo mode) | nodemailer over SMTPS | inbound n8n → `/api/webhooks/email` |
2019

2120
The mode switch is per-integration and driven by env vars. `DEMO_MODE=true` forces the simulator across the board; setting `EQUISMILE_LIVE_MAPS=true` flips just Maps to live while everything else stays simulated (useful for Phase 1 client demos that need real route maps but no real WhatsApp sends).
2221

2322
**Verification baseline (run before starting):**
2423
```bash
2524
curl https://equismile.vercel.app/api/health | jq .
2625
```
27-
You should see `checks.database.status: "up"` and `checks.environment.missing: []`. Each integration shows `"unconfigured"` until you finish its section.
26+
27+
The response has six check groups with these shapes:
28+
29+
| Check | Status values |
30+
|---|---|
31+
| `database` | `up` / `down` (latency ms) |
32+
| `environment` | `ok` / `missing` (with a `missing[]` array) |
33+
| `n8n` | `up` / `unreachable` (with `url`) |
34+
| `whatsapp` | `configured` / `unconfigured` |
35+
| `smtp` | `configured` / `unconfigured` |
36+
| `googleMaps` | `configured` / `unconfigured` |
37+
38+
Top-level `status` is `healthy` if everything is green, `degraded` if `n8n.status: "unreachable"` (or any optional check is down), `unhealthy` if `database.status: "down"` or `environment.status: "missing"`. Before this runbook starts, expect `database: up`, `environment: ok`, the three configured-flag checks at `unconfigured`, and `n8n` likely `unreachable` (so overall `degraded`).
2839

2940
---
3041

@@ -74,7 +85,7 @@ In the Cloud Console for the project you just created:
7485

7586
### 2.4 (Optional) Browser-side key for inline maps
7687

77-
If you want the customer-detail map embed to use real tiles instead of the static Mapbox demo:
88+
If you want the customer-detail map embed to render an interactive Google map instead of the current static placeholder fallback (rendered when `NEXT_PUBLIC_GOOGLE_MAPS_BROWSER_KEY` is unset):
7889
- Repeat 2.3 to generate a second key.
7990
- This time set **Application restrictions → HTTP referrers** to `*.vercel.app/*` and your custom domain.
8091
- API restriction: tick **Maps JavaScript API** only.
@@ -98,13 +109,20 @@ After redeploy:
98109
# expect "configured" not "unconfigured"
99110
curl https://equismile.vercel.app/api/health | jq '.checks.googleMaps'
100111

101-
# real geocode round-trip — admin only:
112+
# real geocode round-trip — admin only.
113+
# Note: /api/route-planning/geocode persists the lat/long onto the
114+
# Yard record and returns { success: true, message: "Yard geocoded
115+
# successfully" } — coordinates aren't in the response. Fetch the
116+
# yard afterwards to confirm they were stored.
102117
JAR=$(mktemp); curl -c "$JAR" -X POST -d 'persona=kathelijne@equismile.demo' \
103118
https://equismile.vercel.app/api/demo/sign-in > /dev/null
104119
curl -b "$JAR" -X POST -H 'Content-Type: application/json' \
105-
-d '{"yardId":"demo-yard-montreux"}' \
120+
-d '{"yardId":"<a-real-yard-uuid>"}' \
106121
https://equismile.vercel.app/api/route-planning/geocode
107-
# expect { "latitude": ..., "longitude": ..., "source": "google", "precision": "ROOFTOP" }
122+
# expect { "data": { "success": true, "message": "Yard geocoded successfully" } }
123+
124+
curl -b "$JAR" "https://equismile.vercel.app/api/yards/<same-yard-uuid>" | jq '{ latitude, longitude }'
125+
# expect non-null lat/long values when the geocode succeeded
108126
```
109127

110128
---
@@ -169,17 +187,25 @@ The "Temporary access token" expires after 24h. For production:
169187

170188
### 3.6 Message template approvals
171189

172-
Templates are pre-defined, brand-vetted message bodies — required for any send outside the 24-hour customer service window. The repo's reminder + confirmation flows all use templates.
190+
Templates are pre-defined, brand-vetted message bodies — required by Meta for any **session-initiated** outbound send (i.e. outside the rolling 24-hour customer service window).
191+
192+
**Which flows currently need Meta-approved templates today vs. later:**
173193

174-
For each template key in `lib/demo/template-registry.ts` (9 templates: 2 appointment, 3 reminder, 4 FAQ), submit it to Meta:
194+
| Flow | Send method (today) | Needs Meta template approval? |
195+
|---|---|---|
196+
| Appointment confirmations | `whatsappService.sendTemplateMessage('appointment_confirmation_v1', …)` | **Yes** — must be approved before live sends |
197+
| Stock-reply / FAQ replies (`StockReplyModal`) | `sendTemplateMessage(<faq_*>)` within the 24h window after an inbound enquiry | Optional — works as free-text in the customer-care window, but template-approved versions unlock outside-window sends |
198+
| Dental / vaccination / overdue-invoice reminders | `whatsappService.sendTextMessage(...)` (free text from `lib/services/reminder.service.ts`) | **Not yet today** — but reminders typically fire outside the 24h window, so getting templates approved unblocks reliable production sending |
199+
200+
For the templates that ARE referenced as registered keys in `lib/demo/template-registry.ts` (9 keys: 2 appointment, 3 reminder, 4 FAQ), submit each to Meta:
175201

176202
1. Meta WhatsApp panel → **Message Templates → Create Template**.
177-
2. Category: `Utility` for all reminders + FAQs; `Marketing` *not* applicable to EquiSmile.
178-
3. Name: must match the registry exactly — e.g. `appointment_reminder_v1`. Lower-case, snake_case, version suffix.
179-
4. Languages: `English (en)` and `French (fr)` — pull the body strings from `messages/en.json` + `messages/fr.json` under the matching key. Replace operator-side variables (e.g. `{{customer_name}}`) with placeholders Meta accepts (`{{1}}`, `{{2}}` etc.) — preserve the order.
203+
2. Category: `Utility` for all reminders + FAQs; `Marketing` not applicable to EquiSmile.
204+
3. Name: must match the registry key exactly — e.g. `appointment_reminder_v1`. Lower-case, snake_case, version suffix.
205+
4. Languages: `English (en)` and `French (fr)`. The canonical body strings are **assembled in code**, not in `messages/{en,fr}.json`. For each template, copy the body text from its corresponding `build…` function in `lib/services/reminder.service.ts` (e.g. `buildDentalDueReminder`, `buildVaccinationDueReminder`, `buildOverdueInvoiceReminder`) and `lib/services/stock-reply.service.ts` for the FAQ bodies. Replace function parameters (e.g. `customer_name`) with positional Meta placeholders (`{{1}}`, `{{2}}`) — preserve the order shown in the function's parameter list.
180206
5. Submit. Approval is automated for utility messages and usually clears in <60 seconds; if rejected for "policy violation" it's almost always a missing footer or unclear `{{n}}` mapping.
181207

182-
> Once a template is approved, set `WHATSAPP_CONFIRMATION_TEMPLATE` and `WHATSAPP_REMINDER_TEMPLATE` env vars to the registry key (default values are already correct: `appointment_confirmation_v1`, `appointment_reminder_v1`).
208+
> Once approved, the existing env-var defaults are already correct: `WHATSAPP_CONFIRMATION_TEMPLATE=appointment_confirmation_v1`, `WHATSAPP_REMINDER_TEMPLATE=appointment_reminder_v1`. Override these only if you've registered renamed template versions in Meta.
183209
184210
### 3.7 Verify
185211

@@ -192,14 +218,31 @@ curl https://equismile.vercel.app/api/health | jq '.checks.whatsapp' # expect
192218
# inbound webhook reachability (Meta-side test)
193219
# in Meta App dashboard → Webhooks → click "Test" → message → expect 200
194220

195-
# real send (admin only) — uses the test number's allowlisted recipient:
196-
JAR=$(mktemp); curl -c "$JAR" -X POST -d 'persona=kathelijne@equismile.demo' \
197-
https://equismile.vercel.app/api/demo/sign-in > /dev/null
198-
curl -b "$JAR" -X POST -H 'Content-Type: application/json' \
199-
-d '{"to":"+41XXXXXXXXX","template":"appointment_reminder_v1","params":["Test","2026-05-15","09:00","Yard A"]}' \
200-
https://equismile.vercel.app/api/demo/simulate-whatsapp
201-
# in DEMO_MODE=true this still routes to the simulator;
202-
# set DEMO_MODE=false on Vercel Production to fire the real send.
221+
# Real outbound smoke test — there isn't a dedicated "send arbitrary
222+
# template now" API in this repo (and /api/demo/simulate-whatsapp
223+
# is an INBOUND webhook simulator, not an outbound sender; it's
224+
# also blocked when DEMO_MODE=false).
225+
#
226+
# Two safe ways to exercise the live outbound path:
227+
# (a) Trigger a routine reminder sweep from the existing endpoint
228+
# (admin only). Pick up the recipient on the allowlisted test
229+
# number, set their preferredChannel=WHATSAPP, and either
230+
# backdate a horse's dentalDueDate to today (test data only)
231+
# or wait for the next cron tick. Then:
232+
#
233+
# curl -b "$JAR" -X POST https://equismile.vercel.app/api/reminders/check
234+
#
235+
# Expect 200 with a count of dispatched reminders. Confirm
236+
# receipt on the test phone.
237+
#
238+
# (b) Approve and book a real test appointment from the operator
239+
# UI (/en/route-runs/[id] → Approve → Book). The booking flow
240+
# calls sendTemplateMessage with appointment_confirmation_v1,
241+
# which must already be Meta-approved.
242+
#
243+
# In both paths, set DEMO_MODE=false on Vercel Production first.
244+
# Otherwise sends route to lib/demo/whatsapp-simulator.ts (visible in
245+
# /api/demo/whatsapp-log) instead of Meta.
203246
```
204247

205248
---
@@ -282,22 +325,24 @@ If you instead prefer the Vercel-side path: unset `EQUISMILE_FORCE_DEMO_SEED` on
282325
curl https://equismile.vercel.app/api/health | jq .
283326
```
284327

285-
Expected:
328+
Expected (note that `n8n` reports `up`/`unreachable`, not configured/unconfigured):
286329

287330
```json
288331
{
289332
"status": "healthy",
290333
"checks": {
291-
"database": { "status": "up" },
334+
"database": { "status": "up", "latency_ms": 4 },
292335
"environment": { "status": "ok", "missing": [] },
293-
"googleMaps": { "status": "configured" },
336+
"n8n": { "status": "up", "url": "https://n8n.equismile.ch" },
294337
"whatsapp": { "status": "configured" },
295338
"smtp": { "status": "configured" },
296-
"n8n": { "status": "configured" }
339+
"googleMaps": { "status": "configured" }
297340
}
298341
}
299342
```
300343

344+
If `n8n.status: "unreachable"` (e.g. you've intentionally not deployed n8n yet), top-level `status` will be `"degraded"` while the rest is configured — that's an acceptable production state if your operator workflows don't yet depend on n8n.
345+
301346
---
302347

303348
## 9. Cost expectations (rough first-month estimate)
@@ -318,11 +363,11 @@ Total: **<CHF 90/month** at single-practice operating volume.
318363
## 10. Operator stop-gate
319364

320365
After completing Sections 1–7:
321-
- All five health-check items report `configured` or `up`.
322-
- Send one real WhatsApp template message to a test number — receipt confirmed on phone.
323-
- Run a real geocode for a Swiss yard — coordinates persisted to DB.
324-
- Trigger one real route optimisation against three live yards — proposal saved.
325-
- Confirm the inbound WhatsApp webhook receives a real test message and creates an Enquiry.
366+
- `/api/health` shows `database: up`, `environment: ok`, `whatsapp / smtp / googleMaps: configured`, and `n8n: up` (or `unreachable` if you're deferring n8n) — six checks total.
367+
- Confirm one real WhatsApp template send: book a real test appointment from `/en/route-runs/[id]` → Approve → Book; the booking flow fires `appointment_confirmation_v1` and the test phone receives the message.
368+
- Confirm one real geocode: from `/en/yards/[id]` admin action, geocode a Swiss yard; refresh and confirm `latitude` + `longitude` are populated on the yard record.
369+
- Confirm one real route optimisation: from `/en/planning`, generate routes against three real geocoded yards; route-run is saved with the optimised stop order.
370+
- Confirm the inbound WhatsApp webhook: from a Meta-allowlisted phone, send a real WhatsApp message to your business number; observe a new Enquiry appear in `/en/enquiries`.
326371

327372
If all five smoke tests pass, integrations are production-ready. Mark UAT-INT-01..04 + UAT-PLN-02 verdicts as PASS in `docs/UAT_v2_VALIDATION.md` next pass.
328373

0 commit comments

Comments
 (0)