# 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. ## Related - 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).