40227-vm/backend/docs/user-progress.md
2026-06-10 18:27:19 +02:00

87 lines
4.5 KiB
Markdown

# User Progress Backend
## Purpose
`user_progress` stores per-user progress for narrow staff workflows, keyed by a typed
`progress_type` and an `item_id`. Current supported types are `sign_learned` (sign-language items
learned) and `zone_checkin` (zones-of-regulation check-ins). The backend owns tenant scope, user
ownership, validation, and persistence (one row per user + type + item, upserted).
## Slice Files (by layer)
- Route: `src/routes/user_progress.ts` (thin wiring; `GET /`, `POST /`, `DELETE /by-item`).
- Controller: `src/api/controllers/user_progress.controller.ts` (custom — not the CRUD factory).
- Service (BLL): `src/services/user_progress.ts`.
- Repository (DAL): queries run through `db.user_progress` inside the service (no separate
`db/api/user_progress.ts`).
- Model: `src/db/models/user_progress.ts`.
- Shared used: `db/with-transaction.ts` (`withTransaction`); `services/shared/access.ts`
(`assertAuthenticatedTenantUser`, `getCampusId`, `getOrganizationIdOrGlobal`, `requireUserId`);
`shared/constants/user-progress.ts` (`USER_PROGRESS_TYPE_VALUES`, `UserProgressType`);
`shared/constants/pagination.ts` (`resolvePagination`); `shared/errors/validation.ts`
(`ValidationError`).
## API
All routes require JWT authentication. Base path mounted at `/api/user_progress`.
- `GET /api/user_progress` -> `200` `{ rows, count }`. Required query `progress_type` (must be a
valid type); optional `item_id`, plus `limit` / `page` (paginated via `resolvePagination`).
Returns the current user's rows for that type, ordered by `createdAt` desc.
- `POST /api/user_progress` -> `200`. Request body wrapped as `{ data: <UserProgressInput> }`.
Creates or updates one progress item and returns the saved DTO.
- `DELETE /api/user_progress/by-item` -> `200` `{ deletedCount }`. Required query `progress_type`
and `item_id`. Deletes the current user's row for that type + item.
## Access Rules
- All operations require an authenticated tenant user (`assertAuthenticatedTenantUser`).
- No additional role gating: every operation is bound to the current user. List, upsert, and delete
are all filtered by `userId` (`requireUserId`), so a user only ever reads, writes, or deletes
their own progress. Frontend-provided names or roles are not trusted for ownership.
## Tenant Scope
- Organization is resolved via `getOrganizationIdOrGlobal`: global access users bypass the org
filter; regular users are bound to their organization. All queries are still filtered by
`userId` so each user only sees their own progress.
- `userId` is required from the current user (`requireUserId`).
- On upsert, `campusId` is set from `getCampusId`; `updatedById` from the current user, and
`createdById` on create.
## Data Contract
- Mutation input (`UserProgressInput`): `progress_type` (must be one of
`USER_PROGRESS_TYPE_VALUES`: `sign_learned`, `zone_checkin`) and `item_id` (non-empty string) are
required. Optional: `value`, `score`, `metadata`. Invalid input raises `ValidationError`.
- On save, `value` is persisted only if a string (else `null`), `score` only if a number (else
`null`), and `metadata` defaults to `null` when absent; `item_id` is trimmed.
- DTO fields: `id`, `progress_type`, `item_id`, `value`, `score`, `metadata`, `organizationId`,
`campusId`, `userId`, `createdAt`, `updatedAt`.
- Model columns: `progress_type` (ENUM over `USER_PROGRESS_TYPE_VALUES`, not null), `item_id`
(TEXT, not null), `value` (TEXT, nullable), `score` (INTEGER, nullable), `metadata` (JSONB,
nullable), `importHash` (unique). Tenant/audit columns: `organizationId` (not null), `userId`
(not null), `createdById` (not null), `campusId` (nullable), `updatedById` (nullable). The model
is `paranoid` (soft delete via `deletedAt`) and uses `freezeTableName`.
- Associations: `belongsTo` organizations (`organization`), campuses (`campus`), users (`user`,
`createdBy`, `updatedBy`).
## Behavior / Notes
- `upsert` runs inside `withTransaction`: it looks up the existing row by `organizationId` +
`userId` + `progress_type` + `item_id`, updating it if present, otherwise creating it.
- `list` validates `progress_type` and is paginated with shared defaults.
- `removeByItem` is a hard `destroy` call (no transaction wrapper) returning the number of rows
deleted.
## Tests
None yet (no `user_progress` unit/e2e test in `src/`).
## Related
- Frontend: `frontend/docs/user-progress-integration.md`,
`frontend/docs/sign-language-integration.md`, `frontend/docs/zones-of-regulation-integration.md`.
- Related slices: `safety-quiz-results.md`, `personality-quiz-results.md`,
`walkthrough-checkins.md`.