# 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 /`, `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. - `POST /api/safety_quiz_results` -> `201`. Request body wrapped as `{ data: }`. 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. - `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`). 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` (non-empty strings); `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. - `list` is paginated with shared defaults (`resolvePagination`). ## Tests - `src/services/personal_scope_results.test.ts` verifies that parent users drilled into child scopes do not create safety quiz result rows. ## Related - Frontend: `frontend/docs/safety-quiz-integration.md`. - Related slices: `personality-quiz-results.md`, `walkthrough-checkins.md`, `user-progress.md`.