The web user-journey checklist (Tests 1–8) only covers human-using-loyalty flows from a browser. The cashier-facing Android tablet built in Phases A–F goes through a different surface and has its own failure modes that won't surface in any web test. Adding 6 dedicated Android tests so a tablet-in-hand verification has the same level of structure as the web side. - Test 9: Tablet pairing — QR scan + manual entry fallback, with the audit (paired-device row appears, last_seen_at populated) - Test 10: PIN screen — wrong/right PIN, offline-capable bcrypt verify, locked-PIN rejection - Test 11: Daily flows — search, scan, enroll, stamp, earn, redeem, with the acting_terminal_device_id audit column check at the end - Test 12: Offline queue + sync — airplane mode → queued → re-online → drain; redeem is hard-disabled offline per spec - Test 13: Auto-lock + manual lock — 2 min idle, immediate lock button, the known caveat that AlertDialog pointer events don't bubble - Test 14: Device revocation — revoke on web → 401 on tablet next call Updated the go-live readiness snapshot to reference these as Step 6b (gated on user obtaining a tablet, not on schedule). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6.8 KiB
Loyalty Go-Live Readiness — 2026-05-10
Snapshot of where the loyalty platform stands the night of 2026-05-10. The
canonical sequenced plan is still
app/modules/loyalty/docs/production-launch-plan.md;
this doc records the current state (✅ / ⏳ / 🟡) and what surfaced
during the prod readiness pass.
TL;DR
The technical pre-launch checklist is green. The remaining gate is a human one — walking the 8 user-journey E2E tests on prod with a real test customer and confirming nothing surprises us. After that, flip the loyalty platform live for FASHIONHUB's stores and start the Google Wallet production-access review in parallel (1–3 day Google review, non-blocking).
Status board
| # | Pre-launch step | State | Notes |
|---|---|---|---|
| 1 | Seed loyalty email templates on prod | ✅ | 20 rows (5 templates × 4 locales) all is_active=true |
| 2 | Google Wallet config on Hetzner | ✅ | Wallet config validator green: credentials valid, issuer 3388000000023089598, origin https://rewardflow.lu, default logo reachable |
| 3 | Database migrations | ✅ | All four module heads current incl. loyalty_011 (acting-device audit) on prod |
| 4 | FR/DE/LB translations for analytics i18n keys | 🟡 | 8 keys still EN-only. Cosmetic, doesn't block soft launch |
| 5 | messaging.manage_templates permission for store owners |
🟡 | Only matters if merchants self-edit templates. Admin can edit centrally. Defer |
| 6 | 8 web user-journey E2E tests | ⏳ | The remaining gate — user does this with a real test customer |
| 6b | 6 Android terminal E2E tests | ⏳ | Pairing, PIN, daily flows, offline queue, auto-lock, device revoke — gated on user obtaining a tablet |
| 7 | Google Wallet real-device pass test | ✅ | Already confirmed earlier — cards register, points/redeem visible on personal Google Wallet |
| 8 | Go live | ⏳ | Gated by #6. Cleanup test data + enable platform feature flags for FASHIONHUB |
| 9 | Google Wallet production access | ⏳ | Post-launch, 1–3 day Google review. App-side change is zero; same issuer + service account, passes become public-visible once approved |
What got sorted tonight
SMTP wired to a self-hosted mail server
Started here:
- prod
.envhadEMAIL_PROVIDER=sendgrid+ a SendGrid API key - SendGrid free trial (60 days) had expired
SMTP_*env vars were placeholders pointing atsmtp.example.com
Discovered that /admin/settings lets you store SMTP config in the DB
(table admin_settings, category email) and those values win over
.env. User had already configured:
email_provider=smtpsmtp_host=mail1.myservices.hostingsmtp_port=465← problematicsmtp_user=support@wizard.lu/ encrypted passwordsmtp_use_ssl=true, smtp_use_tls=false
Diagnosis from the prod container:
| Check | Result |
|---|---|
DNS resolves mail1.myservices.hosting |
✅ 185.26.107.245 |
TCP mail1.myservices.hosting:465 |
❌ timed out |
TCP mail1.myservices.hosting:587 |
✅ open |
Either Hetzner blocks 465 outbound for this VPS or the provider firewalls Hetzner's IP range on 465. Either way, port 587 (submission
- STARTTLS) is the modern path and works.
Fix: changed /admin/settings to port 587, SSL off, TLS on. Test
email landed in inbox immediately, sender header Support Wizard <support@wizard.lu> — proving the DB override was being used.
Cosmetic bug found and fixed
The test email's body claimed the configuration that would have been
used if .env were authoritative — i.e. it said Provider: sendgrid
and From: noreply@wizard.lu even though the actual send went via
SMTP from support@wizard.lu. Two places in the code:
app/modules/core/routes/api/admin_settings.py::send_test_email— body template hardcodedapp_settings.email_providerandapp_settings.email_from_addressapp/modules/messaging/services/email_service.py— the "template not found"EmailLogbranch recordedsettings.email_provider/settings.email_from_addressinstead of the effective config
Both now read from get_effective_email_config(db) /
self._platform_config, so the test email page and audit logs reflect
what was actually used.
Commit: f2d1bdcd on master, deployed via Gitea Actions.
What the user does next
In priority order:
- Tonight or tomorrow — review email copy. Open
/admin/email-templatesand skim the 5 loyalty templates (EN locale).loyalty_enrollment,loyalty_welcome_bonusandloyalty_reward_readyare the customer-visible ones — adjust subject lines + body copy if anything reads off-brand. - Walk the 8 web user-journey E2E tests — checklist at the bottom of
app/modules/loyalty/docs/user-journeys.md. Use a personal email as the test customer. 2b. Once a tablet is on hand: walk the 6 Android terminal tests — same doc, "Android Terminal Tests" section (Tests 9–14). Covers pairing (QR + manual), offline PIN bcrypt verify, daily flows (stamp/earn/redeem/enroll), offline queue drain, idle auto-lock, and device revocation cutoff. - Flip live for FASHIONHUB — clean any test data, double-check
Celery (
docker compose ps | grep celery), enable loyalty feature on FASHIONHUB's stores via the admin UI. - In parallel, file Google Wallet production access — pay.google.com/business/console → Wallet API → Manage → Request production access. Use sample pass screenshots from FASHIONHUB. Google reviews the Issuer, not individual merchants — once approved all merchants on the platform are covered.
Open follow-ups (non-blocking)
These can wait but are worth tracking:
- FR/DE/LB translations for the 8 analytics i18n keys
(
store.analytics.revenue_title,store.analytics.cohort_title, etc.). EN shows through; cosmetic only. messaging.manage_templatespermission discovery for merchant_owner role — needed if/when merchants self-edit templates. Admin can edit centrally for v1.- Failed-PIN-attempt reporting from Android tablet → server lockout
counter — tablet bcrypts locally and silently fails; a stolen
tablet's brute-forcer doesn't trip server-side lockout. Add a tiny
POST /pins/{id}/record-failed-attemptendpoint plus a call from the PinViewModel's failure branch. - Splash screen + per-action success animation for the Android tablet — Phase F polish that was intentionally deferred.
Reference
- Canonical plan:
app/modules/loyalty/docs/production-launch-plan.md - Hetzner runbook:
deployment/hetzner-server-setup.md - Wallet diagnostics page:
/admin/loyalty/wallet-debug(super admin only) - Recent commits relevant to this session:
f2d1bdcdfix(messaging): test email + EmailLog show effective config