90 lines
4.6 KiB
Markdown
90 lines
4.6 KiB
Markdown
# Personality Quiz Results Backend
|
|
|
|
## Purpose
|
|
|
|
`personality_quiz_results` stores each authenticated tenant user's current personality quiz
|
|
result (one row per user per organization) and exposes an aggregate distribution of personality
|
|
types for leadership reporting. The backend owns tenant scope, user ownership, the saved
|
|
personality type, and the answer snapshot. It does not write to staff profile records.
|
|
|
|
## Slice Files (by layer)
|
|
|
|
- Route: `src/routes/personality_quiz_results.ts` (thin wiring; `GET /me`, `PUT /me`,
|
|
`GET /distribution`).
|
|
- 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`, `hasRoleAccess`);
|
|
`shared/constants/personality.ts` (`PERSONALITY_REPORT_ROLE_NAMES`); `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`. Returns the current user's saved result DTO
|
|
(most recently updated), or `null` if none exists.
|
|
- `PUT /api/personality_quiz_results/me` -> `200`. Request body wrapped as
|
|
`{ data: { personality_type, quiz_answers } }`. Creates or updates the current user's result and
|
|
returns the saved DTO.
|
|
- `GET /api/personality_quiz_results/distribution` -> `200`. Optional query `campusId`. Returns
|
|
`{ rows: [{ type, count }], count }` (number of distinct personality types). Restricted to report
|
|
roles.
|
|
|
|
## Access Rules
|
|
|
|
- `getCurrentUserResult` / `upsertCurrentUserResult`: any authenticated tenant user
|
|
(`assertAuthenticatedTenantUser`); each user reads and writes only their own result (filtered by
|
|
`userId`).
|
|
- `distribution`: restricted to `PERSONALITY_REPORT_ROLE_NAMES` (`super_admin`,
|
|
`system_admin`, `owner`, `superintendent`, `director`) or any role with
|
|
`globalAccess`, enforced by `hasRoleAccess`; otherwise `ForbiddenError`. The distribution
|
|
response contains only `type` and `count` per group — no individual names or answers.
|
|
|
|
## 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 staff profile's campus, else the
|
|
user's `campusId`, else `null`); `userId`, `createdById`, `updatedById` come from the current
|
|
user.
|
|
- `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`): `personality_type` (non-empty string) and `quiz_answers` (a non-array
|
|
object whose values are all non-empty strings). Invalid input raises `ValidationError`.
|
|
- On save, `personality_type` is trimmed and upper-cased; `completed_at` is set to the current
|
|
time.
|
|
- DTO fields: `id`, `personality_type`, `quiz_answers`, `completed_at`, `organizationId`,
|
|
`campusId`, `userId`, `createdById`, `updatedById`, `createdAt`, `updatedAt`.
|
|
- Model columns: `personality_type` (TEXT, not null), `quiz_answers` (JSONB, not null),
|
|
`completed_at` (DATE, not null), `importHash` (unique), 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` runs inside `withTransaction`: it looks up the existing row by
|
|
`organizationId` + `userId` and updates it, otherwise creates a new one.
|
|
- `getCurrentUserResult` orders by `updatedAt` desc and returns the first match.
|
|
- `distribution` groups by `personality_type` with a `COUNT(id)` aggregate, ordered by count desc;
|
|
`count` in the response is the number of distinct types returned.
|
|
|
|
## Tests
|
|
|
|
None yet (no `personality_quiz_results` unit/e2e test in `src/`).
|
|
|
|
## 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).
|