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

122 lines
7.4 KiB
Markdown

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