diff --git a/docs/proposals/loyalty-go-live-readiness.md b/docs/proposals/loyalty-go-live-readiness.md index 7228c702..ae2cbef5 100644 --- a/docs/proposals/loyalty-go-live-readiness.md +++ b/docs/proposals/loyalty-go-live-readiness.md @@ -70,6 +70,67 @@ Session paused 2026-05-16 evening. To resume Test 1 round 2: 2. Confirm `/admin/loyalty/programs` shows the Fashion Group program (should be seeded by `seed_demo.py`). 3. Tail `api` and `celery-worker` logs live, then enroll at `https://fashionhub.rewardflow.lu/loyalty/join` with a fresh email. The point of the live tail is to catch where B1-F actually dies — at dispatch, at SMTP, or somewhere else. +## 2026-05-17 update — B1-F resolved (chain of 4 nested bugs) + +End-to-end enrollment → Celery dispatch → email_logs `status=sent` → real +emails arriving in inbox. Verified with the FR locale: enrollment +("Bienvenue chez Fashion Group S.A. Loyalty !") and welcome-bonus +("Vous avez gagné 50 points bonus !") both send within ~4s of submit. + +The "no welcome email" symptom hid four layered bugs; each silently +masked the next, which is why early diagnostics looked clean: + +| # | Bug | Fix | +|---|---|---| +| 1 | `@shared_task` defaulted to `amqp://localhost//` because `celery_app.set_default()` was never called AND the api process never imported `celery_config`. `.delay()` raised `kombu.OperationalError: Connection refused`. | `44c42909` — `set_default()` + early import in `main.py` (with `# isort: split` so ruff doesn't reorder it). | +| 2 | `on_failure` log handler crashed on reserved `LogRecord` attribute name `args` → `KeyError` masked every real task exception. | `3e650ff8` — rename to `task_args` / `task_kwargs`. | +| 3 | `loyalty.send_notification_email` wasn't in worker's task registry — `notifications.py` wasn't imported by `loyalty/tasks/__init__.py`. Worker received the message, couldn't find the task, ACKed silently. | `2a216101` — add the import + `__all__` entry. | +| 4 | Celery worker process never imported all models. First DB query failed `InvalidRequestError: expression 'ContentPage' failed to locate a name`. | `5b21908b` — `_preload_all_module_models()` walks the registry and force-imports each module's `models` package at celery_config load. | + +Three earlier same-session commits also shipped: SMTP password eye toggle +(`64a178f4`), JS error on `/admin/loyalty/programs` (`8d6830fc`), 422 on +ProgramCreate (`120532e6`). + +### Audit finding + +`app/modules/prospecting/tasks/__init__.py` has the same shape as bug #3 +above — `scan_tasks.py` exists but isn't imported. Not blocking anything +today (no prospecting Celery dispatch is wired up yet), but should be +fixed alongside the unit-test pass below. + +### Follow-ups (queued for next session) + +1. **Two Test 1 nits** — date format mm/dd/yyyy on FR storefront + enrollment form (verify the `` deploy actually landed; if it + did, the user's browser doesn't honor `lang` for `` + and we need a JS date-picker swap); "Continuer mes achats" CTA on + `enroll-success.html:118` is wrong for loyalty-only storefronts with no + catalog. +2. **Test 2** — cross-store re-enrollment at FASHIONOUTLET with the email + from Test 1. +3. **Hetzner doc check** — verify whether `docs/deployment/hetzner-server-setup.md` + needs any new step from tonight's fixes. Most likely no (the fixes are + in-code, not deployment), but worth a glance. +4. **Unit tests** — none of the four B1-F bugs were caught by the existing + suite. Add at minimum: + - Assert `celery_app.conf.broker_url` is `redis://...` after importing + `main` (catches future `set_default()` ordering regressions). + - Assert `loyalty.send_notification_email` is in `celery_app.tasks` + after importing `app.modules.loyalty.tasks` (catches future missing + imports in task package `__init__.py`). + - Assert `configure_mappers()` succeeds after importing + `app.core.celery_config` (catches future missing-models regressions + in celery). + - Either assert `task_base.on_failure` doesn't crash on a synthetic + failure, or standardize an `extra=` sanitiser that strips reserved + `LogRecord` attribute names. +5. **Fix `prospecting/tasks/__init__.py`** — add the missing import. +6. **Audit every other module's email path** — are billing's trial-expiration + emails really dispatched via Celery? Messaging's password-reset emails? + If yes, same silent-failure risk exists until a real send hits prod. Add + an integration test that triggers a representative email from every + module and asserts an `email_logs` row appears within N seconds. + ## Status board | # | Pre-launch step | State | Notes |