03 — User Journeys (Settings Module)
- 1. Browse settings (list + group filter)
- 2. Edit a setting (single upsert)
- 3. Batch save (bulk update)
- 4. Permission denied
- 5. Delete a setting
- 6. Cross-cutting (shared rules)
End-to-end journeys computed from
settings.controller.ts+settings.service.ts+setting.repository.ts. Each journey: entry, intent, decision points, system responses, loading, failures, recovery, exit, back navigation, abandonment, timeout, session expiry, permission denial, offline.(planned)/(forward-looking)per global rules.
1. Browse settings (list + group filter)
entry: Settings section → "Settings" (admin), deep link, dashboard quick-link
intent: see the whole tenant config, grouped
sequenceDiagram
actor U as Admin
participant F as SettingsListPage
participant R as SettingsRepository
participant API as GET /settings
U->>F: open Settings
F->>R: load()
R->>API: GET /api/v1/settings (Bearer)
API-->>R: 200 data: [Setting...] (array, meta omitted — non-paginated)
R-->>F: groupBy(group) + sort (server already sorts group,key)
F-->>U: group tabs + key/value rows
alt tap group chip
U->>F: "Attendance"
F->>R: load(group: 'attendance')
R->>API: GET /api/v1/settings?group=attendance
API-->>F: 200 filtered array
else client search
U->>F: type "grace"
F-->>U: filtered rows (client-side only — API has no q param)
end
- Decision points: group chip (server filter) vs free search (client filter).
- Loading:
AppSkeleton(list); content ≤ 2 s budget (00-shared/10 §1). - Failure covers: 401 → silent refresh → session expiry; 5xx →
AppErrorState+ retry; offline → last-good cache +AppOfflineBanner. - Exit: row tap → setting detail; FAB none (edits happen in-line/detail).
- Empty state: no settings at all → "No settings yet — create one" (
AppEmptyState); group empty → "Nothing in this group". - Permission denial: client route guard rejects without
settings.read→ 403 screen (server does not 403 today — OQ-2).
2. Edit a setting (single upsert)
entry: settings list row tap → detail → edit
intent: change one value safely
sequenceDiagram
actor U as Admin
participant D as SettingDetailPage
participant R as SettingsRepository
participant API as PUT /settings
U->>D: open "attendance.gracePeriod" (number editor)
D-->>U: input type inferred from runtime value (number)
U->>D: 10 → 15
D->>R: save(key, value, group)
R->>API: PUT /api/v1/settings {key, value:15, group:'attendance'}
alt 200
API-->>D: data: updated Setting doc
D-->>U: "Saved" snackbar; value reflects server truth
else 400 VALIDATION_ERROR
API-->>D: details per field (group enum invalid etc.)
D-->>U: field error under group picker
else 5xx
D-->>U: generic + requestId; form kept (retry safe — upsert idempotent)
end
- Save semantics (source): upsert =
findOneAndUpdate({tenantId,key,isDeleted:false}, {$set:{value, group?}}, {upsert:true, new:true})(setting.repository.ts:35-39) — the wholevalueis replaced, never merged.label/descriptionfrom the DTO are dropped by the service (settings.service.ts:25). - Idempotency: PUT is safe to retry; last-write-wins per key.
- Client validation: mirrors the DTO —
keynon-empty string,group∈ enum (update-setting.dto.ts:6-15); value type-check is client-only (server stores any JSON). - Optimistic? No — value is config; server-confirm then reflect
(consistent with
00-shared/06 §3.5safe-mutation policy: write-once/semantic ops). In-line save on the list is the fast path (see 4).
3. Batch save (bulk update)
entry: settings list "Edit" mode or multi-select → Save all
intent: apply many config changes in one action
sequenceDiagram
actor U as Admin
participant F as SettingsListPage
participant R as SettingsRepository
participant API as PUT /settings/bulk
U->>F: edit 5 values across groups
F->>F: track dirty keys client-side
U->>F: "Save all" (only dirty keys serialized)
F->>R: saveAll([{key,value,group} x5])
R->>API: PUT /api/v1/settings/bulk [ ...5 dtos ]
loop each dto (server side)
API->>API: repo.upsert(...) sequential, no transaction
end
API-->>R: 200 data: [Setting...] — all saved
R-->>F: clear dirty set; "5 settings saved" snackbar
- Server contract: loop of individual upserts, results returned as array
(
settings.service.ts:28-34). Not atomic — a mid-batch failure returns 5xx with earlier keys already saved (OQ-4). - Client countermeasure: only dirty keys are sent (no wasted writes); on failure show "Saved N of M" with per-key retry — the retry must re-send the full dirty set (upsert is idempotent, so re-sending saved keys is harmless).
- Abandonment: leaving with dirty keys → "Discard changes?" dialog (client-side only; server has no draft).
4. Permission denied
entry: any settings route/action for a user without settings.* (today: client-side only)
sequenceDiagram
actor U as User (no settings perms)
participant R as Router
participant G as RouteGuard
U->>R: navigate /settings
R->>G: guard check permissions.contains('settings.read')
alt lacks permission
G-->>U: redirect /settings/403 (shared 403 screen)
else has permission
G->>R: allow route
end
Note over U: server today returns 200 for any JWT (OQ-2); once @Permissions lands,<br/>403 PERMISSION_DENIED → same redirect via error code mapping
- Failure: 403 from server (future) → shared 403 screen; hidden nav entries for users
without perms (
00-shared/05 §2); inline action 403 → snackbar + hide action (00-shared/06 §5).
5. Delete a setting
entry: detail screen or row menu → Delete
intent: remove a stale key
sequenceDiagram
actor U as Admin
participant D as SettingDetailPage
participant API as DELETE /settings/:key
U->>D: menu → "Delete"
D->>D: AppDialog confirm (destructive)
U->>D: confirm
D->>API: DELETE /api/v1/settings/:key
alt 200
API-->>D: 200 (void data)
D-->>U: snackbar "Setting deleted"; pop to list (row removed)
else 404 RESOURCE_NOT_FOUND
D-->>U: treat as already deleted; remove row
end
Note over U: soft delete — server keeps doc (isDeleted: true)<br/>(base.repository.ts:68-74). Recreating same key → E11000 → 500 (OQ-5)
6. Cross-cutting (shared rules)
| Aspect | Behavior |
|---|---|
| Timeout | dio 15 s; retry on network failure (upsert safe) |
| Session expiry | 401 → silent refresh → sessionExpired → login; state preserved where safe |
| Offline | list from last-good cache + banner; writes blocked with guidance (no offline queue for settings — 00-shared/07 §10) |
| Abandonment | dirty edits dropped on exit with confirm; no server draft |
Deep links (forward-looking) | studylyon://settings → list; studylyon://settings/:key → detail |