# 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: }`. 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`.