10 KiB
Flow — onboarding-nurse
Last verified: 2026-08-02 against commit
c841bde
Actor(s): nurse · Status: partial Client: partial · Server: real Business source: product/business/01-actors-and-onboarding.md Integration: nurse.md · profiles.md
What it does
A phone-verified account becomes a listable nurse: pick the nurse role, write the profile families will read, register the IBAN that earnings land in, and watch one checklist say what is still missing before the listing goes live. Verification itself is a separate journey — nurse-verification.md; this flow is everything around it.
Screens
| Step | Route | Component / notes |
|---|---|---|
| 0 | /fa/select-role |
SelectRole — only customer/nurse offered. Reached programmatically (resolveRoleDestination on an empty roles), never linked |
| 1 | /fa/nurse |
thin RSC → NurseDashboardScreen → DashboardActivationSlot → the shared ActivationChecklist |
| 2 | /fa/nurse/profile |
client page.tsx; react-hook-form, three FormSections (معرفی / تجربه و تحصیلات / تخصصها) + avatar upload + a beforeunload guard on a staged-but-unsaved avatar |
| 3 | /fa/nurse/profile/preview |
«نمایهٔ عمومی من» — the C3 dossier composed from the nurse's own cached queries, so it renders pre-publish with no search-index dependency |
| 4 | /fa/nurse/bank |
an accounts section: submit شبا + holder, watch pending → verified/mismatch, «افزودن حساب دیگر», make-primary |
| — | /fa/nurse/practice, /fa/nurse/services |
the checklist is also mounted above the offerings list (MyServicesList.tsx:92) next to PublishGate |
API
| Call | Endpoint | Notes |
|---|---|---|
| read profile | GET /api/v1/nurse_profiles/me |
404 → null (empty form) via orNull (profiles/apis/clientApi.ts:19-26) |
| save profile | POST /api/v1/nurse_profiles/upsert |
body carries 5 fields only — see the avatarUrl gap |
| avatar | POST /api/v1/nurse_profiles/avatar |
multipart/form-data; clientFetch must not set Content-Type |
| go live | POST /api/v1/nurse_profiles/set_accepting_bookings |
the real switch behind PublishGate; reindexes in-transaction |
| bank list / add / primary | GET nurse_bank_accounts/list, POST .../add, POST .../set_primary/{id} |
add is sensitive 20/min |
| role | POST /api/v1/me/select_role |
RoleNames.SelfAssignable = [customer, nurse] (RoleNames.cs:19) |
Shapes live in profiles.md and nurse.md.
POST nurse_bank_accounts/verify_ownership/{id} is wired server-side and implemented at
nurse/apis/clientApi.ts:30 but no hook or component ever calls it.
Rules that must hold
| Rule | Where enforced |
|---|---|
is_verified is never client-settable. UpsertNurseProfileCommand has no such field; the handler's Apply() writes only bio/years/education×2/specializations. Only the verification finalize transaction flips it (INV-18) |
UpsertNurseProfileCommand.cs, .Handler.cs |
iban_hash is UNIQUE platform-wide — deterministic hash checked in the handler, UNIQUE(iban_hash) as the DB backstop |
AddNurseBankAccountCommand.Handler.cs:36-39 |
Write-then-masked IBAN. Every read model returns ibanMasked (last-4); the full IBAN is never re-served |
nurse.md |
A verified primary IBAN with matchedNationalId == true is the first-payout gate — not a search gate. No account ⇒ the nurse accrues a balance and is skipped with a recorded reason |
payouts.md; product/business/10 §(b) |
is_searchable = is_verified AND is_accepting_bookings AND status != suspended AND variant.is_active (INV-17). Bio and avatar are not in it |
SearchIndexMaintainer.cs:248-249 |
A profile row does not exist until the first upsert; select_role only grants the role |
SelectRoleCommand.Handler.cs |
Bank add requires a nurse profile to already exist ("Create your profile first.") — profile precedes bank |
AddNurseBankAccountCommand.Handler.cs:32-34 |
How to test
Log in as 09120000001 (زهرا عزیزی, verified nurse) — see testing-setup.md.
- Open
/fa/nurse. Expect: the «راهاندازی حساب» card. Actual today: it renders incomplete with «احراز هویت و مدارک» un-passed, because the verification row reads a client mock (see gaps). It never collapses to the green «فعال در جستجو» state for any account. - Open
/fa/nurse/profile. Expect: bio «پرستار سالمند با هشت سال سابقه مراقبت در منزل.», years8, level «کارشناسی», field «پرستاری». A warning card «پروفایل شما هنوز فعال نیست» is shown — also mock-driven, and wrong for this account (GET nurse_profiles/me→isVerified: true). - Edit the bio and save. Expect: a success toast and the value persisted on reload. Verified live:
POST nurse_profiles/upsert→200. - Same call with
"isVerified": false, "isAcceptingBookings": falseinjected into the body. Expect (verified live):200withisVerified: true,isAcceptingBookings: true— the guard holds. - Open
/fa/nurse/bank. Expect: one card, «تأییدشده», «بانک ملت»,••••9012, primary. - Submit
IR820170000000123456789012(nurse 1's own seeded IBAN) via «افزودن حساب دیگر». Expect (verified live):400{"Iban":["This IBAN is already registered."]}and no new row. The UI shows only the generic «ثبت این حساب ممکن نشد…» toast. - Open
/fa/nurse/profile/preview. Expect: the C3 dossier rendered from own data. The avatar renders as a fallback icon, not the photo — see thefile://gap.
Seeded-world limits. Nothing exercises the first-run path: all three demo nurses already have a profile,
and nurses 1–2 already have a verified primary IBAN. To see the empty profile → 404 → blank form, the
"no nurse profile yet" bank rejection, or select-role, you need a new account, which the phone-OTP
login cannot mint for an unseeded phone. 09120000003 (unverified, in_review) is the closest stand-in for
a mid-onboarding nurse; its token in tokens.env returned 401 at this stamp, so its state was not
re-probed live.
Known gaps
- The activation checklist mixes real and mocked truth.
useActivationChecklist.ts:43-47folds four real domains (profiles,catalog,serviceAreas,nurse) with one mocked one (verification,USE_VERIFICATION_MOCK = trueatverification/constants.ts:9). The «احراز هویت و مدارک» row is browser state, not server state. - Every nurse looks unverified on every page load. The verification mock's
stepsstarts[](verification/apis/mockApi.ts:40) andgetStatusreturns that aggregate, soisApproved()isfalsefor the seeded, server-verified nurse 1 — and the module state resets on every reload/HMR. Consequences: the checklist never reaches «فعال در جستجو»;PublishGaterenders the blocked branch and hides the realset_accepting_bookingsCTA;/fa/nurse/profileshows a false «پروفایل شما هنوز فعال نیست» banner. isSearchVisibleis not the server's gate, despite the component saying it is.useActivationChecklist.ts:35-41claims the four rows "are exactly the server'sis_searchablegate". They are not: theprofilerow (bio.trim() !== '' && avatarUrl != null,:31-33) is client-invented and the server never reads it, whileis_accepting_bookings— which is in the gate — is excluded fromsearchRowsand tracked separately. A nurse can pass all four rows and still be invisible.- The avatar URL is unrenderable on the default seam.
LocalDiskObjectStorage.GetUrlreturnsnew Uri(localPath).AbsoluteUri(LocalDiskObjectStorage.cs:51), sonurse_profiles/meservesfile:///C:/Users/.../avatars/nurse/1/….png(verified live). A browser cannot loadfile://from anhttp://page — the<Avatar>on/fa/nurse/profileand the preview both fall back to the placeholder icon, while the checklist'sprofilerow counts the non-null URL as passed. avatarUrlon the upsert input is dead on the real path.UpsertNurseProfileInput.avatarUrl(profiles/types.ts:40) is populated by the form (nurse/profile/page.tsx:154) butprofilesClientApi.upsertNurseProfile(profiles/apis/clientApi.ts:85-95) never sends it. Harmless today (the multipart route already persisted it) but it is a mock-era field the real client silently drops.- Duplicate IBAN is
400, not409. nurse.md says "the secondaddreturns a409"; the handler returnsFailureResult→400with a field error (verified live). The integration doc is wrong. - The duplicate-IBAN message never reaches the nurse.
nurse/bank/page.tsx:51toasts the generic «ثبت این حساب ممکن نشد. شبا را بررسی کرده و دوباره تلاش کنید.» and discards the server's{"Iban":["This IBAN is already registered."]}, so a nurse re-entering their own IBAN is told to check it. - The ownership inquiry ignores the national id.
MockBankAccountOwnershipVerifierdecides from the IBAN alone (:21-26) and matches everything exceptSeams:BankOwnership:MismatchIban. A nurse who has not done identity KYC hasusers.national_id = NULL(NurseIdentityContext.cs:9-10) and still getsmatchedNationalId = true— the payout gate opens on a claim nothing checked. verifyOwnershipis a dead seam op on the client. Declared (nurse/types.ts:41), implemented (nurse/apis/clientApi.ts:30), server route live — but no hook exists, so amismatchaccount has no re-inquiry affordance; the UI only offers "re-enter the IBAN"./fa/select-rolecannot be reached with any seeded account — all 8 demo users already hold a role, so the role picker is untestable end-to-end today.- The bank page polls forever on a stuck inquiry.
useNurseBankAccountsrefetches every 2 s while any account ispending(nurse/constants.ts:14) with no ceiling and no timeout copy.