03 — User Journeys (Parents Module)
- 1. Create parent profile
- 2. Link parent to student
- 3. Unlink parent ↔ student / switch primary guardian
- 4. Parent logs in → sees own children (self-service,
(forward-looking)) - 5. Parent updates own profile (self-service,
(forward-looking)) - 6. Cross-cutting
End-to-end journeys computed from
parent.controller.ts,parent.service.ts,student-parent-link.service.ts,student.controller.ts, andusers.service.ts. Each journey: entry, intent, decision points, system responses, loading, failures, recovery, exit, back navigation, abandonment, permission denial, offline.(planned)/(forward-looking)marks per global rules.
1. Create parent profile
entry: admin/admission staff → Parents → "Add guardian"
intent: record a guardian's business profile for an existing user
precondition: the user account exists (POST /users, users.service.ts:49)
sequenceDiagram
actor A as Admin/Admission staff
participant F as ParentFormPage
participant R as ParentsRepository
participant API as POST /parents
A->>F: search & pick user (users list), fill occupation/company/flags
F->>R: submit(CreateParentDto)
R->>API: {userId, occupation?, company?, annualIncome?, relationshipNotes?,<br/>emergencyContactPriority?, pickupAuthorization?}
alt success
API-->>R: 200/201 envelope.data = parent doc (id, userId, flags, timestamps)
R-->>F: navigate to parent detail
Note over API: ParentCreated → in-app 'parent-created' (event-queue-map.ts:37)
else 409 DUPLICATE_RESOURCE
API-->>R: "Parent profile already exists for this user." (parent.service.ts:32-34)
F-->>A: inline banner + link to existing profile
else 400 VALIDATION_ERROR
API-->>R: field details (userId not MongoId, etc.)
F-->>A: per-field errors
end
- Decision points: which
User(search by name/email viaGET /users?q=,users.service.ts:90-115); pickup authorization default off (parent.schema.ts:28). - Loading: CTA spinner; anti-double-submit (
00-shared/08 §6). - Failure covers: duplicate user (409), invalid id (400), offline (banner, blocked), rate limit (429 countdown), 5xx (generic + requestId).
- Exit: success → detail; back = list. Abandonment: form state lost; user pick kept client-side only.
- Privacy: parentId is tenant-scoped; no cross-tenant read possible
(
base.repository.ts:20-30).
2. Link parent to student
entry: parent detail → "Link to student"; student detail → "Add guardian"
intent: create one relationship row with role, primary flag, pickup, financial flags
precondition: parent and student docs exist in the same tenant
sequenceDiagram
actor A as Admin/Admission staff
participant S as LinkSheet
participant R as ParentsRepository
participant API as POST /parents/link/:studentId
A->>S: search student (admission number/name) or pick from student page
S->>R: submit(LinkParentDto)
R->>API: {parentId, relationship, isPrimaryGuardian?, financialResponsibility?,<br/>pickupAllowed?, emergencyPriority?}
API->>DB: studentRepo.findById(studentId) — result DISCARDED (student-parent-link.service.ts:27)
API->>DB: linkRepo.create({studentId, parentId, ...}) — tenantId injected (base.repository.ts:33-35)
alt success
API-->>R: envelope.data = link doc (relationship, flags, timestamps)
R-->>F: sheet closes; guardian list refreshes (link appears)
else 404
API-->>R: "Student not found." — only if findById throws (today: no-op, OQ-3)
end
- Decision points: relationship (6 values,
student-parent-link.schema.ts:7-14); primary guardian toggle; pickup default true on the link (student-parent-link.schema.ts:33-34). - Known server gaps (design around them):
- Duplicate link not detected → second row appears (OQ-2) → client should pre-check existing links and warn "already linked".
- Missing student not detected → link row created anyway (OQ-3).
relationshipis free text server-side (link-parent.dto.ts:19-20) → client must send only enum values (OQ-6).
- Primary-guardian UX: if the student already has a primary, warn before creating a second primary (server allows it, OQ-5).
- Exit: success → sheet closes; back = cancel sheet.
3. Unlink parent ↔ student / switch primary guardian
entry: parent detail → linked child row → menu; or student detail → guardian row → menu
intent: remove a relationship (transfer, custody change, error) or promote another guardian
sequenceDiagram
actor A as Admin
participant D as UnlinkDialog
participant R as ParentsRepository
participant API as DELETE /parents/link/:linkId
A->>D: "Unlink guardian from student?"
D->>R: confirm(linkId)
R->>API: DELETE /parents/link/:linkId
API->>DB: linkRepo.softDelete(linkId) (student-parent-link.service.ts:38-41)
alt success
API-->>R: envelope.data = undefined (void)
R-->>F: row removed; snackbar
Note over API: no ParentDeleted/update event fired for unlink (only ParentUpdated/Deleted, event-queue-map.ts:38-39)
else 404 RESOURCE_NOT_FOUND
API-->>R: "Link not found." (student-parent-link.service.ts:40)
F-->>A: treat as already removed
end
- Switch primary guardian (two-step, no dedicated endpoint):
PATCHthe new primary link →{isPrimaryGuardian:true}thenPATCHthe old →{isPrimaryGuardian:false}(PATCH /parents/:idcannot touch links — links are only changed by create/delete; so the client calls link-creation? No — there is no link-update endpoint at all:StudentParentLinkServiceexposes onlylinkStudentParent,findByStudent,removeLink(student-parent-link.service.ts:16-41). → Switching primary requires delete + recreate the links (OQ-4/OQ-5). - Unlink of the primary guardian: warn "This student will have no primary guardian"; server does not auto-promote (OQ-4).
- Unlink never deletes the parent — parent profile persists even with zero links
(
04-Modules/Parents.md:58). - Permission denial: JWT-only today; UI gates by intended role
(planned).
4. Parent logs in → sees own children (self-service, (forward-looking))
entry: parent opens app → login → home
intent: land on "My children", switch between kids, deep-dive each child
precondition: parent has ≥ 1 link; user has parent role (role.schema.ts:50)
sequenceDiagram
actor P as Parent
participant H as Home/MyChildrenPage
participant R as ParentsRepository
participant API as (planned) GET /parents/me or /parents?userId=
P->>H: open My Children
H->>R: loadMyChildren()
R->>API: (forward-looking — no endpoint today, OQ-1)
Note over R,API: BLOCKER: no /parents/me, no userId filter,<br/>no "links for my profile" query (parent.service.ts:55-67, findLinks by id only)
alt endpoint exists (future)
API-->>R: links[] → child studentIds
R->>API: GET /students/:id ×N (join, OQ-10)
H-->>P: child switcher (avatar list) + per-child dashboard
else today
R-->>H: (forward-looking) — client cannot resolve; show "coming soon"
end
- Privacy boundary: the intent is "only linked children" (
USER_PERSONAS.md:48-53); today the parent role only carriesstudent.readand has no API to enumerate its own profile (OQ-1). This journey is the top(forward-looking)item. - Child switcher: persists selected child in app state (
13_State_Management.md); deep linksstudylyon://students/:idopen a child if linked. - Offline: last-good children list cached (
00-shared/06 §3.3).
5. Parent updates own profile (self-service, (forward-looking))
entry: my profile → edit
intent: update occupation/company/emergency priority/pickup authorization
sequenceDiagram
actor P as Parent
participant F as MyProfilePage
participant R as ParentsRepository
participant API as PATCH /parents/:id
P->>F: change pickupAuthorization + emergencyContactPriority
F->>R: submit(UpdateParentDto)
R->>API: PATCH /parents/:id {pickupAuthorization, emergencyContactPriority}
API->>DB: findById → updateById ($set + $inc version) (parent.service.ts:74-77)
alt success
API-->>R: envelope.data = updated parent doc
F-->>P: snackbar "Profile updated"; audit job ParentUpdated (event-queue-map.ts:38)
else 404
API-->>R: "Parent not found." (parent.service.ts:77)
else 400
API-->>R: validation details
end
- Caveat:
PATCH /parents/:idaccepts any caller with a JWT (no owner check, OQ-8). When RBAC lands, self-edit must still work for the parent's own profile — the UI must know the parent'sid(via the(forward-looking)my-profile resolution). - Cannot update
userId—UpdateParentDtohas nouserId(update-parent.dto.ts:4-34). - Cannot update link-level flags here — relationship/primary/pickup-per-child live on the link and are immutable after create (no link PATCH endpoint) (OQ-4).
6. Cross-cutting
| Entry | Behavior | Status |
|---|---|---|
Deep link studylyon://parents/:id | open parent detail (admin) | client (forward-looking) |
Deep link studylyon://students/:id | open child (parent: only if linked) | client (forward-looking) |
| Push "new guardian linked" | open parent detail | (planned) push infra (00-shared/12 B3) |
ParentCreated in-app notification | "guardian profile created" job parent-created (event-queue-map.ts:37) | implemented queue, UI (planned) |
Abandonment & exit rules: back = previous screen (never shell home); timeout none
(server stateless); permission denial → 403 screen (planned); offline = cached list,
writes blocked with banner.