6.0 KiB
6.0 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_resultsinside the service (no separatedb/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 queryweek_of, pluslimit/page(paginated viaresolvePagination). Returns results visible to the current user (see Access Rules), ordered bycompleted_atdesc.GET /api/safety_quiz_results/me->200{ completed, result }. Optional queryweek_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 queryweek_of. Requires report access and returns every staff user in the current scope withcompleteorpendingstatus based on savedsafety_quiz_resultsrows.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 returnsnull.
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.createpersists 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 withREAD_SAFETY_QUIZ_REPORTSsee scope-filtered results; everyone else sees only their own rows (filtered byuserId).me: ignores report access and only checks the authenticated user's own saved result rows.completion: requiresREAD_SAFETY_QUIZ_REPORTSand 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_permissionscan grant the report permission andcustom_permissions_filtercan 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,
campusIdis set fromgetCampusId;userId,createdById,updatedByIdcome 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);scoreandtotal_questions(integers);answers(an array of integers). Invalid input raisesValidationError. - On create the backend fills
user_namefromgetDisplayName(currentUser)anduser_rolefrom the user'sapp_role.name(defaulting toteacher);completed_atis 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 isparanoid(soft delete viadeletedAt) and usesfreezeTableName. - Associations:
belongsToorganizations (organization), campuses (campus), users (user,createdBy,updatedBy).
Behavior / Notes
createfirst checks whether the caller is acting in their own scope. If not, it skips persistence and returnsnull. Otherwise it runs insidewithTransaction; trimmed string fields are persisted.listis paginated with shared defaults (resolvePagination).completionreports organization, school, campus, and class scope staff according to the active scope. Student, guardian, guest, and system roles are not completion subjects.
Tests
src/services/personal_scope_results.test.tsverifies 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-quizis organization-scoped). - Related slices:
personality-quiz-results.md,walkthrough-checkins.md,user-progress.md.