122 lines
7.4 KiB
Markdown
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).
|