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>
This commit is contained in:
201
requirement/phase3.7-testing.md
Normal file
201
requirement/phase3.7-testing.md
Normal file
@@ -0,0 +1,201 @@
|
||||
# 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 <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
|
||||
```
|
||||
Reference in New Issue
Block a user