Files
baya-monorepo/archive/docs/flows/onboarding-customer.md
T
2026-08-02 20:01:31 +03:30

11 KiB
Raw Blame History

Flow — onboarding-customer

Last verified: 2026-08-02 against commit c841bde

Actor(s): customer (a family member / payer) · Status: partial Client: real · Server: real Business source: product/business/01-actors-and-onboarding.md Integration: profiles.md · auth.md · patients.md

What it does

A phone that has just verified an OTP holds a session with no public role. This flow is everything between that moment and a usable family app: pick "I need care" vs "I am a nurse", register the first person you arrange care for, and — later, prompted by a nudge on Home — fill in your own payer details and the emergency contact a booking falls back on.

The customer's own identity KYC is deliberately not part of it: a customer registers and browses on a verified phone alone (business/01 §(a)).

Screens

Step Route Component / notes
0 — route (none) RoleRouter runs after verify_otp. resolveRoleDestination (routing.ts:33) sends roles == [] to /select-role, carrying ?role=nurse when that was the login intent
1 — role /fa/select-role SelectRole.tsx. Two radio cards, «خانواده» / «پرستار»; admin is never offered. FocusedLayout, no RoleGuard — resolving the role is the page's job
2 — gate /fa HomeScreen.tsx:90-94total === 0router.replace('/onboarding'). Waits for a settled list so a post-create refetch can't bounce back
3 — welcome /fa/onboarding OnboardingScreen.tsx phase welcome — brand moment, one CTA. Not a stepper step
4 — relation /fa/onboarding phase relation — «مراقبت برای چه کسی است؟» RelationSelect over parent/spouse/child/self, one glyph each
5 — first patient /fa/onboarding phase patientPatientForm with relation pre-set and hidden. On success → router.replace('/')
6 — payer details /fa/profile Customer account hub. Reached from Home's nudge_profile card (HomeScreen.tsx:155-163), shown while me.hasCustomerProfile === false. Three FormDialogShell sheets over one react-hook-form — personal (نام/نام خانوادگی), language, emergency contact

(customer-focused) is a chrome-free route group: same URL space, FocusedLayout strips the bottom nav so the user cannot tab away mid-setup. It still carries RoleGuard expected=customer; /select-role does not.

API

Call Endpoint Notes
commit role POST /api/v1/me/select_role clientApi.ts:56MeController.cs:30SelectRoleCommandHandler. Live: 200 re-selecting customer; 403 for super_admin
token rotation POST /api/v1/auth/refresh useSelectRole.ts:26-33 rotates immediately after — role claims live inside the JWE, so without it the next gated call carries the stale claim. A failed rotation is swallowed by design
identity GET /api/v1/me the only identity source. Live for …010: roles:["customer"], hasCustomerProfile:true, masked 0912*****10
first patient POST /api/v1/patients/create useCreatePatient.ts splices the row into every cached list before invalidating, so there is no transient "0 patients" window
read profile GET /api/v1/customer_profiles/me 404 ⇒ null, not an error (clientApi.ts:19-26) — a first-run customer sees an empty form. Live: 404 for a nurse token, 200 for …010
write profile POST /api/v1/customer_profiles/upsert 403 for a non-customer (verified live). Creates on first call, updates after

Shapes belong to docs/integration/domains/; do not restate them here. USE_AUTH_MOCK, USE_PROFILES_MOCK and USE_PATIENTS_MOCK are all false — every call above is real HTTP.

Rules that must hold

Rule Value Source
Self-selectable roles customer, nurse only; any admin sub-role ⇒ 403 business/01 §(a); RoleNames.SelfAssignable, SelectRoleCommand.Handler.cs:23-24
A user may hold both public roles grants are audited via granted_by/granted_at; a revoked grant is re-activated, never duplicated SelectRoleCommand.Handler.cs:44-50
Customer KYC is deferred a customer registers and browses on a verified phone alone; national_id is nurse-only business/01 §(a)/(c)
Patient ≠ customer the self relation still creates a distinct patient row; the customer is never collapsed into the patient business/01 §(a)
Relation enum parent | spouse | child | self (REQ-005, delivered) business/01 §(a)
PII at rest phone, name and the emergency contact are encrypted; /me masks the phone to 0912*****10 INV-21; IFieldEncryptor, IdentityDefaults.MaskPhone
Tenancy the profile is resolved from ICurrentUser, never the body UpsertCustomerProfileCommand.Handler.cs:17-21

No money, no config rate, no deadline is involved in this flow.

How to test

Log in as 09120000010 (سارا محمدی, customer) — see testing-setup.md for boot, the OTP and the account list.

All 8 demo accounts are already onboarded, so steps 15 cannot be walked with a seeded account. Two options:

A — inspect the finished state (any demo customer).

  1. Log in as 09120000010, land on /fa. Expect: Home renders (2 patients seeded), not a redirect to /fa/onboarding.
  2. Open /fa/profile. Expect: header «سارا محمدی» + 0912*****10; the emergency card shows «بهرام محمدی» / 09121110010. No nudge_profile card on Home, because hasCustomerProfile is true.
  3. Type /fa/select-role directly. Expect: the picker renders (it has no RoleGuard). Choosing «پرستار» adds the nurse role to this account — a real, permanent write to the shared demo DB. Do not do it casually.

B — see the genuine first-run state (a fresh phone).

  1. POST /api/v1/auth/request_otp with an unused Iranian mobile, e.g. 09129990001. The handler creates an inactive shell users row with no role and no PII beyond the encrypted phone (RequestOtpCommand.Handler.cs:39-54) — creation happens before the SMS send, so a 500 from the telegram relay still leaves the account.
  2. GET /api/v1/dev/last_otp/09129990001, then log in at /fa/login. Expect: /me returns roles: []RoleRouter replaces to /fa/select-role.
  3. Pick «خانواده» → expect a 200 from select_role, a silent token rotation, then /fa, which immediately replaces to /fa/onboarding (zero patients).
  4. Welcome → relation → patient. On save expect the «ذخیره شد» toast and a landing on /fa, now with the nudge_profile card visible because hasCustomerProfile is still false.
  5. Tap the nudge → /fa/profile → «اطلاعات شخصی» and the emergency sheet. On save expect GET /me to flip hasCustomerProfile to true and the nudge to disappear.

This creates a permanent shell account on the shared remote demo DB. Prefer a local instance (testing-setup.md) if that matters.

curl on Git Bash mangles Persian in -d. A round-trip upsert sent inline wrote ????? into the DB during this verification (restored). Put the JSON in a UTF-8 file and use --data-binary @file.

Known gaps

  • Onboarding never creates a customer_profile — it creates a patient and stops. hasCustomerProfile stays false and the only path to the payer details is Home's dismissible-free nudge_profile card (HomeScreen.tsx:155-163). Nothing blocks a customer from booking without an emergency contact.
  • POST /api/v1/customer_profiles/avatar is live in swagger and implemented (CustomerProfilesController.cs:37) but no client code calls ituseUploadAvatar is imported only by nurse/profile/page.tsx:21. A customer can never set a photo; ProfileSummary always shows initials.
  • The customer's own gender is unsettable through the app. auth/types.ts:20 says "null until the profile flow (b3) sets it", but UpsertCustomerProfileCommand carries no gender field (confirmed against the live swagger) and no client call sends one. A fresh account's /me.gender stays null forever.
  • preferredLanguage is write-only. It round-trips (verified live) but nothing on either side reads it — the UI locale comes from the /fa|/en URL prefix and LocaleSwitcher. The language sheet at profile/page.tsx:91-93 therefore persists a preference the app ignores.
  • The upsert has no PATCH semantics (profile/page.tsx:76-79): every sheet save rewrites the whole profile and users.Name/FamilyName. Saving only the emergency contact re-sends the name; a stale form can silently overwrite a name changed on another device.
  • Saving any sheet writes preferredLanguage: values.language, whose default is 'fa' when the served value is null (profile/page.tsx:85). A customer who opens the language sheet and saves silently commits 'fa' they never chose.
  • /fa/select-role is reachable by any authenticated user at any time and has no RoleGuard by design, so an onboarded customer can permanently add themselves the nurse role by typing the URL. Intentional (role_add_later_note), but there is no confirmation step for an irreversible grant.
  • The seeded demo world contains no un-onboarded account, so this flow's first-run half is untestable without minting a shell user against the shared DB. See testing-setup.md.
  • useSelectRole swallows a failed post-select token rotation (useSelectRole.ts:30-32). The role is persisted server-side, but the client keeps a JWE without the new claim until the fetch layer's silent refresh happens to fire — a role-gated call in between 403s.