40227-vm/backend/docs/safety-quiz-results.md

6.7 KiB

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.
  • 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.