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