24 KiB
Balinyaar — Manual Testing Plan
The flow-by-flow walkthrough for testing the whole product against the seeded lifecycle demo world. Written after the UI/backend/refinement phase chains completed (2026-07-26). Companion docs:
- Bring-up: refinement/RUNBOOK.md — how to boot API + client, OTP login mechanics.
- Business rules with numbers:
product/business/— what "correct" means; the load-bearing numbers are repeated inline below.
How to use this doc: pick a flow section, log in as the named account, follow the steps, compare against Expected. Anything marked ⚠ known gap is pre-existing and filed (REQ-xxx) — record it as confirmation, not as a new bug. Anything else that deviates is a genuine finding.
1. Environment
| Piece | Where | Notes |
|---|---|---|
| API | http://localhost:5002 (cd server && dotnet run --project src/API/Baya.Web.Api/Baya.Web.Api.csproj) |
Boots in Development: migrates + seeds (reference + demo world + lifecycle world) against the DB in appsettings.Development.json — currently the remote dev SQL Server (87.107.152.16). Swagger at /swagger. |
| Client | http://localhost:3000 (cd client && npm run dev) |
.env.development already points at http://localhost:5002. |
| Crypto keys | dotnet user-secrets (from server/src/API/Baya.Web.Api) |
The committed config carries SET_VIA_USER_SECRETS_OR_ENV placeholders. The remote DB's data is encrypted under the dev keys — any machine booting against it must set the same four user-secrets: IdentitySettings:SecretKey = dev-only-jwe-signing-key-not-for-production-0123456789abcdef, IdentitySettings:Encryptkey = dev-only-16bytes, Seams:FieldEncryption:Key = local-dev-field-encryption-key-not-for-production, Seams:FieldEncryption:HashKey = local-dev-field-hash-key-not-for-production. Booting with different field keys makes every phone lookup miss and every PII read throw Padding is invalid. |
| OTP codes | server console (MOCK SMS — OTP code …) or GET /api/v1/dev/last_otp/{phone} |
Resend window 120 s, 5 wrong attempts per code, OTP valid 60 s. The OTP endpoints are rate-limited per IP — hammering logins in scripts will 429. |
| Reset | drop the Baya DB (dotnet ef database drop --force --project src/Infrastructure/Baya.Infrastructure.Persistence --startup-project src/API/Baya.Web.Api), reboot |
Everything reseeds. Needed when the time-relative scenarios age out (see §3 note). |
2. Test accounts
All log in via phone-OTP at /login. Any listed account works immediately.
| Phone | Actor | Who | Carries |
|---|---|---|---|
09120000001 |
nurse (verified) | زهرا عزیزی | 4 variants (incl. the 5-session package), whole-city+2-district coverage, ★5.00 (1 published review), 3 completed bookings, a pending request in the inbox, the in-progress visit today, earnings incl. a paid payout and a pending clawback |
09120000002 |
nurse (verified) | علی کریمی | Partner-sponsored (مرکز پرستاری آرامش), 2 completed bookings (one BNPL), a draft payout waiting, a hidden 1★ review, an expired request |
09120000003 |
nurse (unverified) | مریم احمدی | Mid-pipeline verification (KYC+Shahkar passed, MoH license in review w/ document, criminal record pending) — never searchable |
09120000010 |
customer | سارا محمدی | 2 patients, 1 address, 6 bookings across all states, pending/accepted/rejected requests, tickets, notifications, care records |
09120000011 |
customer | رضا حسینی | 1 infant patient, 2 completed bookings (one BNPL w/ processing refund), expired + cancelled requests, a closed support ticket |
09120000020 |
admin (super_admin) |
نگار مدیری | Full /admin console incl. RBAC |
09120000021 |
admin (finance) |
کامران مالی | /admin scoped to money consoles (useAdminCapabilities) |
09120000030 |
partner owner | بهنام رستگار | Owns the merchant-of-record center; navigate to /partner manually after login (⚠ REQ-038 — no /me partner signal) |
any new 09xxxxxxxxx |
fresh user | — | Registers on first OTP verify → /select-role → onboarding. Use for first-run testing. |
3. The seeded world at a glance
Booking scenarios (all Tehran; seed-time-relative — T0 = the boot that first seeded):
| Key | Who | State | Money / attachments |
|---|---|---|---|
| pending request | محمدی → عزیزی | pending_nurse_response, 24 h countdown |
nurse-inbox + C5 tracker fodder |
| accepted request | محمدی → عزیزی | accepted_awaiting_payment (24 h demo window, not the real 30 min) |
checkout entry; 1 failed card attempt (wallet history) |
| rejected / expired / cancelled requests | mixed | terminal request states | C5 terminal cards + inbox tabs |
B1 upcoming |
محمدی × عزیزی | confirmed, scheduled T0+3d, care instructions written | paid (card), invoice issued, coordination ticket |
B2 in_progress |
محمدی × عزیزی | 5-session package: 2 done, session 3 checked-in now, 2 scheduled | paid, emergency ticket logged |
B3 completed_in_window |
محمدی × عزیزی | completed T0−1d, dispute window open until T0+2d | review pending moderation, care record |
B4 completed_paid |
محمدی × عزیزی | completed T0−10d, paid out | ★5 published review (punctual/professional), care record |
B5 completed_eligible |
حسینی × کریمی | completed T0−5d, in the draft batch | hidden 1★ review, out-of-range EVV (1220 m) |
B6 bnpl_settled |
حسینی × کریمی | completed T0−6d, paid via SnappPay (net-of-fee) | processing bnpl_revert 50% refund (ETA ~10 business days), excluded from payouts, care record |
B7 cancelled_refunded |
محمدی × عزیزی | cancelled ≥24 h ahead (standard_24h, 100%) | succeeded card refund |
C8 clawback |
محمدی × عزیزی | completed T0−11d, paid out, then fully refunded | pending nurse clawback, support alert |
Plus: 9+ tickets (coordination per booking, 3 refund tickets, open support w/ an internal admin note, closed support, emergency), notifications for every persona (mixed read/unread), 4 support alerts (low-rating / EVV-mismatch / clawback / emergency), a paid + a draft payout batch, invoices for all 8 bookings (کریمی's resolve to the partner center as merchant-of-record).
Time decay: the world is date-relative and ages. After ~24 h the pending request expires (the sweep runs every minute); after ~2 days B3's dispute window closes and the "review-eligible + in-window" state is gone; the accepted request's payment window lasts 24 h. To refresh the timeline: drop the DB and reboot (seeder guards make an in-place re-run a no-op, not a re-date).
4. Mock-vs-real map (critical for honest testing)
15 of 22 client domains hit the real API. 7 still run on in-browser mocks (USE_*_MOCK = true in
client/src/services/{domain}/constants.ts) because a blocking backend REQ is deferred. On those screens
the UI shows mock data regardless of the seeded DB — test the UI/UX there, and verify the server
data through Swagger instead.
| Mocked domain | Screens affected | Server data still verifiable via |
|---|---|---|
verification |
nurse /nurse/verification/* journey, admin /admin/verification queue |
GET nurse_verification/status (nurse token), Swagger |
refunds |
customer cancel flow + /bookings/[id]/refund_status, wallet «استردادها» |
GET /api/v1/refunds/by_booking/{id}, GET /api/v1/admin_refunds |
payouts |
nurse /nurse/earnings*, admin /admin/payouts |
`GET /api/v1/nurse_payouts/earnings_balance |
admin |
`/admin/config | holidays |
bnpl |
the whole D1–D5 BNPL checkout + wallet «اقساط» | GET /api/v1/admin_bnpl/{id} |
partnerCenter |
/partner/* portal + /admin/partners |
GET /api/v1/admin/partner-centers |
patientRecords |
E2 family care record editor + nurse visit-note panel | GET /api/v1/patients/{id}/care_records |
Real domains (UI = server truth): auth, geography, patients, profiles, nurse-bank, addresses, serviceAreas, catalog, search, bookingRequests, bookings, payment, reviews (incl. admin moderation), tickets (incl. admin ticket console), notifications.
5. Flow walkthroughs
5.1 Auth & first-run (real)
| # | Steps | Expected |
|---|---|---|
| A1 | /login → fresh phone → request code → wrong code ×5 |
Refused after 5 attempts until a fresh code; resend locked 120 s; no reveal whether the phone exists |
| A2 | Fresh phone, correct code | → /select-role (no roles yet) → pick خانواده → /onboarding wizard (relation → first patient) → customer home |
| A3 | Same phone again later | Straight to customer home (role remembered) |
| A4 | Nurse 09120000001 login |
Routed to /nurse dashboard (never a flash of the customer shell) |
| A5 | Admin 09120000020 vs 09120000021 |
Both land /admin; finance sees only money consoles in the sidebar |
| A6 | Deep link while logged out (e.g. /bookings) |
→ /login?next=/bookings → after login lands back on /bookings |
| A7 | Logged out, open / |
Public landing (/welcome content, URL stays /) — ⚠ verify via npm run build && npm start (dev-server root-path quirk, ui-phase-13) |
5.2 Customer — discovery & booking request (real)
Login: 09120000010.
| # | Steps | Expected |
|---|---|---|
| C1 | Home → search entry → category سالمند, region Tehran/whole city | Live count CTA > 0; unverified مریم احمدی never appears in any result |
| C2 | Results | عزیزی shows ★5.00 (1 نظر), 3 visits, tappable verified badge; district-3 search still shows her whole-city rows |
| C3 | Gender filter مرد | Only کریمی remains |
| C4 | Open عزیزی's profile | Trust dossier: badge → verification panel (credential types only, never numbers); services/prices tab; «نظرات» tab shows exactly 1 published review with tags — the pending + hidden ones never |
| C5 | «درخواست رزرو» → C4 form: patient حسن, address, tomorrow, morning window → submit | Request created; C5 tracker shows a 24 h countdown ring |
| C6 | As عزیزی (second browser/profile): /nurse/requests |
New request at top with urgency countdown; detail shows only customer notes + coarse city/district (⚠ stage-1 masked address) |
| C7 | Accept it | Customer C5 flips to accepted + 30-minute payment window countdown (real window — the seeded one is a 24 h demo exception) |
| C8 | Reject another (with reason) | C5 terminal card + «درخواست دوباره» / «پرستاران مشابه» off-ramps |
5.3 Customer — checkout & payment (real contract, mock PSP)
| # | Steps | Expected |
|---|---|---|
| P1 | C5 of the seeded accepted request → «پرداخت» → C6 | Nurse identity + <Money> total (variant price), reconciling breakdown, escrow explainer. ⚠ REQ-046: avatar/badge enrichment may be absent on the real path |
| P2 | «ادامه پرداخت» (card) | Initiate succeeds → browser redirects to https://mock-psp.local/... — a dead page. Expected: the mock PSP has no UI (real E2E completion needs the Phase-8 real PSP config). |
| P3 | To actually complete a payment: Swagger → POST /api/v1/bookings/convert body {"bookingRequestId": <id>} with the customer's bearer token (Dev-only capture simulator) |
Booking appears under «رزروها» as confirmed; coordination ticket auto-opens; invoice issued; nurse gets a notification |
| P4 | Wallet «پرداختها» | ⚠ REQ-047: empty on the real path (no transactions-list endpoint). The seeded failed+succeeded transactions are visible in DB/Swagger only |
| P5 | BNPL branch (D1–D5) | ⚠ Whole flow is client-mock (REQ-022/024); /bookings/checkout/bnpl/gateway only exists under npm run dev. Server-side BNPL truth: admin_bnpl via Swagger (B6 is reverted) |
5.4 Customer — bookings lifecycle (real)
| # | Steps | Expected |
|---|---|---|
| L1 | /bookings tabs |
«در انتظار پاسخ»: pending request w/ countdown · «فعال»: B1, B2 · «گذشته»: B3, B4, B7, C8 |
| L2 | B1 (upcoming) detail | Status timeline at تایید شده; next-session headline T0+3d; .ics download; address card |
| L3 | B2 (package) detail | 5 sessions listed: 2 completed, 1 در حال ویزیت (presence headline), 2 scheduled |
| L4 | B3 review CTA | Star strip on the completed row → review form (context recap + moderation note) — but B3 already carries a pending review, so expect the "under review" state, never a second form |
| L5 | B4 | «مشاهده نظر» shows the published ★5 review state |
| L6 | B1 → لغو رزرو | Off-ramps first; tier disclosure: ≥48 h → 100%، ≥24 h → 50%، <24 h → 0% (server tiers) — ⚠ the UI copy states a single 24 h boundary (REQ-065 divergence; record the exact numbers shown). ⚠ the actual cancel + refund-status screens run on the mock refunds domain — server truth via Swagger |
| L7 | B7 → وضعیت استرداد | ⚠ mock domain: UI shows mock stages. Server truth: refund succeeded, channel psp_card (GET refunds/by_booking/{B7}) |
| L8 | /bookings/[B1]/invoice |
Real invoice: sequential number, VAT = 10% on the commission line only, مودیان state pending. ⚠ REQ-049: payment-method/reference/seller-fiscal block may be absent |
5.5 Customer — account, care circle, tickets, notifications
| # | Steps | Expected |
|---|---|---|
| E1 | /patients («حلقهٔ مراقبت») |
حسن + فاطمه cards; add/edit/archive (real); tap → E2 record |
| E2 | /patients/[حسن]/record |
⚠ patientRecords domain is mock: meds/routine/tasks + the nurse visit-note history shown are mock data (the 2 real seeded notes are server-side only). Access-denied state testable with the mock's foreign-patient id 8888 |
| E3 | /addresses |
Real CRUD; map is the canvas stand-in unless NEXT_PUBLIC_NESHAN_KEY is set |
| E4 | /profile |
Identity header (server-masked phone), personal-info edit, language row, emergency-contact card; avatar upload round-trips |
| E5 | /support/tickets |
Real inbox: refund tickets (TKT-DEMORF0x), open support TKT-DEMOSUP1, coordination tickets; thread of TKT-DEMOSUP1 shows customer msg + admin reply + follow-up — the internal admin note must never appear |
| E6 | Send a message; send another containing /fail |
Normal send lands; ⚠ /fail sentinel only drives the mock — on the real path it just sends |
| E7 | /notifications |
Seeded rows, unread-first, day-grouped; mark-read flips the bell count. ⚠ Notification titles/bodies are English — server-rendered copy from the real handlers (finding F-4 §7) |
5.6 Nurse — daily ops (real except earnings)
Login: 09120000001.
| # | Steps | Expected |
|---|---|---|
| N1 | /nurse dashboard |
Next-visit card (B2 today), requests strip (pending w/ countdown), earnings snapshot (⚠ mock numbers — payouts domain), activation checklist collapsed to «فعال در جستجو» |
| N2 | /nurse/visits |
Today's feed shows B2 session 3 checked-in → the check-out CTA is the hero action |
| N3 | Check out (confirm step) | Session completes; EVV panel updates. GPS: set NEXT_PUBLIC_EVV_MOCK_GPS=in_range in client/.env.local to force a passing location, else real geolocation runs (out-of-tolerance is advisory — banner + staff alert, never a block). Completing the last session would complete the booking and start its 72 h dispute clock |
| N4 | /nurse/visits/[B2] |
Address card + map link (⚠ REQ-051: address may stay masked w/ quiet fallback note), gated care card (care instructions readable post-confirmation), visit-note composer (⚠ mock — patientRecords) |
| N5 | /nurse/requests |
Tabs: pending (live countdown) / answered / expired. Accept behind confirm dialog → opens the payment window |
| N6 | /nurse/earnings + /payouts |
⚠ Mock domain — UI shows mock buckets. Server truth (Swagger, nurse token): earnings_balance = signed ledger sum (عزیزی: B1+B2+B3 payouts pending — B7 reversed, B4/C8 paid out), history shows the paid payout w/ masked IBAN |
| N7 | /nurse/services |
4 offerings incl. the package variant; edit/deactivate; the accepting-bookings toggle is real (pausing → disappears from search) |
| N8 | /nurse/coverage |
Whole-city + districts 1&3; duplicate add → 409 message |
| N9 | /nurse/bank |
Verified account, masked IBAN, ownership state «تأیید شده» |
| N10 | As کریمی (09120000002) |
Draft-batch earnings waiting; B6's payout share reversed by the 50% refund; hidden review does not appear on his public profile (0 reviews) |
| N11 | As احمدی (09120000003) |
Activation checklist far from complete; /nurse/verification — ⚠ mock domain: the journey UI shows mock state; the real mid-pipeline case (KYC ✓, Shahkar ✓, license in-review, criminal pending) is server-side (nurse_verification/status via Swagger). She must never appear in search |
5.7 Admin console
Login: 09120000020 (all consoles) and 09120000021 (money only).
| # | Console | Real/Mock | What the seeded world provides (server-side) |
|---|---|---|---|
| AD1 | /admin/reviews moderation queue |
Real | B3's pending review — publish it → عزیزی's public aggregate recomputes (★ count 2); hide/reject paths; the hidden 1★ shows the hide flow already happened |
| AD2 | /admin/tickets |
Real | Full queue incl. emergency (red) + refund tickets; thread of TKT-DEMOSUP1 shows the internal note distinctly; composer's internal-note mode goes amber |
| AD3 | /admin/verification |
⚠ Mock | Real queue data (احمدی in review + document metadata) via Swagger admin_verifications; the document's signed-URL fetch 404s (no real bytes — expected) |
| AD4 | /admin/payouts |
⚠ Mock | Real: admin_payouts/batches shows the completed batch (2 links) + the draft batch; batches/{id}/process on the draft is the demoable money-out action; eligible preview |
| AD5 | Refunds (via ticket RefundPanel) | ⚠ Mock | Real: admin_refunds lists 3 refunds; {id}/confirm_settlement on the processing BNPL refund is the demoable action (posts the clearing leg) |
| AD6 | /admin/alerts |
⚠ Mock | Real: 4 seeded alerts (low_rating / evv_location_mismatch / nurse_clawback / emergency) via support_alerts endpoints |
| AD7 | /admin/config, /admin/holidays, /admin/audit, /admin/users, /admin/roles |
⚠ Mock | Real: b1 endpoints (config list incl. the §6 numbers, audit trail rows for every seeded money/trust entity — actor null = system) |
| AD8 | /admin/partners |
⚠ Mock | Real: admin/partner-centers shows مرکز پرستاری آرامش (MoR, verified, masked IBAN), کریمی sponsored |
5.8 Partner portal (⚠ mock — REQ-032/033/038)
Login 09120000030 → navigate to /partner manually. UI runs on mock center data; the real center +
MoR invoice resolution (کریمی's two invoices carry issuing_entity_type = partner_center) is verifiable
via Swagger. Test the portal chrome, the MoR settlement view, and the CSV export against mock data.
6. The numbers the UI must respect
From seeded platform_configs (admins can change them; rates are snapshotted — a change never
re-prices an existing booking/invoice/deadline):
| Rule | Value |
|---|---|
| Nurse response deadline | 24 h (frozen per request) |
| Payment window after accept | 30 min (frozen per request) |
| Dispute window after completion | 72 h → then payout-eligible |
| Platform commission | 15% of gross (gross = commission + payout, always) |
| VAT | 10%, on the commission line only |
| Cancellation tiers | ≥48 h → 100% · ≥24 h → 50% · <24 h → 0% (⚠ client copy says a single 24 h boundary — REQ-065) |
| BNPL provider fee | 7% — a platform expense; the nurse payout is payment-method-invariant |
| BNPL refund ETA | ~10 business days (Fridays skipped); UI copy says 7–10 |
| EVV tolerance | 200 m, advisory only |
| No-show threshold | 60 min after scheduled start |
| Payout cadence | weekly generation (auto, draft); processing stays a manual admin action |
| Review alert | rating ≤ 2 raises a staff alert |
| OTP | resend 120 s · 5 attempts · code lives 60 s · session 30 d · access token 60 min |
7. Findings from this bring-up (decide / track)
Recorded while building the seed — each needs an owner decision:
| # | Finding | State |
|---|---|---|
| F-1 | Committed real DB credential: appsettings.Development.json ships the remote server's SQL login in git (the exact thing refinement-phase-5 removed; the pre-commit hook that blocks it isn't enabled on this clone). Recommendation: move the connection strings to user-secrets like the crypto keys and restore placeholders. |
Open — decision needed |
| F-2 | /me hardcoded hasCustomerProfile/hasNurseProfile/nurseVerificationStatus to false/not_started for everyone (broke the customer home profile nudge + profile-hook invalidations). |
Fixed this session (snapshot projection + ToMeResult) |
| F-3 | Stored Persian text is normalized destructively: an ApplicationDbContext save hook runs Fa2En().FixPersianChars() over every string property — Persian digits become ASCII and ZWNJ becomes a space in everything persisted (e.g. «شبانهروزی» → «شبانه روزی»), contradicting the client's STYLE.md ZWNJ rules. Any Persian-literal equality against stored data silently misses. |
Open — decision needed (legacy starter behavior; removing it changes stored data shape) |
| F-4 | Notification copy is English on the real path — the b-phase handlers write "Booking confirmed"-style titles/bodies straight into ops.Notifications; the Persian UI renders them verbatim. |
Open — decision needed (server-side i18n or client-side type→copy mapping) |
| F-5 | Mock PSP redirect goes to a dead host (mock-psp.local) — in-browser card completion impossible until a real PSP is configured (Payments:Provider=zarinpal); the Dev bookings/convert affordance is the workaround (§5.3 P3). |
By design (refinement-phase-8 rails exist behind config) |
| F-6 | 7 client domains still mocked (§4) — the biggest lever for "test everything for real" is flipping verification, refunds, payouts (user halves are contract-ready; the blocking REQs are admin-side). Flipping a shared flag turns the admin half's screens into real-API screens against missing endpoints. |
Open — decision needed (split flags per half, or deliver the admin REQs) |
| F-7 | Seeded object-storage keys (verification document, invoice PDFs) have no real bytes — signed-URL fetches 404 in admin viewers. | Accepted for dev (never fake bytes) |
8. Suggested test order
- Auth & routing (§5.1) — everything depends on it.
- Customer discovery → request → accept → convert → booking (§5.2–5.4) — the core funnel, all real.
- Nurse ops (§5.6) — inbox/EVV against the same bookings.
- Reviews + tickets + notifications (§5.4 L4/L5, §5.5 E5–E7) — real, seeded both sides.
- Admin real consoles (§5.7 AD1/AD2) — moderation + tickets.
- Mock-domain UI passes (§5.3 P5, §5.5 E2, §5.6 N6/N11, §5.7 AD3–AD8, §5.8) — UX only, verify server truth via Swagger as listed.
- Sweep §7's decisions.