03 — User Journeys (Students Module)
- 1. Create + enroll student (admission desk)
- 2. Link parent / guardian
- 3. Transfer student to another class
- 4. Upload student documents
- 5. Bulk CSV import (admissions batch)
- 6. Graduate / withdraw student
- 7. Student self-view
(forward-looking) - Journey → endpoint inventory (summary)
Happy-path + failure branch journeys with Mermaid. Every endpoint referenced is exact (
src/modules/students/controllers/student.controller.ts,src/modules/parents/ controllers/parent.controller.ts,src/modules/bulk/bulk.controller.ts). All journeys assume an authenticated JWT;tenantIdcomes from the token only.
1. Create + enroll student (admission desk)
sequenceDiagram
actor R as Admission staff
participant UI as Students UI
participant API as POST /users + POST /students
participant Q as BullMQ (in-app / audit)
R->>UI: Open "Add student" wizard
UI->>API: POST /users {firstName,lastName,email} (identity step)
alt user already exists (409/duplicate email)
API-->>UI: 409 — staff searches existing user instead
UI->>API: GET /users?q=email (reuse existing userId)
else created
API-->>UI: 200 user {_id}
end
UI->>API: POST /students {userId, admissionNumber, classId, academicYearId, gradeId, sectionId, admissionType…}
Note over API: duplicate admissionNumber → 409 ConflictException<br/>(student.service.ts:56-62); else status=active + auto-enroll ACTIVE (71-78)
alt duplicate admission number
API-->>UI: 409 DUPLICATE_RESOURCE — inline field error, staff corrects
else success
API-->>UI: 200 student doc (with enrollment created)
UI-->>R: Success snackbar → detail screen
end
Note over Q: StudentCreated → in-app "student-enrolled"<br/>(event-queue-map.ts:28); audit log via StudentCreated/audit
Failure branches: 400 validation (missing userId, malformed admissionNumber),
409 duplicate, 5xx. Identity-first ordering is a hard server constraint: CreateStudentDto.userId
is required (create-student.dto.ts:6-7).
2. Link parent / guardian
sequenceDiagram
actor R as Admission staff
participant UI as Student detail → Parents tab
participant API as POST /parents + POST /parents/link/:studentId
R->>UI: "Add parent" (choose existing or create new)
alt new parent
UI->>API: POST /users (guardian identity)
UI->>API: POST /parents {userId, occupation?, pickupAuthorization?}
Note over API: duplicate userId → 409 (parent.service.ts:30-34)
end
UI->>API: POST /parents/link/:studentId {parentId, relationship, isPrimaryGuardian?, financialResponsibility?, pickupAllowed?, emergencyPriority?}
Note over API: student existence checked (student-parent-link.service.ts:27);<br/>parent existence NOT checked (OQ-11)
alt success
API-->>UI: 200 link doc (relationship enum: mother|father|guardian|grandparent|relative|foster_parent — student-parent-link.schema.ts:7-14)
UI-->>R: Snackbar "Guardian linked"; Parents tab refreshes
else 404 student
API-->>UI: 404 RESOURCE_NOT_FOUND
end
opt unlink
UI->>API: DELETE /parents/link/:linkId (soft delete; 404 if missing — student-parent-link.service.ts:38-41)
end
Relationship is required on the link DTO (link-parent.dto.ts:9-20); all four
flags are optional booleans with schema defaults (student-parent-link.schema.ts:27-37).
3. Transfer student to another class
flowchart TD
A[Student detail → Actions → Transfer] --> B{status == active?}
B -- no --> C[Blocked: 409 'Cannot transfer a student with status X'<br/>student.service.ts:175-179 — show status banner]
B -- yes --> D[Transfer form: class + academic year<br/>+ optional grade/section/rollNumber<br/>transfer-student.dto.ts]
D --> E[POST /students/:id/transfer]
E --> F{Server}
F -- ok --> G[Old enrollments set TRANSFERRED + leftAt<br/>student.service.ts:125-131 via enroll()]
F -- ok --> H[Student classId/academicYearId updated + optional gradeId/sectionId<br/>student.service.ts:185-192]
F -- ok --> I[StudentUpdated → audit 'log-student-updated'<br/>event-queue-map.ts:29]
G & H & I --> J[UI: refresh detail + academic history<br/>snackbar 'Transferred']
F -- 404 --> K[Student not found]
F -- 5xx --> L[Generic + requestId]
gradeId/sectionId are optional and default to the target class's own grade/section
(transfer-student.dto.ts:13-25).
4. Upload student documents
sequenceDiagram
actor R as Admission staff
participant UI as Documents tab
participant API as POST /students/:id/documents
participant S as StorageProvider
R->>UI: Tap "+ Document" → pick/capture file
UI->>API: multipart POST file=… + field category (optional, string)
Note over API: FileInterceptor('file') (student.controller.ts:82-90);<br/>no size/type validation server-side (OQ-9)
API->>S: storage.upload({buffer, filename, mimeType, tenantId}) → fileId
S-->>API: fileId (local: "<tenantId>/<uuid>--<name>" — local-storage.provider.ts:26-30)
API-->>UI: 200 document doc {fileName, mimeType, size, fileId, category, uploadedBy}
UI-->>R: Attachment tile appears; list sorted createdAt desc (student.service.ts:273-279)
Note over UI: Preview/download needs a file-serving endpoint — only fileId returned today (OQ-9)
5. Bulk CSV import (admissions batch)
flowchart TD
A[Students list → menu → Import] --> B[Download template GET /bulk/export/students<br/>bulk.controller.ts:50-60 — exports admissionNumber, rollNumber, status, admissionDate]
B --> C[Fill rows: firstName*, lastName*, email*, admissionNumber*,<br/>grade*, section*, academicYear*, rollNumber?<br/>students-import.adapter.ts:17-26]
C --> D[Pick CSV → POST /bulk/import/students<br/>multipart field 'file']
D --> E{Parse}
E -- malformed/empty --> F[400 'Malformed CSV' / 'CSV must include a header row'<br/>bulk-import.service.ts:31-35]
E -- ok --> G[Per-row validate + create<br/>rowNumber = index + 2 (header = row 1)<br/>bulk-import.service.ts:45-63]
G --> H[Report: totalRows, imported, failed, errors[{rowNumber, errors}]]
H --> I[UI: result screen — imported count, failed rows list with<br/>per-row messages: missing columns, invalid email,<br/>duplicate admission, duplicate email,<br/>academicYear/grade/section not found, no class for grade+section<br/>students-import.adapter.ts:38-60,100-144]
I --> J[Retry failed rows after fixing CSV]
Import is synchronous in code (loop in the request) — no BullMQ progress job today; the "import progress" state is client-side simulation of a long request (OQ-12). A 1000-row file = 1000 sequential create calls.
6. Graduate / withdraw student
flowchart TD
A[Student detail → Actions → Graduate] --> B[Confirm dialog]
B --> C[POST /students/:id/graduate]
C --> D{status == graduated?}
D -- yes --> E[409 'Student is already graduated.' student.service.ts:207-209]
D -- no --> F[status := graduated; StudentUpdated ['graduate'] → audit]
A2[Student detail → Actions → Archive] --> B2[Confirm dialog<br/>copy: hidden from roster, restorable]
B2 --> C2[POST /students/:id/archive]
C2 --> D2{status == archived?}
D2 -- yes --> E2[409 student.service.ts:228-230]
D2 -- no --> F2[status := archived; StudentUpdated ['archive']]
F2 --> G[Restore: POST /students/:id/restore → active<br/>student.controller.ts:74-77]
Note over G: There is NO 'withdraw' status — withdrawal = archive today<br/>(enum has inactive/transferred but unwritable, OQ-5/OQ-6)
7. Student self-view (forward-looking)
sequenceDiagram
actor S as Student (linked userId)
participant UI as My Profile
participant API as (planned) student-self endpoints — OQ-1
S->>UI: Open My Profile
Note over UI: No GET /students/me exists. Today the student's userId<br/>has no student-scoped endpoint; profile reachable only via staff routes.
UI->>API: (planned) GET /students/me → profile + academic history + documents
Note over UI: Alternative interim: staff shares a read-only link?<br/>Blocked on OQ-1 decision.
Journey → endpoint inventory (summary)
| Journey | Primary endpoints | Source |
|---|---|---|
| Create + enroll | POST /users, POST /students | users.controller.ts:36-40, student.controller.ts:38-40 |
| Link parent | POST /parents, POST /parents/link/:studentId, DELETE /parents/link/:linkId | parent.controller.ts:29-31,47-60 |
| Transfer | POST /students/:id/transfer | student.controller.ts:62-65 |
| Documents | POST /students/:id/documents, GET /students/:id/documents | student.controller.ts:78-90 |
| Bulk import | POST /bulk/import/students, GET /bulk/export/students | bulk.controller.ts:35-60 |
| Graduate/archive | POST /students/:id/graduate|archive|restore | student.controller.ts:66-77 |
| Academic history | GET /students/:id/academic-history, GET /students/:id/enrollments | student.controller.ts:47-49,91-94 |