Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

03 — User Journeys (Users Module)

End-to-end journeys computed from users.controller.ts, users.service.ts, bulk.controller.ts / bulk-import.service.ts / students-import.adapter.ts, rbac.controller.ts, auth.service.ts, and tenant-purge.worker.ts. Each journey: entry, intent, decision points, system responses, failures, recovery, exit, permission denial, offline, conflict, multi-device, deep-link/push/email entry. (planned) / (forward-looking) per global rules.


1. Invite a user (admin creates identity + membership)

entry: /users → FAB "Add user"
intent: onboard a new staff member / teacher / parent with roles
sequenceDiagram
    actor A as Org Admin / HR
    participant F as CreateUserScreen
    participant R as UsersRepository
    participant API as POST /api/v1/users
    participant RBAC as POST /api/v1/rbac/members
    A->>F: firstName, lastName, email (+ optional middleName/displayName/phone/gender/DOB/language/timezone/status)
    F->>R: submit()
    R->>API: CreateUserDto — create-user.dto.ts:5-62
    alt 201 success
        API-->>R: user doc — users.service.ts:49-78 (displayName defaults to "firstName lastName" — :62)
        Note over API: EventBus emits UserCreated → in-app queue job<br/>user-created-notification — event-queue-map.ts:10
        R->>RBAC: {userId, roles: ["teacher"]} — rbac.service.ts:113-127 (status ACTIVE, joinedAt now)
        F-->>A: success snackbar; row appears in list (refresh)
    else 409 DUPLICATE_RESOURCE
        API-->>R: "User with email X already exists." — users.service.ts:50-54
        F-->>A: inline email conflict; suggest search; block submit until fixed
    else 400 VALIDATION_ERROR
        F-->>A: field errors (email format — create-user.dto.ts:24-26; gender enum :33-38; status enum :44-47)
    else 429 / 5xx / offline
        F-->>A: retry-able error; form draft kept
    end
    Note over F: Invite e-mail is (planned): email.worker.ts handles only<br/>UserRegistered / PasswordResetRequested (:25-42); no UserInvited event.
  • Decision points: status (default activecreate-user.dto.ts:44-47); roles to assign (via RBAC step); whether to send verification email — today not sent for admin-created users (OQ-2).
  • Failures/recovery: email/phone dup → inline conflict (users.service.ts:50-60); RBAC member create failing after user create → user exists without roles (compensate: retry membership; note OQ-5).
  • Exit: success → list (refresh); back → draft warning.
  • Permission denial: no user.create → FAB hidden + 403 screen (client guard; server RBAC guard (planned) — only JwtAuthGuard today, users.controller.ts:31).
  • Multi-device: second admin sees the row on refresh; membership visible via GET /rbac/members.
  • Deep links / email entry: (forward-looking) invite email with accept-link; no accept endpoint exists yet.

2. Bulk CSV import (users) with progress

entry: /users → "Import CSV" (bulk wizard)
intent: onboard N users from a spreadsheet
sequenceDiagram
    actor A as Org Admin / HR
    participant W as ImportWizard
    participant P as CsvService (isolate parse + preview)
    participant API as POST /api/v1/users/import (multipart file)
    participant S as UsersService.bulkImport
    A->>W: pick file (CSV) — desktop drag-drop
    W->>P: parse in isolate → header map + row count + preview 5 rows
    P-->>W: preview table, column mapping (firstname|first_name …)
    A->>W: confirm → upload with progress bar (client-side stage progress)
    W->>API: multipart field "file" — users.controller.ts:104-110
    API->>S: buffer → utf-8 → lines — users.service.ts:236-237
    Note over S: synchronous loop per row — :250-279 (min 2 lines :238-243; header lowercased :244-247)
    S-->>API: {imported: N, errors: ["Row 3: missing email", …]} — :281
    API-->>W: 201 envelope
    W-->>A: result screen: N imported, M failed — expandable error list per row
    alt some rows failed
        A->>W: "Download errors" (client-side CSV) → fix → re-upload
    end
  • Progress semantics: the endpoint is synchronous — there is no progress percentage from the server and no polling endpoint. The wizard's progress bar reflects client-side stages (parse → upload → server processing indeterminate) — honest labeling required (OQ-6). Async queue import with polling (planned) per PLAN.md 2.7 (POST /api/v1/files/upload-csv → per-row UserCreated).
  • Validation facts shown in preview: required columns email, firstname|first_name, lastname|last_name; optional phone, gender, language, timezone (users.service.ts:264-271); within-file duplicate emails are caught sequentially (:260-263); quoted commas are not supported by the naive parser (:251split(',')) — warn on quotes (OQ-7).
  • Partial-import semantics: rows are committed as they go; a failed row does not roll back previous rows. Re-upload after fixes re-checks duplicates, so already-imported emails are skipped with an error (:260-263) — user must delete those rows or accept the errors.
  • Idempotency: retrying the same file is safe (duplicates are rejected, not duplicated) — but each retry returns the dupes as errors (see 14_QA_Checklist.md C2).
  • Timeout: for very large files the request may exceed gateway timeouts — client guidance ≤ 1000 rows (see 14_QA_Checklist.md C1); upload timeout 120 s (00-shared/11 §5).

3. Edit profile & preferences (self-service)

entry: avatar menu → Profile (self) — or admin: user row → Edit
intent: update contact info / language / timezone / notification toggles / theme
sequenceDiagram
    actor U as User (self) / Admin (managed)
    participant F as EditUserScreen / PreferencesScreen
    participant API as PATCH /api/v1/users/:id (+ /preferences)
    participant S as UsersService.update / updatePreferences
    U->>F: change firstName/lastName/email/phone/… 
    F->>API: UpdateUserDto — update-user.dto.ts:5-70
    alt 200 success
        S-->>F: updated doc; displayName recomputed if names changed — users.service.ts:136-139
        Note over API: UserUpdated emitted with changes list — :145-153 → audit-write — event-queue-map.ts:11
        F-->>U: success snackbar; audit trail continues in background
    else 409 (email/phone taken by another user)
        S-->>F: ConflictException — :120-134 → inline field error
    else 404 (cross-tenant or erased id)
        S-->>F: NotFoundException — :118,144 → "user not found" empty state
    end
    U->>F: Preferences tab → toggle email/push/sms, theme mode light|dark|system
    F->>API: PATCH /users/:id/preferences — update-user-preferences.dto.ts:4-21
    Note over S: preferences replaced wholesale ($set preferences: dto) — users.service.ts:161-163 — send full object!
  • Critical contract: preferences are replaced ($set: {preferences: dto}, users.service.ts:161-163) — the client must PATCH the full merged preferences object; a partial payload wipes unmentioned sections.
  • Optimistic UI: safe toggles (theme, notification switches) may be optimistic with rollback (00-shared/06 §3.5); email/phone changes are never optimistic (conflict-prone, 00-shared/07 §9).
  • Self vs managed: controller accepts any :id; client restricts "self profile" to sub == :id (server-side self guard (planned), OQ-3).
  • Avatar: separate multipart flow POST /users/:id/avatar (users.controller.ts:95-102); old avatar deleted best-effort (users.service.ts:227-229).

4. Deactivate / soft-delete / GDPR erasure

entry: user row → menu → "Deactivate" | "Delete user" | "GDPR erase"
intent: stop access; remove from lists; comply with right-to-erasure
sequenceDiagram
    actor A as Org Admin
    participant F as UserDetailScreen
    participant API as DELETE /api/v1/users/:id
    participant P as TenantPurgeWorker
    actor E as Erased user
    A->>F: Deactivate → PATCH /users/:id {status: inactive}
    F->>API: status change — update-user.dto.ts:52-55
    API-->>F: 200 (user can no longer be found by scoped queries? no — inactive ≠ deleted; login still possible if credentials exist)
    A->>F: Delete → typed-confirm dialog (destructive, 05_Global_IA §5)
    F->>API: DELETE /users/:id — users.controller.ts:62-67
    API->>API: softDelete: isDeleted, deletedAt, deletedBy — base.repository.ts:68-74
    Note over API: UserDeleted → audit-write — event-queue-map.ts:12
    F-->>A: row removed; snackbar "Purged after 30 days"
    A->>F: GDPR → POST /users/:id/erasure — users.controller.ts:69-76
    API->>API: anonymize PII + isDeleted — users.service.ts:187-197
    API->>P: gdpr-erasure job (attempts 3, backoff exp 5s) — :200-209
    P-->>E: hard-delete user doc — tenant-purge.worker.ts:49-56
  • Status vs delete distinction (must be explicit in UI):
    • inactive — record visible in admin lists, can be re-activated, can still log in if an auth_account exists (auth.service.ts:124-137 checks only isDeleted).
    • Soft-deleted — cannot log in (findByEmailUnscoped filters isDeleted:false, users.repository.ts:21-25), invisible everywhere, hard-purged after 30 days (tenant-purge.worker.ts:32-43).
    • Erased — anonymized immediately, hard-deleted by job.
  • Active sessions caveat: deleting/erasing a user does not revoke existing JWTs/sessions (no session revocation call — OQ-8); logout-all exists (auth.controller.ts:66-72) but is not called by user deletion.
  • Failures: 404 if already gone; network loss after confirm → retry is idempotent (DELETE is safe to retry).
  • Permission: user.delete for delete; erasure uses the same permission surface today (no separate perm) — note in UI copy.

5. Import failures & recovery

entry: bulk wizard → result screen with errors → fix & re-run
sequenceDiagram
    actor A as Org Admin
    participant W as ImportResultScreen
    participant API as POST /api/v1/users/import
    A->>W: result: imported=97, errors=[{Row 3…}, {Row 14…}, …] — users.service.ts:281
    W-->>A: error rows in a table (row number, message) — copy = backend text ("Row 3: missing email", "Row 3: email x@y already exists")
    A->>W: download error CSV (client-side) → edit in Excel
    A->>W: re-upload corrected file
    W->>API: retry — same endpoint
    alt duplicate of already-imported email
        API-->>W: "Row N: email already exists" — re-verify before upload: keep previously imported rows out
    end
  • Recovery rules: partial imports are expected; the UI must never imply atomicity (server commits row-by-row — users.service.ts:250-279); error list is authoritative; cross-check imported count vs. expected.
  • Import via students adapter (bulk module) reports richer errors {rowNumber, errors[]} + totalRows/imported/failed (import-adapter.interface.ts:14-25) and pre-validates refs (students-import.adapter.ts:38-60); entity selector in the wizard (planned) for a users adapter (only students exists — bulk-import.service.ts:17-20).
  • Exit: result screen can be closed; import is already committed — no cancel path (synchronous).

Journey matrix (who / where / what)

JourneyEntryPrimary screensBackend truth
Invite user/users FABCreateUser + RBAC roles sheetPOST /users, POST /rbac/members
Bulk import/users "Import CSV"ImportWizard (upload→preview→result)POST /users/import (sync)
Edit profile/prefsAvatar menu / row editEditUser, PreferencesPATCH /users/:id, PATCH /users/:id/preferences
Deactivate / delete / eraseRow menu / detailConfirm dialogsPATCH status, DELETE /users/:id, POST /users/:id/erasure
Import recoveryResult screenError table + re-uploadretry POST /users/import