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

7.4 KiB

Personality Quiz Results Backend

Purpose

personality_quiz_results stores authenticated tenant users' Emotional Intelligence and Personality quiz completion history. The backend owns tenant scope, user ownership, quiz kind, quiz version identifiers, weekly EI completion windows, personality type snapshots, scores, and answer snapshots. It does not write to user employment fields.

Slice Files (by layer)

  • Route: src/routes/personality_quiz_results.ts (thin wiring; GET /me, GET /me/history, PUT /me, GET /distribution, GET /completion).
  • Controller: src/api/controllers/personality_quiz_results.controller.ts (custom — not the CRUD factory).
  • Service (BLL): src/services/personality_quiz_results.ts.
  • Repository (DAL): queries run through db.personality_quiz_results inside the service (no separate db/api/personality_quiz_results.ts).
  • Model: src/db/models/personality_quiz_results.ts.
  • Shared used: db/with-transaction.ts (withTransaction); services/shared/access.ts (getOrganizationIdOrGlobal, getCampusId, assertAuthenticatedTenantUser, hasFeaturePermission); shared/errors/* (ForbiddenError, ValidationError).

API

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

  • GET /api/personality_quiz_results/me -> 200. Query quiz_kind can be ei_self_assessment or personality_type. Returns the current user's latest saved result DTO for that kind, or null if none exists. EI self-assessment reads are limited to the current Sunday-start week. Personality type reads use the user's latest historical result.
  • GET /api/personality_quiz_results/me/history -> 200. Optional query quiz_kind can be ei_self_assessment or personality_type; optional limit defaults to 25 and is capped at 100. Returns { rows, count } for the current user's saved EI and personality quiz history ordered by completed_at descending. This endpoint is used by the profile page so historical completions remain visible after weekly EI windows roll forward.
  • PUT /api/personality_quiz_results/me -> 200. Request body wrapped as { data: { quiz_kind, quiz_id, quiz_title, quiz_answers, total_questions, ... } }. Creates a new completion-history row and returns the saved 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.
  • GET /api/personality_quiz_results/distribution -> 200. Optional query campusId. Returns { rows: [{ type, count }], count } (number of distinct personality types). Restricted to report roles.
  • GET /api/personality_quiz_results/completion -> 200. Optional query quiz_kind. Returns staff completion rows with current-week EI self-assessment status and latest personality type status. Restricted to READ_PERSONALITY_REPORTS.

Access Rules

  • getCurrentUserResult / getCurrentUserHistory / upsertCurrentUserResult: any authenticated tenant user (assertAuthenticatedTenantUser); each user reads and writes only their own result (filtered by userId).
  • upsertCurrentUserResult persists only when the active scope is the user's own scope. Parent users drilled into a child school/campus/classroom can complete the UI flow there, but the backend does not create or update reportable quiz rows for that child scope.
  • distribution and completion: restricted to READ_PERSONALITY_REPORTS; otherwise ForbiddenError. Role-seeded permissions are only the baseline grants. The distribution response contains only type and count per group — no individual names or answers. 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 can see their results across organizations; regular users are bound to their org.
  • On upsert, campusId is set from getCampusId (the current user's direct campus, else the user's campusId, else null); 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 reads and reports use the active scope for visibility.
  • distribution filters by organization via getOrganizationIdOrGlobal (global users see all orgs) and, when a campusId query value is provided, additionally by that campus.

Data Contract

  • Mutation input (PUT /me): quiz_kind, quiz_id, quiz_title, quiz_answers, and total_questions. Personality type submissions also require personality_type; EI submissions may include score, result_label, and result_payload. Invalid input raises ValidationError.
  • On save, personality types are trimmed and upper-cased; completed_at is set to the current time. EI self-assessment rows get week_of set to the current Sunday-start week. Personality type rows keep week_of = null because the type quiz is not a weekly workflow.
  • DTO fields: id, quiz_kind, quiz_id, quiz_title, personality_type, quiz_answers, score, total_questions, result_label, result_payload, week_of, user_name, user_role, completed_at, organizationId, campusId, userId, createdById, updatedById, createdAt, updatedAt.
  • Model columns: quiz_kind, quiz_id, quiz_title, personality_type, quiz_answers, score, total_questions, result_label, result_payload, week_of, user_name, user_role, completed_at, importHash, 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

  • upsertCurrentUserResult first checks whether the caller is acting in their own scope. If not, it skips persistence and returns null. Otherwise it creates a new history row inside withTransaction; old completions are never overwritten.
  • getCurrentUserResult orders by completed_at desc and returns the latest match. For ei_self_assessment, the query includes current week_of; for personality_type, it does not.
  • getCurrentUserHistory returns the user's saved EI and personality completions across weeks and quiz versions. It never exposes another user's rows and does not require report permissions.
  • distribution keeps each user's latest personality type result, counts those current types, and orders groups by count desc; count in the response is the number of distinct types returned.
  • completion combines both categories: current-week EI self-assessment results and latest all-time personality type results for each reportable staff user.

Tests

  • src/services/personal_scope_results.test.ts verifies that parent users drilled into child scopes do not create personality quiz rows, that current-user EI reads are week-scoped, that profile history reads persisted rows for the current user, and that completion reporting combines weekly EI with all-time personality type results.
  • Frontend: frontend/docs/personality-integration.md, frontend/docs/personality-catalog.md.
  • Related slices: safety-quiz-results.md, walkthrough-checkins.md, user-progress.md (similar per-user tenant-scoped result/progress pattern).