# FRAME Entries Backend ## Purpose `frame_entries` stores weekly F.R.A.M.E. focus entries per organization. The backend is the source of truth for persisted FRAME data; the frontend never substitutes static samples for it. ## Slice Files (by layer) - Route: `src/routes/frame_entries.ts` (thin wiring; `GET /`, `POST /`, `PUT /:id`). - Controller: `src/api/controllers/frame_entries.controller.ts` (custom — not the CRUD factory). - Service (BLL): `src/services/frame_entries.ts`. - Repository (DAL): queries run through `db.frame_entries` inside the service (no separate `db/api/frame_entries.ts`). - Model: `src/db/models/frame_entries.ts`. - Shared used: `db/with-transaction.ts` (`withTransaction`), `services/shared/access.ts` (`getOrganizationIdOrGlobal`, `hasRoleAccess`), `shared/constants/pagination.ts` (`resolvePagination`), `shared/constants/frame.ts` (`FRAME_EDITOR_ROLE_NAMES`), `shared/errors/*` (`ForbiddenError`, `ValidationError`). ## API All routes require JWT authentication. - `GET /api/frame_entries` -> `200` `{ rows, count }` for the current user's organization (paginated via `resolvePagination`). - `POST /api/frame_entries` -> `201` the created entry DTO. - `PUT /api/frame_entries/:id` -> `200` the updated entry DTO (scoped to the org). Request body for create/update is wrapped as `{ data: }`. ## Access Rules - Read: any authenticated user in the organization, or any user with `globalAccess` (sees all organizations). - Edit (create/update): restricted to roles in `FRAME_EDITOR_ROLE_NAMES` (director/superintendent capabilities) — `super_admin`, `system_admin`, `owner`, `superintendent`, `director`. Enforced by `assertCanEdit` via `hasRoleAccess`; a non-editor gets `ForbiddenError`. Frontend may hide editing controls, but the backend check is authoritative. ## Tenant Scope - Organization is resolved via `getOrganizationIdOrGlobal`: users with `globalAccess` bypass the org filter and see/create entries across all organizations; regular users are bound to their organization. - `campusId` is optional; when omitted it defaults to the current staff profile's campus (`currentUser.staff_user[0].campusId`) when available, else `null`. ## Data Contract Required request fields (`REQUIRED_FIELDS`): `week_of`, `posted_date`, `formal`, `recognition`, `application`, `management`, `emotional`, `author`. Optional: `week_label`, `campusId`. Missing/invalid input raises `ValidationError`. `week_of` is the **week the entry covers**, sent as a `YYYY-MM-DD` date and stored as the **canonical Sunday-start ISO date** (American week) — the server normalizes it via `toWeekStartIso` (`shared/constants/week.ts`) and rejects non-dates. `week_label` is an optional free-text note for that week (e.g. "Spring Break week"), stored trimmed or `null`. ## Behavior / Notes - Create/update run inside `withTransaction`. - List is paginated with the shared defaults (`resolvePagination`). - The same Sunday-start canonicalization is used on the frontend (`shared/business/week.ts`) for the dashboard hero, the safety-quiz week, and the F.R.A.M.E. week picker, so the week is consistent everywhere. ## Tests - **Unit** (`npm test`): `shared/constants/week.test.ts` — the Sunday-start `week_of` normalization (`toWeekStartIso`) + invalid-date rejection. - **Seeded e2e** (`frontend/tests/e2e/product-workflow.seeded.e2e.ts`, `npm run test:e2e:content`): a director posts a FRAME entry and reads it back, asserting the persisted `week_of` is normalized to its Sunday week-start. ## Related - Frontend: `frontend/docs/frame-integration.md`. - Related slices: `user-progress.md` (dashboard zone check-ins), `staff` (campus resolution).