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

17 KiB
Raw Permalink Blame History

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-plan.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_approveauto_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:

# 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