40227-vm/backend/docs/personality-quiz-results.md
2026-06-12 06:55:35 +02:00

4.6 KiB

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

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