# Phase 3.7 Testing Checklist End-to-end verification for Phase 3.7 (Paid Pairing Flow + Returning-Chat + Extension Flip). Tick boxes as you verify. Cluster labels: **[BE]** backend / curl, **[CC]** control_center, **[M]** mitra_app, **[C]** client_app. Related docs: [phase3.7.md](./phase3.7.md), [phase3.7-plan.md](./phase3.7-plan.md), [phase3.7-questions.md](./phase3.7-questions.md). > **Backend curl smoke (a–n) was executed in Stage 2** and re-validated after the `/simplify` Tier 1 bug fixes. This document focuses on the real-device E2E layer that the agent sandbox couldn't run, plus the regression checks for the bugs caught in `/simplify`. --- ## Setup - [ ] Backend running on `192.168.88.247:3000` (public) + `:3001` (internal) — verify with `curl http://192.168.88.247:3000/health` - [ ] Migrate has been re-run after Phase 3.7 (`payment_sessions`, `pairing_failures`, `chat_sessions.payment_session_id`, `idx_chat_sessions_mitra_status` all present) - [ ] Seed has been re-run / 4 new app_config keys exist: `payment_session_timeout_minutes=20`, `returning_chat_confirmation_timeout_seconds=20`, `extension_default_action_on_timeout=auto_approve`, `pairing_blast_timeout_seconds=60` - [ ] Existing `extension_timeout_seconds` is set to `10` (default flip in 3.7 — manual update needed on existing dev DBs) - [ ] Both apps built with `--dart-define=API_BASE_URL=http://192.168.88.247:3000` and installed on real devices (or 1 device + 1 emulator) - [ ] Control center reachable in browser (port 3001 or whatever the dev URL is) and CC operator logged in - [ ] At least 2 mitra accounts available (1 to be online by default, 1 to flip during tests) --- ## Section A — Home CTA Gating + 5s Availability Poll - [ ] **[C]** No mitra online → "Mulai Curhat" CTA is greyed-out with subtitle "Belum ada bestie tersedia" - [ ] **[C]** Mitra goes online → CTA enables within 5 seconds (check the polling cadence in backend logs) - [ ] **[C]** Last mitra hits capacity (start `max_customers_per_mitra` chats with that mitra) → CTA flips disabled within 5 seconds - [ ] **[C]** Background the app for 60s → poll pauses (no per-5s log entries from this device); foreground → immediate poll fires - [ ] **[C]** Pull-to-refresh on home → manual one-shot poll fires - [ ] **[C]** Kill backend (or disconnect WiFi) → CTA flips disabled within 5s (poll-fail default per PRD §1.3) - [ ] **[C]** Customer UI never displays the count — only the binary state (PRD §1.5) - [ ] **[BE]** Backend logs confirm `mitra-availability` does NOT issue a Postgres query on every poll — only once per 10s TTL window (the `countAvailableMitrasFromCache()` cache backstop) ## Section B — Payment Screen + Happy-Path Blast - [ ] **[C]** Tap CTA → `/payment` route renders the tier picker - [ ] **[C]** Paid tier: pick a tier → "Bayar" CTA → searching screen → mitra accepts → chat starts - [ ] **[BE]** After chat starts: `payment_sessions.status = consumed`, `chat_sessions.payment_session_id` is set - [ ] **[C]** Free trial-eligible customer: payment screen shows "Gratis" / Rp 0 → "Mulai" CTA → searching → chat - [ ] **[BE]** Free-trial flow creates a `payment_sessions` row with `amount=0, is_free_trial=true` - [ ] **[C]** Customer hits Android back button on payment screen → `cancelIfPending` fires (verify `payment_sessions.status = abandoned` server-side) - [ ] **[C]** Customer abandons payment screen, waits >20 minutes → row auto-expires (`payment_sessions.status = expired`) via the 60s sweeper - [ ] **[C]** Customer confirms payment, then no mitra accepts within blast window (60s default) → terminal screen with copy "Maaf, kami tidak bisa menemukan bestie untuk sesimu. Tim kami akan menghubungimu segera." + "Kembali ke beranda" CTA - [ ] **[BE]** Above produces a `pairing_failures` row tagged `no_mitra_available` + `payment_sessions.status = failed_pairing` - [ ] **[C]** Customer confirms, all blasted mitras explicitly decline → terminal screen - [ ] **[BE]** Above produces a row tagged `all_mitras_rejected` ## Section C — Customer Cancel (Regression for `/simplify` Tier 1 Bug Fix #3) - [ ] **[C]** Customer hits "Batalkan" mid-search → routes to home, **NOT** to the failed-pairing terminal screen - [ ] **[C]** No FCM "Sesi gagal" notification fires for the canceller's own action (test with app backgrounded — kill the app, send the cancel from another device, verify no FCM lands) - [ ] **[BE]** Cancel still produces a `pairing_failures` row tagged `customer_cancelled` for CC visibility - [ ] **[BE]** Backend logs show **no** `PAIRING_FAILED` WS push from `cancelPairingRequest` / `cancelPaymentSearch` paths ## Section D — Returning Chat ("Curhat lagi") - [ ] **[C]** Chat history row has "Curhat lagi" CTA on every entry - [ ] **[C]** Tap "Curhat lagi" → payment screen with `targeted_mitra_id` baked in → confirm → searching screen with **targeted-waiting overlay** showing mitra name + countdown - [ ] **[C]** Countdown starts from `confirmation_timeout_seconds` value returned by backend (regression for `/simplify` Tier 1 fix #6 — was hardcoded to 20) - [ ] **[CC]** Change `returning_chat_confirmation_timeout_seconds` from 20 → 7 in CC; next "Curhat lagi" overlay starts at 7 - [ ] **[C]** Mitra accepts within window → chat starts (`payment_sessions.status = consumed`) - [ ] **[BE]** Verify exactly **one** `chat_sessions` row, FK'd to the original payment session - [ ] **[C]** Mitra ignores the card → 20s elapses → bestie-unavailable popup ("Bestie sedang tidak online" + "Chat dengan bestie lain" + "Kembali") - [ ] **[BE]** Above produces `pairing_failures` row tagged `targeted_mitra_timeout`; payment session **stays `confirmed`** (intermediate failure — verify via SQL) - [ ] **[C]** Mitra explicitly declines within window → bestie-unavailable popup - [ ] **[BE]** Above produces row tagged `targeted_mitra_rejected`; payment still `confirmed` - [ ] **[C]** Tap "Chat dengan bestie lain" → general blast fires against the **same** payment session, no double-charge - [ ] **[BE]** Verify `chat_sessions` table now shows TWO rows for one `payment_session_id` (original targeted = expired/cancelled, fallback blast = active or whatever it ends as). The CC Failed Pairings table will show the targeted attempt as a row keyed on its own `pairing_failures.id`. - [ ] **[C]** Tap "Kembali" instead → home (no fallback fired) - [ ] **[C]** Targeted mitra is offline at tap time → 409 from server → bestie-unavailable popup opens directly (no overlay was rendered) - [ ] **[BE]** Above produces row tagged `targeted_mitra_offline`; payment stays `confirmed` - [ ] **[C]** **Sensitive topic regression** (`/simplify` Tier 1 fix #7): start a sensitive-topic session, return-to-history, "Curhat lagi" → fallback to general blast → verify the new general blast carries the sensitive flag (mitra sees the warning badge). Pre-fix: was hardcoded to regular. - [ ] **[C]** Mitra is at-capacity-but-mid-session-with-this-customer → returning request still goes through (PRD §3.4 carve-out) - [ ] **[C]** Mitra is at-capacity with someone else (not this customer) → 409 → bestie-unavailable popup ## Section E — Extension Flip (10s Auto-Approve + Safeguard) - [ ] **[C]** In an active chat, tap extension → picker shows tiers (no free-trial option per PRD §5.2) - [ ] **[BE]** Customer cannot create an extension payment_session with `is_extension=true AND is_free_trial=true` (server returns 400) - [ ] **[M]** Extension card on mitra side shows the new copy: "Tidak menjawab dalam {X} detik = otomatis disetujui" — `{X}` is read from WS payload `timeout_seconds`, NOT hardcoded - [ ] **[CC]** Change `extension_timeout_seconds` from 10 → 6 in CC; next extension card shows "6 detik" - [ ] **[M+C]** Mitra explicitly approves within 10s → extension applied, payment consumed, transaction recorded - [ ] **[M+C]** Mitra explicitly declines within 10s → no extension, no charge, audit row tagged `extension_rejected` - [ ] **[M+C]** Mitra ignores the card 10s, stays online + WS connected → **auto-approve** fires, charge applied - [ ] **[M+C]** Mitra toggles offline mid-window → 10s expires → safeguard auto-rejects, no charge, audit row tagged `extension_safeguard_tripped` - [ ] **[M+C]** Mitra force-quits the app mid-window → same as offline (WS disconnect detected by safeguard) - [ ] **[CC]** Flip `extension_default_action_on_timeout` from `auto_approve` → `auto_reject`; next extension that times out is rejected (audit tagged `extension_rejected`) - [ ] **[M+C]** Race regression (`/simplify` Tier 1 fix #4): mitra approves at exactly the moment timer fires → exactly one outcome wins; no double-action; no spurious "session closing" push if approve won ## Section F — Mitra App: Returning vs General Card - [ ] **[M]** General blast WS arrives → overlay shows accept/decline (NO countdown) - [ ] **[M]** Returning chat WS arrives → overlay shows accept/decline + visible countdown starting at `confirmation_timeout_seconds` from payload - [ ] **[M]** Countdown ticks down 1Hz; at 0s, overlay auto-dismisses (server is source of truth — no client-side decline call) - [ ] **[M]** Subsequent `chat_request_closed` WS confirms server already auto-rejected - [ ] **[M]** Cold-start via FCM tap (kill app, tap notification): pending list correctly surfaces `request_type` for in-flight requests; returning ones get the countdown - [ ] **[M]** Mitra accepts a general blast that another mitra also accepts simultaneously → exactly one wins, the other sees "request no longer available" (existing DB unique constraint regression check) ## Section G — Control Center ### G.1 Settings page - [ ] **[CC]** New row "Batas Waktu Blast Pairing" — number input, current value visible, save fires PATCH - [ ] **[CC]** New row "Batas Waktu Sesi Pembayaran" — minutes input, save works - [ ] **[CC]** New row "Batas Waktu Konfirmasi Chat Lanjutan" — seconds input, save works - [ ] **[CC]** New row "Aksi Default Jika Bestie Tidak Menjawab Extension" — radio group `Auto-approve` / `Auto-reject`, save works - [ ] **[CC]** Each save triggers in-memory cache invalidation (verify by changing the value and seeing the new behavior on the next request, no backend restart needed) ### G.2 Failed Pairings page - [ ] **[CC]** Sidebar entry "Failed Pairings" appears between Sesi and Users - [ ] **[CC]** Page lists rows with: Created, Customer, Targeted Mitra (or "—"), Cause, Amount, Operator Action, Actioned By, Actioned At - [ ] **[CC]** Cause-tag multi-select filter narrows the list correctly - [ ] **[CC]** Date range filter narrows correctly - [ ] **[CC]** Per-row action menu: "Mark as refunded" / "Mark as credited" / "Mark as no-action" — each updates the row - [ ] **[CC]** Already-actioned rows show "—" instead of the action button (current client-side lock — see Open Question below) - [ ] **[CC]** Pagination Prev/Next works at >50 rows (seed enough failures to test) ## Section H — Cross-Cutting + Hot-Path Verification - [ ] **[BE]** `findAvailableMitras` projects `active_session_count` in one query (no per-mitra COUNT roundtrip) — confirm by enabling pg query logging during a blast with 5+ available mitras - [ ] **[BE]** Blast loop runs notify+insert in parallel — verify total blast time scales sub-linearly with mitra count - [ ] **[BE]** `expireStalePaymentSessions` sweeper runs every 60s; under load (manually create 20 stale rows via direct DB INSERT), single batch UPDATE flips them all + parallel notifies - [ ] **[BE]** Composite index `idx_chat_sessions_mitra_status` exists; `EXPLAIN ANALYZE` on the per-mitra capacity subquery uses the index, not a seq scan - [ ] **[C]** Availability notifier no-op guard: keep mitra availability stable, observe Riverpod listeners on home — no rebuild every 5s when the value is unchanged (use Flutter DevTools rebuild counter) - [ ] **[BE]** Mitra goes offline mid-session → existing chat continues; mitra app shows offline toggle; new returning-chat requests to that mitra immediately auto-reject (Section D check) ## Section I — Anonymity Regression (existing rule, must not regress) - [ ] **[M]** Mitra always sees customer's `call_name` in chat header, request card, history, returning-chat card - [ ] **[M]** Mitra never sees customer phone, email, or social ID anywhere in 3.7-touched UI ## Section J — Multi-Actor Scenario: Mitra Goes Offline Mid-Session Verifies the domain rule that the mitra offline toggle is independent of active session state — going offline mid-chat must not interrupt the in-progress session, but availability for *new* customers must reflect the offline state immediately. > Memory reference: "Mitra Can Go Offline Mid-Session" — never use "in-session" as a proxy for "online". ### Setup - [ ] Set `max_customers_per_mitra = 2` in CC (so a single chat doesn't auto-fill capacity and mask the test signal) - [ ] Two customer accounts (Customer 1, Customer 2) signed in on two devices/emulators (or one of each) - [ ] One mitra account (Mitra A) signed in on the mitra app - [ ] No other mitra is online (verify via CC mitra dashboard) ### Steps 1. [ ] **[M]** Mitra A taps online toggle → status flips to online 2. [ ] **[C, Customer 1]** Home shows CTA enabled 3. [ ] **[C, Customer 1]** Tap "Mulai Curhat" → payment screen → "Bayar" → searching screen 4. [ ] **[BE]** Verify blast fires to Mitra A (check `chat_request_notifications` row inserted) 5. [ ] **[M]** Incoming-request overlay appears → tap "Terima" 6. [ ] **[C, Customer 1]** Searching → "Bestie Ditemukan" → chat screen with Mitra A 7. [ ] **[BE]** Verify `chat_sessions.status = active`, `payment_sessions.status = consumed` 8. [ ] **[C, Customer 1 + M]** Exchange messages both ways (e.g., "hai" → "halo apa kabar" → "baik" → "ada yang bisa dibantu") — each message renders on both sides with delivery + read status 9. [ ] **[C, Customer 2]** Open Customer 2's app — home CTA is **enabled** (Mitra A has 1/2 capacity, still available) 10. [ ] **[M]** Mitra A taps offline toggle **while still on the chat screen with Customer 1** 11. [ ] **[M]** Toggle succeeds — no "session ended" / "you're in a session" blocker dialog. Mitra A status now offline in CC dashboard 12. [ ] **[C, Customer 1]** Chat screen is **uninterrupted**: - No "session ended" banner - No closure dialog - Session timer continues counting down - Messages sent before are still visible 13. [ ] **[C, Customer 1 + M]** Exchange more messages after Mitra went offline ("masih ada?" → "ada, masih disini") — messages still flow normally via WS (mitra's WS connection is independent of the online toggle) 14. [ ] **[C, Customer 2]** Wait up to 5 seconds (one poll cycle) → CTA flips to **disabled** with subtitle "Belum ada bestie tersedia" (Mitra A is now offline, no other mitras online) 15. [ ] **[BE]** Verify `GET /api/client/mitra-availability` returns `{ available: false }` 16. [ ] **[C, Customer 1]** Let session run to natural expiry (or end early if `early_end_*` flag enabled) — verify normal session-close flow (closing screen → goodbye composer → completed) 17. [ ] **[BE]** Verify `chat_sessions.ended_by` reflects timer-expiry / customer / mitra-explicit-close — NOT a `mitra_offline` force-close ### Regression checks tied to this scenario - [ ] **[C, Customer 1]** While Mitra A is offline mid-session, Customer 1 requests an extension → extension request fires normally (existing chat scope), but the safeguard tagged `extension_safeguard_tripped` fires when the 10s auto-approve timer hits because mitra is unreachable (PRD §5.5 — already in Section E.23, this is the multi-actor variant) - [ ] **[C, Customer 2]** While Mitra A is offline + Customer 1's session is still active, Customer 2 attempting "Curhat lagi" against Mitra A from a *prior* chat history row → 409 `targeted_mitra_offline` → bestie-unavailable popup (auto-reject immediate per Section D) - [ ] **[CC]** While Mitra A is offline mid-session, the active sessions admin view still lists Customer 1's session as active — operator should not be misled into thinking the session was force-closed ## Outstanding Open Questions (need user decision before resolving) - [ ] **Re-action lock**: Failed Pairings rows are locked client-side after first operator action even though backend `setOperatorAction` allows overwriting. Decide: keep lock (current) or allow revisions. - [ ] **Mitra pre-existing `ref.listen` in `build`** in `mitra_app/lib/core/chat/chat_request_notifier.dart` — flagged in `/simplify`, out of 3.7 scope, separate cleanup pass. - [ ] **Browser smoke for Failed Pairings page** — Stage 4 verified backend round-trip but skipped real browser click-through (no headless browser in agent sandbox). --- ## Test Data Setup Helpers Quick curl commands to seed test conditions: ```bash # Force a mitra online for testing curl -X POST http://192.168.88.247:3000/api/mitra/status/online \ -H "Authorization: Bearer " # Create a stale payment_session for sweeper testing psql -c "INSERT INTO payment_sessions (customer_id, amount, duration_minutes, status, expires_at) VALUES ('', 50000, 30, 'confirmed', NOW() - INTERVAL '1 minute')" # Force a config invalidation event (simulating multi-instance) redis-cli PUBLISH config:invalidate '{"key":"max_customers_per_mitra","ts":1234567890}' # List all failed-pairing rows by tag curl -s http://localhost:3001/internal/failed-pairings?cause_tags=customer_cancelled \ -H "Authorization: Bearer " | jq ```