Phase 4 §4: payment-before-pair for returning users + Maestro suite
Stages 5.1, 5.3, 5.4 of the returning-user flow rework. All three §4 entry paths now require payment BEFORE pairing, matching the updated mermaid spec. * Spec (requirement/flow_customer.mermaid.md §4): payment block converges three call-sites (bestie-yang-udah-kenal-online, bestie-baru, offline-popup → cari bestie lain). PairRoute dispatches lama → targeted pair, baru/cari-lain → §3 blast. §3 retains its post-payment-shared contract. * Stage 5.1 (client_app): PaymentDraft carries targetedMitraId + topicSensitivity. bestie_history_list seeds the draft + pushes /payment/entry (was legacy /payment). searching_screen branches on draft.targetedMitraId for blast-vs-targeted dispatch. payment_entry uses resetExceptTarget(); bestie_choice_sheet + home _onCurhatBestieBaruPressed call explicit reset() before push so the keepAlive draft can't leak stale targeting into a blast. * Stage 5.3 (client_app): new BestieOfflineVariant.prePayReturning. Bestie-history-list _BestieRow splits tappable from dim so offline rows render dimmed but route taps into the popup. CTA "cari bestie lain" resets the draft + pushes /payment/entry. * Stage 5.4 (client_app): deleted legacy /payment route, payment_screen.dart, payment_notifier.dart(+.g.dart). router cleaned. * Tests (requirement/phase4-customer-flow.md + client_app/.maestro/): six Maestro flows TS-01..TS-06 covering every §4 branching point, all passing end-to-end. Shared onboarding prelude under .maestro/subflows/. New helper scripts: accept_latest_pending, force_mitra_offline, force_other_mitra_online, reset_all_mitras_online, mitra_accept_latest_internal. New backend _test endpoints to match. /reset-phone now cascade-deletes customer_transactions (FK was blocking). /force-pairing-timeout branches targeted (RETURNING_CHAT_TIMEOUT via expireTargetedPairingRequest, now exported) vs blast (PAIRING_FAILED). seed_history_session also outputs MITRA_NAME_RE (regex-escaped) for reliable selectors against display names containing regex specials. * mitra_app: dispose-during-deactivate guardrail for back-press on the mitra chat screen after the customer's goodbye message. Pending real emulator repro verification (carried over from 2026-05-15). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -599,3 +599,282 @@ OTP smoke (existing `01_smoke.yaml`) must keep passing — uses the dev-only
|
||||
10. **Maestro coverage** + visual regression sweep.
|
||||
|
||||
Each block is shippable independently — they share no breaking schema change.
|
||||
|
||||
---
|
||||
|
||||
# Test Scenarios
|
||||
|
||||
Manual reproduction checklists for Phase 4 customer flows. Tick boxes as
|
||||
verified. Cluster tag `[C]` = client_app, `[BE]` = backend setup.
|
||||
|
||||
> **Coverage map** — these scenarios collectively exercise every branching
|
||||
> point in §4 of `flow_customer.mermaid.md`:
|
||||
>
|
||||
> | Branching point | Scenario(s) |
|
||||
> |---|---|
|
||||
> | Choice: `bestie yang udah kenal` vs `bestie baru` | TS-01/02/03/05/06 vs TS-04 |
|
||||
> | CheckOnline: yes vs no (pre-pay) | TS-01/05/06 vs TS-02/03 |
|
||||
> | OfflinePopup (pre-pay): `cari bestie lain` vs `tanya admin` | TS-02 vs TS-03 |
|
||||
> | PayStat: `paid` vs `timeout 20 min` | TS-01/02/04/06 vs TS-05 |
|
||||
> | PairRoute: `lama (Targeted)` vs `baru / cari lain (BlastFlow)` | TS-01/05/06 vs TS-02/04 |
|
||||
> | TargetedRes: `accept` vs `reject/timeout` | TS-01/05 vs TS-06 |
|
||||
|
||||
## TS-01 — Returning user re-pays an online bestie (lama happy path)
|
||||
|
||||
**Flow:** §4 `Choice → "bestie yang udah kenal" → CheckOnline (yes) → PickMethod → … → paid → PairRoute (lama) → Targeted → accept → S10`
|
||||
|
||||
**Affects:** `client_app`, `backend`.
|
||||
|
||||
**Goal:** Confirm the returning-user flow gates on payment and routes the
|
||||
same picked mitra through targeted pairing (not blast).
|
||||
|
||||
**Pre-reqs**
|
||||
- [ ] **[BE]** Backend reachable; test mitra signed in + online (renders
|
||||
`ONLINE` pill in history list).
|
||||
- [ ] **[BE]** Free-trial config OFF for the test customer (otherwise the
|
||||
paywall path replaces the QRIS flow).
|
||||
- [ ] **[C]** `client_app` pointed at local backend
|
||||
(`--dart-define=API_BASE_URL=http://192.168.88.247:3000`); test customer
|
||||
has at least one closed session with the test mitra so they appear in
|
||||
bestie history.
|
||||
|
||||
**Steps**
|
||||
1. [ ] **[C]** From home (returning state), tap `curhat sama bestie baru`
|
||||
→ Bestie Choice Sheet appears.
|
||||
2. [ ] **[C]** Tap `bestie yang udah kenal` → bestie history list opens;
|
||||
the test mitra row shows `ONLINE` pill (not dimmed).
|
||||
3. [ ] **[C]** Tap the test mitra row → app navigates to `/payment/entry`
|
||||
(PickMethod). **The legacy `/payment` route is no longer reachable as
|
||||
of Stage 5.4.**
|
||||
4. [ ] **[C]** Pick `chat` (or `voice call`) → PickDuration.
|
||||
5. [ ] **[C]** Pick any tier (e.g. `5 Menit`) → `/payment/method` (the
|
||||
"cara bayar" screen).
|
||||
6. [ ] **[C]** Pick a payment method (e.g. QRIS) → tap `Bayar` →
|
||||
`/payment/waiting` (20-min QRIS countdown).
|
||||
7. [ ] **[BE]** Manually confirm the payment via
|
||||
`POST /api/client/payment-sessions/:id/confirm` (or use the mock
|
||||
helper script).
|
||||
8. [ ] **[C]** App auto-advances through notif-gate and lands on
|
||||
`/chat/waiting-targeted/<mitraId>` ("Menunggu bestie tertentu" with
|
||||
20s overlay).
|
||||
9. [ ] **[mitra_app]** Accept the incoming targeted request.
|
||||
10. [ ] **[C]** Customer lands on `/chat/session/:id` (S10 Chat Room) —
|
||||
WS open, session timer running.
|
||||
|
||||
**Expected result**
|
||||
- [ ] **[BE]** `payment_sessions` row has
|
||||
`targeted_mitra_id = <test mitra id>` and `status = 'confirmed'`.
|
||||
- [ ] **[BE]** `chat_sessions` row created with the same `mitra_id`; no
|
||||
blast log entries.
|
||||
- [ ] **[C]** Chat opens against the original mitra; no fallback to
|
||||
`/chat/searching`.
|
||||
|
||||
**Notes / known gaps**
|
||||
- Maestro flow `client_app/.maestro/flows/10_returning_repays.yaml` was
|
||||
written against the pre-Stage-5.1 screen graph and needs a rewrite —
|
||||
its selectors target the deleted legacy `/payment` screen
|
||||
(`Chat lagi dengan <mitraName>` app-bar title, `MENUNGGU JAWABAN`
|
||||
intermediate). When automating, rewrite this flow to walk the new
|
||||
multi-screen path described above.
|
||||
- Stage 5.4 (2026-05-18) deleted the legacy `/payment` route +
|
||||
`payment_screen.dart`. Any selector still expecting the legacy app-bar
|
||||
title is stale.
|
||||
|
||||
---
|
||||
|
||||
## TS-02 — Returning user picks offline bestie, "cari bestie lain" → blast
|
||||
|
||||
**Flow:** §4 `Choice → "bestie yang udah kenal" → CheckOnline (no) → OfflinePopup (pre-pay) → "cari bestie lain" → PickMethod → … → paid → PairRoute (baru) → BlastFlow → S10`
|
||||
|
||||
**Affects:** `client_app`, `backend`.
|
||||
|
||||
**Goal:** Verify the `BestieOfflineVariant.prePayReturning` popup fires
|
||||
when the picked bestie is offline pre-payment, and that "cari bestie lain"
|
||||
routes through a fresh blast-payment flow with the targeted intent cleared.
|
||||
|
||||
**Pre-reqs**
|
||||
- [ ] **[BE]** Test mitra from customer's history is **offline** (signed
|
||||
out or heartbeat expired — row shows no `ONLINE` pill in history list).
|
||||
- [ ] **[BE]** At least one OTHER mitra is online (so the blast can match).
|
||||
- [ ] **[BE]** Free-trial OFF.
|
||||
|
||||
**Steps**
|
||||
1. [ ] **[C]** From home, tap `curhat sama bestie baru` → Bestie Choice
|
||||
Sheet.
|
||||
2. [ ] **[C]** Tap `bestie yang udah kenal` → history list; the test mitra
|
||||
row is **dimmed** (offline styling preserved as of Stage 5.3).
|
||||
3. [ ] **[C]** Tap the dimmed row → `BestieOfflinePopup`
|
||||
(`prePayReturning` variant) appears showing the mitra's name. Two
|
||||
CTAs: `cari bestie lain` and `tanya admin`.
|
||||
4. [ ] **[C]** Tap `cari bestie lain` → popup closes; app navigates to
|
||||
`/payment/entry`. Payment draft has been `reset()` (no stale
|
||||
`targetedMitraId`).
|
||||
5. [ ] **[C]** Walk PickMethod → PickDuration → PayMethod → Pay →
|
||||
`/payment/waiting`.
|
||||
6. [ ] **[BE]** Manually confirm payment.
|
||||
7. [ ] **[C]** App routes to `/chat/searching` (NOT
|
||||
`/chat/waiting-targeted/...`).
|
||||
8. [ ] **[mitra_app]** A different online mitra receives the blast and
|
||||
accepts.
|
||||
9. [ ] **[C]** Customer lands on `/chat/session/:id` with the new mitra.
|
||||
|
||||
**Expected result**
|
||||
- [ ] **[BE]** `payment_sessions` row has `targeted_mitra_id IS NULL`
|
||||
(draft was reset before push to `/payment/entry`).
|
||||
- [ ] **[C]** Chat opens with the fallback mitra, not the original
|
||||
offline one.
|
||||
|
||||
---
|
||||
|
||||
## TS-03 — Returning user picks offline bestie, "tanya admin" (escape)
|
||||
|
||||
**Flow:** §4 `Choice → "bestie yang udah kenal" → CheckOnline (no) → OfflinePopup (pre-pay) → "tanya admin" → AdminSheet (terminal)`
|
||||
|
||||
**Affects:** `client_app`.
|
||||
|
||||
**Goal:** Confirm the escape hatch — the user can leave the offline-popup
|
||||
flow without paying by tapping "tanya admin", and no payment row is
|
||||
created.
|
||||
|
||||
**Pre-reqs**
|
||||
- [ ] Same as TS-02 (offline test mitra in customer's history).
|
||||
|
||||
**Steps**
|
||||
1. [ ] **[C]** Reach the `BestieOfflinePopup` (`prePayReturning` variant)
|
||||
via TS-02 steps 1-3.
|
||||
2. [ ] **[C]** Tap `tanya admin` → popup closes; admin sheet opens with
|
||||
WhatsApp / Telegram contact options.
|
||||
3. [ ] **[C]** Dismiss the admin sheet → user returns to the bestie
|
||||
history list.
|
||||
|
||||
**Expected result**
|
||||
- [ ] **[BE]** No new `payment_sessions` row created during this scenario.
|
||||
- [ ] **[C]** Payment draft state unchanged (no `targetedMitraId`, no
|
||||
`paymentId`). User can re-enter the flow normally afterward.
|
||||
|
||||
---
|
||||
|
||||
## TS-04 — Returning user picks "bestie baru" → blast happy path
|
||||
|
||||
**Flow:** §4 `Choice → "bestie baru" → PickMethod → … → paid → PairRoute (baru) → BlastFlow → S10`
|
||||
|
||||
**Affects:** `client_app`, `backend`.
|
||||
|
||||
**Goal:** Confirm the "bestie baru" branch routes through payment FIRST,
|
||||
then blasts to all online mitras (no targeting).
|
||||
|
||||
**Pre-reqs**
|
||||
- [ ] **[BE]** At least one online mitra (for blast match).
|
||||
- [ ] **[BE]** Free-trial OFF.
|
||||
- [ ] **[C]** Returning customer (has session history → Bestie Choice
|
||||
Sheet renders both options).
|
||||
|
||||
**Steps**
|
||||
1. [ ] **[C]** From home, tap `curhat sama bestie baru` → Bestie Choice
|
||||
Sheet.
|
||||
2. [ ] **[C]** Tap `bestie baru` → app navigates to `/payment/entry`.
|
||||
Draft is explicitly `reset()` on this branch (clears any stale
|
||||
`targetedMitraId` per Stage 5.1 Risk #4 mitigation).
|
||||
3. [ ] **[C]** Walk PickMethod → PickDuration → PayMethod → Pay →
|
||||
`/payment/waiting`.
|
||||
4. [ ] **[BE]** Confirm payment.
|
||||
5. [ ] **[C]** App routes to `/chat/searching` (NOT
|
||||
`/chat/waiting-targeted/...`).
|
||||
6. [ ] **[mitra_app]** An online mitra accepts the blast.
|
||||
7. [ ] **[C]** Customer lands on `/chat/session/:id`.
|
||||
|
||||
**Expected result**
|
||||
- [ ] **[BE]** `payment_sessions` row has `targeted_mitra_id IS NULL`.
|
||||
- [ ] **[C]** Searching screen shows briefly; chat opens against whichever
|
||||
mitra accepted.
|
||||
|
||||
---
|
||||
|
||||
## TS-05 — QRIS payment expired → retry preserves targeting
|
||||
|
||||
**Flow:** §4 `PickMethod → … → WaitPay → PayStat (timeout 20 min) → PayExpired → Pay (retry) → paid → PairRoute (lama) → Targeted → S10`
|
||||
|
||||
**Affects:** `client_app`, `backend`.
|
||||
|
||||
**Goal:** Verify the QRIS 20-min expired retry path works for a returning
|
||||
targeted attempt. The `targetedMitraId` on the draft must survive the
|
||||
retry (no need to re-pick mitra or duration) — this is the
|
||||
`resetExceptTarget` invariant from Stage 5.1.
|
||||
|
||||
**Pre-reqs**
|
||||
- [ ] **[BE]** Backend reachable; online test mitra (from customer's
|
||||
history).
|
||||
- [ ] **[BE]** Either the sweeper marks `pending → expired` after 20 min,
|
||||
or the test uses a shortened TTL / direct `UPDATE` to force expiry.
|
||||
|
||||
**Steps**
|
||||
1. [ ] **[C]** Walk TS-01 steps 1-6 to reach `/payment/waiting` for a
|
||||
targeted attempt against the test mitra.
|
||||
2. [ ] **[BE]** Wait for or force the `pending → expired` transition on
|
||||
the payment row.
|
||||
3. [ ] **[C]** Polling sees `status = 'expired'` → app routes to
|
||||
`/payment/expired/:paymentId`.
|
||||
4. [ ] **[C]** Tap the retry CTA → app routes back to `/payment/method`
|
||||
(NOT all the way to PickMethod; draft preserved via
|
||||
`resetExceptTarget`).
|
||||
5. [ ] **[C]** Re-pick payment method → tap `Bayar` → new
|
||||
`/payment/waiting`.
|
||||
6. [ ] **[BE]** Confirm the new payment.
|
||||
7. [ ] **[C]** App routes to `/chat/waiting-targeted/<mitraId>` for the
|
||||
**same mitra** as step 1 (no re-pick required).
|
||||
|
||||
**Expected result**
|
||||
- [ ] **[BE]** Original `payment_sessions` row has `status = 'expired'`.
|
||||
**New** row created with `status = 'confirmed'`. Both rows have the
|
||||
same `targeted_mitra_id`.
|
||||
- [ ] **[C]** Targeted intent survives retry; chat opens with the
|
||||
original picked mitra.
|
||||
|
||||
**Variant note:** the same retry path applies to the blast branch (TS-02 /
|
||||
TS-04) — draft has `targetedMitraId IS NULL` throughout, retry routes
|
||||
back to `/payment/method`, blast fires after re-confirm. Worth a quick
|
||||
sanity check if behavior diverges.
|
||||
|
||||
---
|
||||
|
||||
## TS-06 — Targeted request fails post-payment → fallback to blast
|
||||
|
||||
**Flow:** §4 `Targeted → TargetedRes (reject / timeout) → OfflinePopup (post-pay, returning variant) → "cari bestie lain" → fallback-to-blast → §3 BlastFlow → S10`
|
||||
|
||||
**Affects:** `client_app`, `backend`.
|
||||
|
||||
**Goal:** Verify the post-payment fallback path. After paying for a
|
||||
targeted pair, if the picked mitra rejects or doesn't answer within 20s,
|
||||
the customer can fall back to blast WITHOUT a second payment.
|
||||
|
||||
**Pre-reqs**
|
||||
- [ ] **[BE]** Online test mitra (from history) AND at least one OTHER
|
||||
online mitra (for the blast fallback).
|
||||
|
||||
**Steps**
|
||||
1. [ ] **[C]** Walk TS-01 steps 1-8 to reach
|
||||
`/chat/waiting-targeted/<mitraId>`.
|
||||
2. [ ] **[mitra_app]** Reject the incoming targeted request (or do
|
||||
nothing for the 20s countdown).
|
||||
3. [ ] **[C]** Targeted-waiting screen detects the failure →
|
||||
`BestieOfflinePopup` (`returning` variant, post-pay) appears with
|
||||
`canFallbackToBlast: true`. CTAs: `cari bestie lain` and `tanya admin`.
|
||||
4. [ ] **[C]** Tap `cari bestie lain` → app calls
|
||||
`POST /api/client/chat/chat-requests/:paymentSessionId/fallback-to-blast`
|
||||
→ routes to `/chat/searching`.
|
||||
5. [ ] **[mitra_app]** A DIFFERENT online mitra accepts the blast.
|
||||
6. [ ] **[C]** Customer lands on `/chat/session/:id` with the new mitra.
|
||||
|
||||
**Expected result**
|
||||
- [ ] **[BE]** Same `payment_sessions` row is reused (still
|
||||
`status = 'confirmed'`); customer is **not** charged a second time.
|
||||
- [ ] **[BE]** `chat_sessions` row created with the fallback mitra
|
||||
(NOT the original `targeted_mitra_id`).
|
||||
- [ ] **[C]** Chat opens with the fallback mitra; no fresh payment
|
||||
screens shown.
|
||||
|
||||
**Variant note:** the "tanya admin" CTA on this same popup is a terminal
|
||||
escape (same shape as TS-03), but post-payment — the customer has already
|
||||
paid, so this is effectively abandoning a paid session. Worth confirming
|
||||
the UX (probably a confirmation prompt) and whether the payment is
|
||||
refunded / converted to credit.
|
||||
|
||||
Reference in New Issue
Block a user