# Backend (Postman) vs Frontend — fields & endpoints to decide on

Source of truth: the Postman collection covering Terms, Courses, Sessions, Exams, Homeworks, Media (2026-05). This doc lists every place where backend and FE disagree on shape, plus FE-side concepts the backend doc has no slot for. Each item needs a product/design call before we wire it up.

Conventions in the rest of this doc:

- **B → FE** means the backend exposes a field/endpoint that the FE doesn't surface yet.
- **FE → B** means the FE shows/sends a field the backend doc doesn't accept.
- **shape diff** means both sides handle the concept but in different shapes (enum values, nesting, naming).

---

## Terms

### Endpoints

| Backend | FE today | Status |
| --- | --- | --- |
| `GET /terms?per_page&active_only` | `getTermsList` | aligned |
| `GET /terms/:id` | `showTerm` | aligned |
| `POST /terms` | `addNewTerm` | aligned |
| `PATCH /terms/:id` | `updateTerm` | aligned |
| `DELETE /terms/:id` | `deleteTerm` | aligned |
| — | `cloneTerm` (`POST /admin/terms/:id/clone`) | **FE-only; backend has nothing.** Decide: drop the clone button, or ask backend to add it. |
| — | `changeStatusTerm` (`POST /admin/terms/:id/status`) | **Use `PATCH /terms/:id` with `is_active`** — dedicated status endpoint dropped. |
| — | `listUserTerm`, `addUserTerm`, `removeUserTerm`, `changeLeaveStatus` | **FE-only.** Term-students subtab + leave toggle. Backend exposes nothing equivalent. Keep mock-only until backend adds. |
| — | `listCourseTerm`, `addCourseTerm`, `removeCourseTerm` | **FE-only.** Can be replaced by `GET /courses?term_id=` for the list; the attach/detach side has no backend equivalent (course `term_id` is set at create-time). |

### Fields

| Backend → FE | FE has it as | Notes |
| --- | --- | --- |
| `starts_at`, `ends_at` (ISO datetime) | `startDate`, `endDate` | Naming diff; we send `starts_at`/`ends_at` in the mock already — confirm the FE form should rename or keep an adapter. |
| `cover_url` (read) + `cover_media_id` (write) | `coverUrl` / `coverMediaId` | aligned in shape, just camelCase. |
| `created_at` | `createdAt` | aligned. |
| `is_active` | `isActive` | aligned. |
| `description` | `description` | aligned. |

**FE → B** (need decision):

- `studentsCount`, `coursesCount` — computed UI counts. Backend response doesn't include them. Either compute client-side or ask backend for `?include=counts`.
- `image` (UI alias of cover) — FE keeps this for a fallback. Backend response only has `cover_url`.

---

## Courses

### Endpoints

| Backend | FE today | Status |
| --- | --- | --- |
| `GET /courses?term_id&per_page` | `getCoursesList` | aligned (drop `/admin/` prefix in `endpoints.js`). |
| `GET /courses/:id` | `showCourse` | aligned. Backend response includes nested `term` and `teacher` — FE already reads `course.term` / `course.teacher`, good. |
| `POST /courses` | `addNewCourse` | aligned. Backend currently requires `term_id` (422 example), but per user instruction the standalone (template-tab) flow must allow `term_id: null`. **Decide:** ask backend to make `term_id` nullable, or refuse to submit until a term is picked. |
| `PATCH /courses/:id` | `updateCourse` | aligned. Used now for status toggle too (drops dedicated `/status` endpoint). |
| `DELETE /courses/:id` | `deleteCourse` | aligned. |
| — | `changeStatusCourse` (`/admin/courses/:id/status`) | **Dropped.** Use `PATCH /courses/:id { is_active }`. |
| — | `listCourseStudents`, `addCourseStudent`, `removeCourseStudent` | **FE-only.** CourseDetailsModal "students" tab + `AddCourseStudentModal`. Backend doesn't expose course-students; either add a sub-resource or remove the UI. Kept mock-only for now. |
| — | `listCourseSessions`, `attachCourseSession`, `detachCourseSession` | **FE-only.** CourseDetailsModal "sessions" tab can be served by `GET /sessions?course_id=`. The attach/detach side (M:N) has no backend equivalent — backend uses session.course_id (1:N). Kept mock-only for now; recommend swapping the list tab to the regular sessions query. |

### Fields

| Backend ↔ FE | Notes |
| --- | --- |
| `term_id` ⇄ `termId` | aligned. Nullable in CourseFormPage, required in AddOfferedCourseModal — schema reflects this. |
| `teacher_id` ⇄ `teacherId` | aligned. |
| `capacity` ⇄ `capacity` | aligned. |
| `is_active` ⇄ `isActive` | aligned. |
| `cover_media_id` (write) / `cover_url` (read) ⇄ `coverMediaId` / `coverUrl` | aligned. |
| `description` ⇄ `description` | aligned. |
| Nested `term`, `teacher` on show response | FE already reads. |

**FE → B** (need decision):

- `sessionsCount` — number-of-sessions field on the create form. Backend has nothing. Either compute server-side from related sessions, or drop the field.
- `prerequisites` (array of { courseId, course }) — backend has no prerequisite relation. Drop or ask backend for it.
- `contentType` (video/voice/text) + `contentMediaId` — single course-level content file. Backend treats files only as session media. Decide whether course-level content should move to "intro session" or stay a course concept.
- `image` (UI alias of cover_url) — kept as a fallback alongside `coverUrl`.

---

## Sessions

### Endpoints

| Backend | FE today | Status |
| --- | --- | --- |
| `GET /sessions?course_id&per_page` | `getSessionsList` | aligned. |
| `GET /sessions/:id` | `showSession` | aligned. |
| `POST /sessions` | `addNewSession` | aligned. |
| `PATCH /sessions/:id` | `updateSession` | aligned. Used for status toggle now. |
| `DELETE /sessions/:id` | `deleteSession` | aligned. |
| — | `changeStatusSession` (`/admin/sessions/:id/toggle-status`) | **Dropped.** Use PATCH with `is_active`. |
| — | `getSessionsAttendance` (`/admin/sessions/:sessionId/attendances`) | **FE-only.** `SessionAttendanceModal` depends on this. Kept mock-only. |

### Fields — biggest gap of all five resources

| Backend | FE today | Notes |
| --- | --- | --- |
| `type` enum: `online \| offline \| content` | `sessionType` enum: `in_person`, `online`, `video`, `audio`, `text`, `slide`, `pdf` | **Shape diff.** FE has 7 values; backend has 3. There's an existing `SESSION_TYPE_TO_SPEC` mapper in `services/mock/fixtures/admin-sessions.js`. Decide whether the FE keeps the richer 7-value enum (and we map down to backend's 3) or collapses. |
| `starts_at`, `location`, `link` | All three live **inside** `form.sessionConfig.*` plus also derived to top-level `startsAt` / `location` / `link` in the mock | **Structural diff.** Backend wants flat fields; FE form nests them under `sessionConfig` keyed by `sessionType`. The mock derives top-level from `sessionConfig.*` for show payloads. Decide whether the FE form should flatten the schema to match backend or keep the conditional-by-type config UI. |
| `media_ids[]` (write) / `media[]` (read with `collection_name`, `file_name`, `mime_type`, `file_size`, `url`, `download_url`) | `materials[]` with `{ fileId, isRequired, type, title, order }` | **Shape diff.** Backend's media rows are typed by upload-purpose (video/voice/pdf/slide/attachment); FE has its own `type` enum. Decide which shape the FE keeps. |
| — | `durationMinutes`, `order`, `sessionConfig.minWatchedPercent`, `sessionConfig.minReadPercent`, `sessionConfig.mustCompleteBeforeNext`, `sessionConfig.platform` | **FE → B**, all UI-only fields. Backend has nothing equivalent. Drop, move into a `metadata` JSON, or ask backend to add. |
| — | `usedInTerms` | UI-only count, no backend. |
| — | `image` (vs `media`) | UI-only thumbnail; backend doesn't separate. |

---

## Exams

### Endpoints

| Backend | FE today | Status |
| --- | --- | --- |
| `GET /exams/:id` (with questions+options) | `showExam` | aligned. |
| `POST /exams` | `addNewExam` | aligned. |
| `PATCH /exams/:id` | `updateExam` | aligned. |
| `DELETE /exams/:id` | `deleteExam` | aligned. |
| `POST /exams/:examId/questions` | — | **Backend → FE.** New endpoint. Today the FE submits the whole question list inside the exam create/update payload. Decide whether to keep "all-in-one" submission (and ask backend to accept it) or switch to add-questions-after-create. |
| `POST /questions/:questionId/options` | — | **Backend → FE.** Same as above — backend lets you add options one at a time. FE today bundles all options with the question. |
| `POST /exams/:examId/submit` | — (student-side feature) | **Backend → FE.** Student-side; not in current admin UI. |
| — | `getExamsList` (`/admin/exams`) | **FE-only.** ExamsListPage depends on it. Kept mock-only; ask backend to add a list endpoint. |
| — | `getExamParticipants`, `showExamParticipant` | **FE-only.** ExamParticipantsModal + ExamParticipantDetailsModal depend on these. Kept mock-only. |

### Fields

| Backend ↔ FE | Notes |
| --- | --- | --- |
| `session_id` ⇄ `sessionId` | aligned. |
| `title` ⇄ `title` | aligned. |
| `description` ⇄ `description` | aligned. |
| `pass_score` ⇄ `passingScore` | aligned (naming diff). |
| `is_active` | **Backend → FE.** Exam form has no active toggle. Decide whether to add it. |
| Backend question shape: `{ question_text, position, options: [{ option_text, is_correct }] }` | FE: `{ title, score, correctAnswerId, answers: [{ id, title }] }` | **Shape diff.** Backend hides `is_correct` from public reads (only on add). FE concept of `score` (per-question weighting) has no backend slot. Decide: keep FE scoring (ask backend to store) or drop. |

**FE → B** (need decision):

- `durationMinutes` — no backend slot.
- `randomize` — no backend slot.
- `endDate` / `startDate` — exam validity window, no backend slot.
- `usedInTerms` — derived UI count.

---

## Homeworks (FE calls them "assignments")

### Endpoints

| Backend | FE today | Status |
| --- | --- | --- |
| `POST /homeworks` | `addNewAssignment` | aligned (URL renamed to `/homeworks`; FE key name kept). |
| `PATCH /homeworks/:id` | `updateAssignment` | aligned. |
| `DELETE /homeworks/:id` | `deleteAssignment` | aligned. |
| `POST /homeworks/:homeworkId/submit` (student) | — | **Backend → FE.** Student submit, not in admin UI yet. |
| `PATCH /homework-submissions/:submissionId/review` | `reviewAssignmentSubmission` | aligned (URL renamed). |
| — | `getAssignmentsList` | **FE-only.** AssignmentsListPage depends on it. Kept mock-only. |
| — | `showAssignment`, `getAssignmentSubmissions`, `showAssignmentSubmission` | **FE-only.** Detail + submissions list — kept mock-only. |

### Fields

| Backend ↔ FE | Notes |
| --- | --- | --- |
| `session_id` ⇄ `sessionId` | aligned. |
| `title`, `description` | aligned. |
| `deadline` (single datetime) | FE: `startDate` + `endDate` + computed `durationDays` | **Shape diff.** Backend has one deadline; FE has a window. Decide: drop start/end and use single deadline, or ask backend to add a window. |
| `is_active` | **Backend → FE.** FE form has no active toggle. |
| Submission `status`: `accepted \| denied` | FE: `pending \| approved \| rejected \| needs_revision` | **Shape diff.** Backend has two states; FE has four. The FE `pending/needs_revision` have no backend slot. |
| Submission `media_id` (single) | FE `attachments[]` (multiple) | **Shape diff.** Backend allows one file per submission; FE expects many. |
| Submission `teacher_feedback` ⇄ `reviewerNote` | naming diff. |
| Submission `reviewed_at` (read) | — | Backend provides; FE doesn't surface. |

**FE → B** (need decision):

- `priority` enum (`mandatory \| optional`) — no backend slot.
- `submissionsCount` — derived count, no backend slot.

---

## Media

### Endpoints

| Backend | FE today | Status |
| --- | --- | --- |
| `POST /media` (multipart with `purpose` + `file`) | `uploadMedia` | aligned URL & method. |
| `GET /media/:id/download` | — | **Backend → FE.** Add as `downloadMedia`. |
| `DELETE /media/:id` | — | **Backend → FE.** Add as `deleteMedia`. |

### Fields

- Backend response: `{ id, collection_name, file_name, mime_type, file_size, url, download_url }`. FE today reads `payload.id`, `payload.url`, sometimes `payload.uploadId` (a now-stale field). **Cleanup needed:** drop `uploadId`, use `id` everywhere.
- Backend `purpose` enum: `avatar | cover | video | voice | pdf | slide | attachment | homework_file`. FE upload calls currently hardcode `purpose: 'cover'` or `purpose: 'content'`. **`content` is not in the backend enum.** Decide which of the backend purposes each FE uploader should send (e.g., session video → `video`, course PDF → `pdf`, homework upload → `homework_file`).
- Pending media TTL: backend deletes unreferenced pending uploads after 24h. FE doesn't track this; if a user uploads a cover, abandons the form, and comes back next day, the `cover_media_id` reference will 404 on submit. Document the failure mode.

---

## Cross-cutting

### Snake_case vs camelCase

Backend wire format is snake_case throughout. FE today reads camelCase (e.g., the mock returns `coverUrl`, `isActive`). When the real backend lands, the FE will either need:

- a HTTP-layer transformer (camelize on response, snake_case on request), or
- camelCase field aliases on the backend serializer.

Decide before swapping the mock for the real API. Affects every screen.

### `/admin/` URL prefix

Backend doc has none — endpoints live at `/terms`, `/courses`, etc. The FE previously had `/admin/courses`, `/admin/sessions`, `/admin/exams`, `/admin/assignments`. **Aligned to backend (prefix dropped).** Auth context (admin role) is implicit in the token, not the URL.

### Sub-features that depend on missing backend endpoints

UI screens that work today against the mock but have no backend equivalent in the current doc (kept mock-only with TODOs in `endpoints.js`):

- Term: clone term, term status toggle, students-in-term subtab, leave toggle, courses-in-term subtab (could swap to `GET /courses?term_id=`)
- Course: status toggle (swapped to PATCH), students-in-course subtab, attach/detach sessions (M:N), `AddCourseStudentModal`, `AddSessionToCourseModal`
- Session: status toggle (swapped to PATCH), attendance roster
- Exam: list page, participants list, participant detail
- Assignment/Homework: list page, detail show, submissions list, submission detail

For each of these we need to either (a) ask backend to expose the endpoint, or (b) drop the UI surface.
