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