Files
halobestie-clone/requirement/phase3.7-testing.md
ramadhan sjamsani d09e50af55 Phase 3.7: paid pairing flow + returning chat + extension flip
- Backend: payment_sessions + pairing_failures tables; payment.service.js
  and pairing-failure.service.js (new); rewritten pairing.service.js
  (payment-gated blast + targeted "Curhat lagi" + cancel + fallback);
  rewritten extension.service.js (data-driven auto-approve with offline
  safeguard, charge-at-approval); pricing.service.js (extension tiers
  without free trial); mitra-status.service.js (countAvailableMitras
  cached path); 60s sweeper for stale payment sessions
- Backend routes: client.payment.routes, client.mitra-availability.routes,
  internal/failed-pairings.routes; client.chat.routes rewritten for
  payment-gated start + /returning + /cancel + /fallback-to-blast;
  internal/config.routes adds 4 new keys with Valkey invalidate publish
- client_app: mitra-availability poll, payment screen + notifier, pairing
  notifier rewrite (PairingTargetedWaiting + PairingFailed states),
  targeted-waiting overlay + bestie-unavailable dialog, "Curhat lagi"
  CTA, failed-pairing terminal, extension via payment-session
- mitra_app: PairingRequestType enum, returning-chat 20s countdown
  auto-dismiss, extension card "otomatis disetujui" copy
- control_center: 4 new config rows in Settings, Failed Pairings page
  (filter + paginate + action menu), sidebar + route registered
- Test infrastructure: Vitest backend (7/7 pass), Playwright CC (4/4
  pass), Maestro mobile scaffold (CLI install pending)
- Bugs found via Playwright + fixed: LoginPage labels not associated
  with inputs (a11y); backend internal CORS missing PATCH/PUT/DELETE
  in allow-methods (silent settings breakage in browsers since Stage 4)
- Docs: phase3.7.md PRD, phase3.7-plan.md, phase3.7-questions.md (Q&A),
  phase3.7-testing.md (E2E checklist), phase3.7-test-run-2026-05-03.md
  (today's run results)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 23:02:49 +08:00

202 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 (an) 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 <mitra-jwt>"
# Create a stale payment_session for sweeper testing
psql -c "INSERT INTO payment_sessions (customer_id, amount, duration_minutes, status, expires_at)
VALUES ('<customer-uuid>', 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 <cc-jwt>" | jq
```