docs(loyalty): add Android terminal E2E tests to user-journeys checklist
All checks were successful
CI / ruff (push) Successful in 14s
CI / pytest (push) Successful in 2h25m39s
CI / validate (push) Successful in 31s
CI / dependency-scanning (push) Successful in 32s
CI / docs (push) Successful in 50s
CI / deploy (push) Successful in 1m11s

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>
This commit is contained in:
2026-05-10 22:05:00 +02:00
parent d3b1670623
commit a3fb7029bd
2 changed files with 101 additions and 2 deletions

View File

@@ -897,4 +897,97 @@ If Fashion Group doesn't have a loyalty program yet:
- [ ] Cross-store email search works in the terminal (cross-location enabled)
- [ ] "Already a member" message shows correct locations/store based on cross-location setting
- [ ] No duplicate cards created under same merchant (when cross-location enabled)
## Android Terminal Tests (clients/terminal-android)
Same merchant (Fashion Group / FASHIONHUB), with the Android tablet app
built from `clients/terminal-android/`. Build the APK with
`./gradlew assembleDebug` and install via `./gradlew installDebug` to a
tablet or emulator on the same network as prod (`https://rewardflow.lu`)
or your dev server (`http://10.0.2.2:8000` for the emulator).
Use a separate test customer (e.g. `terminal-test@example.com`) — none
of the verifications below should touch the customers from web Tests
18 to keep audit trails clean.
### Test 9: Tablet Pairing
| Step | Action | Expected Result |
|------|--------|-----------------|
| 9.1 | Web admin: visit `/merchants/loyalty/devices` as merchant owner | List page loads, "Pair Tablet" button visible |
| 9.2 | Click "Pair Tablet" → label "Counter A", store FASHIONHUB → Confirm | Modal shows QR + raw JSON, device row appears with `status=active` |
| 9.3 | Tablet: launch app on first boot — landscape setup screen with camera preview | Camera permission prompt; QR scanner active |
| 9.4 | Point camera at the web modal's QR | Decoded → AuthInterceptor swaps host + bearer → `/program` 200 → navigates to PIN screen |
| 9.5 | Web admin: list refreshes | Same device row shows `last_seen_at` populated within ~5s |
| 9.6 | Tablet: kill + relaunch app | Stays on PIN screen (paired flag persists in DataStore) |
| 9.7 | **Manual entry path:** wipe app data (`adb shell pm clear lu.rewardflow.terminal`), relaunch | SetupScreen renders the right-hand form |
| 9.8 | Enter api_url / store_id / store_code / auth_token from the modal's "Show raw payload" → Connect | Same success outcome as QR path |
### Test 10: PIN Screen (Offline-Capable Verify)
Pre-requisite: at least one staff PIN created at `/merchants/loyalty/pins`
for FASHIONHUB. Use a memorable 4-digit PIN.
| Step | Action | Expected Result |
|------|--------|-----------------|
| 10.1 | Tablet: arrives on PIN screen after Test 9 | Left pane lists staff names with avatar circles; right pane shows PIN keypad |
| 10.2 | Type the **wrong PIN** | Shake/error: "Wrong PIN", digits clear, keypad still responsive |
| 10.3 | Type the **right PIN** | Brief spinner → navigates to Terminal screen, header shows staff name |
| 10.4 | **Offline-capable verify:** turn off wifi, lock screen (top-right Lock button), type PIN again | Verifies offline against cached bcrypt hashes; bcrypt-verifies locally |
| 10.5 | Web admin: lock the PIN by triggering 5 wrong attempts via web terminal | `is_locked=true` in DB |
| 10.6 | Tablet: refresh PIN cache (next launch, or after ~5 min) → try the locked PIN | Rejected: locked PINs never match |
### Test 11: Daily Flows from the Tablet
| Step | Action | Expected Result |
|------|--------|-----------------|
| 11.1 | Terminal screen: type a customer email in the search field → Search | Customer card panel renders with name, balance, available rewards |
| 11.2 | Clear (top-right ×) → tap "Scan QR Code" → scan a card QR (use the card's `qr_code_data` field) | Card looked up, same panel renders |
| 11.3 | Clear → tap "Enroll Customer" → fill name + email + birthday → Enroll | Success → customer pane renders with new card pre-selected |
| 11.4 | With customer selected, tap "Add Stamp" → pick a category pill → Confirm | Stamp count increments by 1, recent transactions list shows it |
| 11.5 | "Earn Points" → enter `12.50` (or `12,50`) → pick a category → Confirm | Points balance increases by 125 (assuming 10 pts/€), feed updated |
| 11.6 | Stamp until 10/10 → "Redeem Stamps" → Confirm | Stamp count resets to 0, reward description shown in transactions |
| 11.7 | "Redeem Reward" with enough points → pick a reward → Confirm | Points decremented, reward feed entry shown |
| 11.8 | Backend audit: `SELECT acting_terminal_device_id FROM loyalty_transactions ORDER BY id DESC LIMIT 5` | Latest 5 rows have `acting_terminal_device_id` set to the paired device id |
### Test 12: Offline Queue + Sync
| Step | Action | Expected Result |
|------|--------|-----------------|
| 12.1 | Tablet: airplane mode ON (or emulator: extended controls → cellular off) | Top-bar pill flips to "Offline" |
| 12.2 | "Add Stamp" with categories → Confirm | Snackbar: "Queued — will sync when back online"; top bar shows "1 pending sync" |
| 12.3 | "Earn Points" with amount + categories → Confirm | Same: queued, pending count → 2 |
| 12.4 | Tap "Redeem Stamps" (any redeem) | Button is **disabled** when offline (per plan; redeem requires authoritative balance) |
| 12.5 | Re-enable wifi | NetworkMonitor flips to Online, sync worker fires under network constraint |
| 12.6 | Within ~510s, pending count drops to 0 | Recent transactions feed auto-refreshes with the queued operations now applied |
| 12.7 | Backend audit: queued rows in `loyalty_transactions` carry `acting_terminal_device_id` | All synced operations attributed to this device |
| 12.8 | DB: `SELECT COUNT(*) FROM pending_transactions WHERE status='pending'` (Room db on device — verify via adb if needed) | 0 — synced rows deleted, queue empty |
### Test 13: Auto-Lock + Manual Lock
| Step | Action | Expected Result |
|------|--------|-----------------|
| 13.1 | After verifying PIN, leave the Terminal screen untouched for ~2 minutes | IdleTracker fires `onIdle` → navigates back to PIN screen |
| 13.2 | Verify PIN → Terminal → tap the top-right Lock button | Immediate return to PIN screen |
| 13.3 | While an action dialog (e.g. "Earn Points") is open, wait 2 min | Tracker still locks behind the dialog — pointer events on Compose dialogs don't bubble to the tracker. Intended behavior. |
### Test 14: Device Revocation
| Step | Action | Expected Result |
|------|--------|-----------------|
| 14.1 | Web admin: `/merchants/loyalty/devices` → click the orange Revoke icon on the paired tablet's row → Confirm | Row shows "Revoked" status, `revoked_at` set |
| 14.2 | Tablet: try any action (e.g. lookup a card) | API returns 401 `TERMINAL_DEVICE_REVOKED`; cashier sees the error inline |
| 14.3 | Tablet: kill + relaunch | Still rejects — token is no longer valid; merchant must re-pair |
| 14.4 | Re-pair from the web → tablet reset + new QR scan | Restored to working state |
### Things to Watch on the Tablet
- [ ] Camera permission prompt is in-place (no jarring redirect to system settings)
- [ ] QR decode is single-shot — scanner doesn't fire twice on the same code
- [ ] PIN keypad stays responsive even during bcrypt verify (it's brief but visible)
- [ ] Action dialogs auto-close on success; failure messages quote the server's actual error (e.g. "Daily stamp limit reached"), not a generic "HTTP 400"
- [ ] Offline pill in top bar matches actual connectivity state (toggle wifi and watch it flip)
- [ ] "Pending sync N" badge increments on queue, drops on drain
- [ ] After successful action, balance card + recent feed update without manual refresh
- [ ] Immersive mode keeps system bars hidden; swipe down briefly reveals them, then auto-hide
- [ ] Rate limiting: rapid-fire stamp calls eventually return 429

View File

@@ -24,7 +24,8 @@ non-blocking).
| 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 user-journey E2E tests | ⏳ | **The remaining gate** — user does this with a real test customer |
| 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, 13 day Google review. App-side change is zero; same issuer + service account, passes become public-visible once approved |
@@ -94,9 +95,14 @@ In priority order:
locale). `loyalty_enrollment`, `loyalty_welcome_bonus` and
`loyalty_reward_ready` are the customer-visible ones — adjust
subject lines + body copy if anything reads off-brand.
2. **Walk the 8 user-journey E2E tests** — checklist at the bottom of
2. **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 914). Covers
pairing (QR + manual), offline PIN bcrypt verify, daily flows
(stamp/earn/redeem/enroll), offline queue drain, idle auto-lock,
and device revocation cutoff.
3. **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.