diff --git a/app/modules/loyalty/docs/user-journeys.md b/app/modules/loyalty/docs/user-journeys.md index ca7fc3cd..4d5afef3 100644 --- a/app/modules/loyalty/docs/user-journeys.md +++ b/app/modules/loyalty/docs/user-journeys.md @@ -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 +1–8 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 ~5–10s, 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 diff --git a/docs/proposals/loyalty-go-live-readiness.md b/docs/proposals/loyalty-go-live-readiness.md index 4a28790c..3739db3b 100644 --- a/docs/proposals/loyalty-go-live-readiness.md +++ b/docs/proposals/loyalty-go-live-readiness.md @@ -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, 1–3 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 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. 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.