fix(whatsapp,roles): self-heal legacy whatsapp_log schema; map underscore role aliases

CT115 (Mumuni relay #747) — two live-instance defects after PR #12:

1. GET /api/whatsapp/mock-log 500'd with a valid admin token:
   'no such column: whatsapp_log.message_text'. The model gained
   message_text/wa_message_id/ticket_number (and dropped command) in
   4afdc36 with no migration, so legacy DBs keep the (command, ...) shape.
   ensure_legacy_schema (startup, app/main.py) now adds the three missing
   columns idempotently and backfills legacy command bodies into
   message_text before dropping the obsolete NOT NULL command column, so
   both the mock-log read path and the ORM write path work on healed DBs.
   New producer/consumer regression (tests/test_whatsapp_log_legacy_heal.py)
   reproduces the exact OperationalError, then asserts 200 + data.

2. ROLE_ALIASES gap: underscore legacy roles (cs_rep, cs_manager,
   fm_dispatcher) were not mapped, so normalize_legacy_user_roles could not
   converge rows like user 18 (test@denya.com, role 'cs_rep') and the
   frontend stranded them on /tickets. Added the underscore aliases; tests
   assert normalize_role('cs_rep') == 'CS Rep' and a cs_rep row converges
   and authenticates.
This commit is contained in:
root
2026-09-09 12:52:07 +00:00
parent 7b0365b135
commit 4e8b96ed0a
5 changed files with 238 additions and 3 deletions
+10 -1
View File
@@ -66,6 +66,13 @@ sign-up UI; users are created/managed by admins only (P0 hardening batch).
| POST | `/api/whatsapp/webhook` | `X-Webhook-Secret` header | Inbound message → ticket + log. Fail-closed: 403 when `WHATSAPP_WEBHOOK_SECRET` is unset or the header doesn't match |
| GET | `/api/whatsapp/mock-log` | Bearer | Recent webhook submissions (debug; auth required) |
`whatsapp_log` predates the real Meta webhook (the model gained
`message_text`/`wa_message_id`/`ticket_number` and dropped `command` without a
migration), so legacy tables keep the old `(command, …)` shape and every ORM
read/write 500s. `ensure_legacy_schema` (app/main.py) adds the missing columns
and backfills+drops the obsolete NOT NULL `command` column idempotently at
startup — do not hand-edit legacy DBs, ship a self-heal there instead.
### Pages (Sprint 3) — Jinja2 templates at `app/templates/`
| Method | Path | Auth | Description |
|--------|------|------|-------------|
@@ -123,7 +130,9 @@ Frontend: Alpine.js + Tailwind CSS vendored same-origin (no CDN) — see
old open register) fail closed at login/JWT and can never be recreated via the
API (422). Startup self-heals (lifespan in `app/main.py`, helpers in
`app/services/seed.py`) converge legacy rows: `normalize_legacy_user_roles`
maps unambiguous alias nicknames onto canonical roles, and
maps unambiguous alias nicknames onto canonical roles (space and underscore
forms alike — `cs rep`/`cs_rep` → `CS Rep`, `fm dispatcher`/`fm_dispatcher`,
`cs manager`/`cs_manager`), and
`normalize_legacy_user_emails` lowercases stored emails — login and the admin
create-user duplicate check both compare on the lowercased form, so pre-P0
mixed-case emails are never silently locked out.