# Safety Quiz Results Backend

## Purpose

`safety_quiz_results` stores weekly safety/de-escalation quiz submissions per authenticated staff
user. The backend owns tenant scope, user ownership, the user display-name snapshot, the product
role snapshot, and persistence. Each submission is an append (create) — there is no update path.

## Slice Files (by layer)

- Route: `src/routes/safety_quiz_results.ts` (thin wiring; `GET /`, `GET /me`,
  `GET /completion`, `POST /`).
- Controller: `src/api/controllers/safety_quiz_results.controller.ts` (custom — not the CRUD
  factory).
- Service (BLL): `src/services/safety_quiz_results.ts`.
- Repository (DAL): queries run through `db.safety_quiz_results` inside the service (no separate
  `db/api/safety_quiz_results.ts`).
- Model: `src/db/models/safety_quiz_results.ts`.
- Shared used: `db/with-transaction.ts` (`withTransaction`); `shared/constants/pagination.ts`
  (`resolvePagination`); `services/shared/access.ts` (`getOrganizationIdOrGlobal`, `getCampusId`,
  `assertAuthenticatedTenantUser`, `hasFeaturePermission`, `getDisplayName`);
  `shared/constants/roles.ts` (`ROLE_NAMES`); `shared/errors/validation.ts` (`ValidationError`).

## API

All routes require JWT authentication. Base path mounted at `/api/safety_quiz_results`.

- `GET /api/safety_quiz_results` -> `200` `{ rows, count }`. Optional query `week_of`, plus
  `limit` / `page` (paginated via `resolvePagination`). Returns results visible to the current user
  (see Access Rules), ordered by `completed_at` desc.
- `GET /api/safety_quiz_results/me` -> `200` `{ completed, result }`. Optional query `week_of`.
  Always reads only the authenticated user's own saved quiz result and is used for profile status
  and weekly notification state.
- `GET /api/safety_quiz_results/completion` -> `200` `{ summary, rows }`. Optional query
  `week_of`. Requires report access and returns every staff user in the current scope with
  `complete` or `pending` status based on saved `safety_quiz_results` rows.
- `POST /api/safety_quiz_results` -> `201`. Request body wrapped as `{ data: <SafetyQuizInput> }`.
  Returns the created result DTO. If the caller is a parent-scope user acting through a drilled
  child scope, the request is accepted as a no-op and returns `null`.

## Access Rules

- All operations require an authenticated tenant user (`assertAuthenticatedTenantUser`).
- `create`: a staff user creates a result for themselves; ownership fields are filled from the
  authenticated user. The submitted `week_of` date is normalized to the Sunday-start week before
  storage so weekly notification, profile, completion, and dashboard reads use one canonical key.
- `create` persists only when the active scope is the user's own scope. Parent users drilled into a
  child school/campus/classroom can complete the quiz there, but the backend does not create
  reportable quiz rows for that child scope.
- `list`: users with `READ_SAFETY_QUIZ_REPORTS` see scope-filtered results;
  everyone else sees only their own rows (filtered by `userId`).
- `me`: ignores report access and only checks the authenticated user's own saved result rows.
- `completion`: requires `READ_SAFETY_QUIZ_REPORTS` and joins staff users in the current scope with
  saved result rows. Pending rows are derived from missing database results, not frontend state.
  Role-seeded permissions are only the baseline grants. `custom_permissions` can
  grant the report permission and
  `custom_permissions_filter` can remove it for non-global users.

## Tenant Scope

- Organization is resolved via `getOrganizationIdOrGlobal`: global access users bypass the org
  filter and see results across all organizations; regular users are bound to their organization.
- On create, `campusId` is set from `getCampusId`; `userId`, `createdById`, `updatedById` come from
  the current user.
- Drilled child scopes are not treated as the user's own scope for personal saves, even though list
  visibility can use the active scope for reports.

## Data Contract

- Mutation input (`SafetyQuizInput`): `quiz_id`, `quiz_title`, `week_of` (`YYYY-MM-DD`, normalized
  to the Sunday-start week before storage);
  `score` and `total_questions` (integers); `answers` (an array of integers). Invalid input raises
  `ValidationError`.
- On create the backend fills `user_name` from `getDisplayName(currentUser)` and `user_role` from
  the user's `app_role.name` (defaulting to `teacher`); `completed_at` is set to the current time. The frontend does not
  send name, role, or ownership fields.
- DTO fields: `id`, `quiz_id`, `quiz_title`, `week_of`, `score`, `total_questions`, `answers`,
  `user_name`, `user_role`, `completed_at`, `organizationId`, `campusId`, `userId`, `createdAt`,
  `updatedAt`.
- Model columns: `quiz_id`, `quiz_title`, `week_of`, `user_name`, `user_role` (all TEXT, not null);
  `score`, `total_questions` (INTEGER, not null); `answers` (JSONB, not null); `completed_at`
  (DATE, not null); `importHash` (unique); plus tenant/audit UUID columns (`organizationId`,
  `campusId`, `userId`, `createdById`, `updatedById`, all nullable). The model is `paranoid` (soft
  delete via `deletedAt`) and uses `freezeTableName`.
- Associations: `belongsTo` organizations (`organization`), campuses (`campus`), users (`user`,
  `createdBy`, `updatedBy`).

## Behavior / Notes

- `create` first checks whether the caller is acting in their own scope. If not, it skips
  persistence and returns `null`. Otherwise it runs inside `withTransaction`; trimmed string fields
  are persisted and `week_of` is canonicalized to Sunday week start.
- `list` is paginated with shared defaults (`resolvePagination`).
- `completion` reports organization, school, campus, and class scope staff according to the active
  scope. Student, guardian, guest, and system roles are not completion subjects.
- Result history is append-only. Retakes and future weekly submissions create additional rows;
  current-week status, notifications, staff completion, and dashboards filter by the current
  canonical `week_of` value.
- Quiz content is stored in `content_catalog` as `safety-qbs-quiz`. Organization managers edit only
  the active version; updates preserve old quiz content by marking the previous row inactive and
  inserting a new active row. Readers load only the active quiz payload.

## Tests

- `src/services/personal_scope_results.test.ts` verifies that parent users drilled into child
  scopes do not create safety quiz result rows, that personal status reads from saved rows, and that
  completion reports include both completed and pending staff.

## Related

- Frontend: `frontend/docs/safety-quiz-integration.md`.
- Quiz content management: `backend/docs/content-catalog.md` (`safety-qbs-quiz` is organization-scoped).
- Related slices: `personality-quiz-results.md`, `walkthrough-checkins.md`, `user-progress.md`.
