# 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`, `hasFeaturePermission`), `shared/constants/pagination.ts` (`resolvePagination`),
  `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: <FrameEntryInput> }`.

## Access Rules

- Read: any authenticated user in the organization, or any user with `globalAccess` (sees all
  organizations).
- Edit (create/update/delete): requires `MANAGE_FRAME`. Role-seeded permissions
  are only the baseline grants. Per-user `custom_permissions` can grant it and
  `custom_permissions_filter` can remove it for non-global users.

## 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 user's direct campus scope 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), `users` (campus resolution).
