83 lines
4.1 KiB
Markdown
83 lines
4.1 KiB
Markdown
# 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`, `hasRoleAccess`, `getDisplayName`); `shared/constants/roles.ts`
|
|
(`ROLE_NAMES`); `shared/constants/safety-quiz.ts`
|
|
(`SAFETY_QUIZ_REPORT_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: <SafetyQuizInput> }`.
|
|
Returns the created result DTO.
|
|
|
|
## 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.
|
|
- `list`: users holding a role in `SAFETY_QUIZ_REPORT_ROLE_NAMES` (`super_admin`,
|
|
`system_admin`, `owner`, `superintendent`, `director`) or any role with
|
|
`globalAccess` (via `hasRoleAccess`) see all org-level results; everyone else sees only their own
|
|
rows (filtered by `userId`).
|
|
|
|
## 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.
|
|
|
|
## 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` runs inside `withTransaction`; trimmed string fields are persisted.
|
|
- `list` is paginated with shared defaults (`resolvePagination`).
|
|
|
|
## Tests
|
|
|
|
None yet (no `safety_quiz_results` unit/e2e test in `src/`).
|
|
|
|
## Related
|
|
|
|
- Frontend: `frontend/docs/safety-quiz-integration.md`.
|
|
- Related slices: `personality-quiz-results.md`, `walkthrough-checkins.md`, `user-progress.md`.
|