# Backend API — response model gaps

This document tracks fields the frontend already consumes that are **not yet present** in the current backend response models. Each section lists:

- **Postman / current spec**: what the backend returns today.
- **UI also needs**: fields the frontend mock fixture exposes and the UI reads from. These must be added to the real response to remove the mock.
- **Used by**: where on the frontend the field is consumed.

Naming convention: the backend uses `snake_case`; the HTTP client camelizes responses, so this doc lists backend keys in `snake_case` and notes the camelCase form the UI sees.

---

## ⚠️ Cross-cutting: `name` vs `first_name` / `last_name`

The current Postman models expose only a single `name` field on the user object. **The UI cannot work with `name` alone.**

- The signup form (`/register`) collects **two separate inputs**: _نام_ (first name) and _نام خانوادگی_ (last name). If the backend only stores `name`, these two inputs collapse on the server and cannot be returned to the UI later.
- After signup, `GET /auth/me` is the source of truth for the authenticated user — the layout header, the dashboard, and `EditProfilePage.vue` all read `firstName` / `lastName` (not `name`). A `/auth/me` response without `first_name` / `last_name` leaves these views blank.
- Every other user-returning endpoint has the same need (`/admin/users` list rows, `/admin/users/:id` detail modal, `/admin/users` create, `/admin/users/:id` update, `/admin/users/:id/role` assign-roles response, login response `data.user`, register response, etc.).

**Required action on the backend side:**

1. Persist `first_name` and `last_name` as **independent columns** on the user table — accept them as separate keys in every write endpoint that touches the user (`/register`, `POST /admin/users`, `PATCH /admin/users/:id`).
2. Return `first_name`, `last_name`, **and** the convenience `full_name` (or keep `name`) in **every** user-returning response. The UI will continue reading `firstName` / `lastName`; `fullName` / `name` are used only where a single display string is needed (tables, badges).
3. If the existing data already only has `name`, do a one-time migration (split on the first whitespace, fall back to leaving `last_name` empty for single-token names). New writes must keep the two columns in sync with `name` / `full_name`.

The per-endpoint tables below repeat `first_name` / `last_name` under each "Missing — UI also needs" section so the backend dev cannot miss it on individual routes; this top section explains _why_ it is the same answer everywhere.

---

## POST `/media/upload` — single-step media upload (canonical)

**This is the only media-upload endpoint in the project.** Earlier revisions of this doc referenced per-resource cover endpoints (`POST /terms/:id/image`, `POST /courses/:id/image`, `POST /sessions/:id/media`) — those were a mistake and have been removed from the FE, the API client, and the mock.

### Flow

1. User picks a file (avatar / cover / attachment) in the form.
2. FE posts the file as `multipart/form-data` to `POST /media/upload` (was `POST /upload-temp`).
3. Backend stores the file and returns its media `id`.
4. FE puts that id on the resource create/update body under whichever key the resource expects: `avatar_id`, `image_id`, `cover_id`, `material_id`, etc.
5. The resource endpoint (`POST /courses`, `PATCH /terms/:id`, `POST /admin/users`, …) resolves the id and attaches the media.

The previous mock endpoint path was `/upload-temp`. It now lives at `/media/upload`. The FE function `useUploadTemporaryMutation` was renamed to `useUploadMediaMutation` (consumed by the profile form, admin-user form, admin-term form, admin-course forms, admin-session form, and the student-register media/personal-info components).

### Mock request

`multipart/form-data` with a single `file` field.

### Mock response

```json
{
  "success": true,
  "message": "Media uploaded.",
  "data": {
    "id": 1234,
    "upload_id": 1234,
    "url": "https://…/240/240"
  }
}
```

`upload_id` is an alias for `id` and is what older FE code reads (`form.value.imageId = payload?.uploadId || payload?.id`). Backend should return both keys, or eventually the FE migrates to reading only `id`.

### How the resource body references the upload

| Resource form | Field on the body | Backend should treat it as a media id |
| --- | --- | --- |
| `/auth/me` profile | `avatar_id` | yes |
| `POST /admin/users` | `avatar_id` | yes |
| `POST /terms` | `image_id` | yes — sets `cover_url` |
| `POST /courses` | `image_id` | yes — sets `cover_url` |
| `POST /sessions` | `image_id` / `materials[]` | yes — `image_id` sets cover; `materials` array carries session attachments by their media ids |

> **Decision pending:** rename FE field `image_id` → `media_id` across the board, or have the backend accept the resource-specific field names. The mock currently honors the resource-specific names.

---

## GET `/auth/me` — current user

Mock fixture: `src/services/mock/fixtures/me.js` (`currentMe`). Frontend query: `useGetMeQuery` in `src/services/query/auth.js`.

> This endpoint is **the source of truth for the logged-in user** immediately after `/register` (signup). Anything the signup form collects but `/auth/me` doesn't return is effectively lost from the UI's perspective. See the cross-cutting `name` vs `first_name`/`last_name` note above before reviewing this section.

### Spec (already implemented)

| Backend key           | UI key (camelCase)  | Type     | Notes                       |
| --------------------- | ------------------- | -------- | --------------------------- |
| `id`                  | `id`                | number   |                             |
| `name`                | `name`              | string   | Full display name           |
| `email`               | `email`             | string   |                             |
| `phone`               | `phone`             | string   | E.164, e.g. `+989121234567` |
| `roles`               | `roles`             | string[] | e.g. `["student"]`          |
| `avatar_url`          | `avatarUrl`         | string   | Public avatar URL           |
| `avatar_download_url` | `avatarDownloadUrl` | string   | Signed/download endpoint    |
| `created_at`          | `createdAt`         | string   | ISO 8601                    |

### Missing — UI also needs

| Backend key (proposed) | UI key (camelCase) | Type | Used by |
| --- | --- | --- | --- |
| `first_name` | `firstName` | string | `EditProfilePage.vue` form, header/greeting |
| `last_name` | `lastName` | string | `EditProfilePage.vue` form |
| `full_name` | `fullName` | string | Lists/tables that render a single name string |
| `phone_number` | `phoneNumber` | string | `EditProfilePage.vue` form (local format, e.g. `09121234567`) |
| `national_code` | `nationalCode` | string | `EditProfilePage.vue` form, identity validation |
| `status` | `status` | string | enum: `pending` \| `approved` \| `rejected` — gates access in the UI |
| `address.address` | `address.address` | string | `EditProfilePage.vue` address textarea |
| `address.province.id` | `address.province.id` | number | `EditProfilePage.vue` province select |
| `address.province.name` | `address.province.name` | string |  |
| `address.city.id` | `address.city.id` | number | `EditProfilePage.vue` city select |
| `address.city.name` | `address.city.name` | string |  |
| `profile.bio` | `profile.bio` | string | `EditProfilePage.vue` bio textarea |
| `profile.birth_date` | `profile.birthDate` | string | ISO 8601, `EditProfilePage.vue` date picker |
| `profile.marital_status` | `profile.maritalStatus` | string | enum: `single` \| `married` |
| `profile.gender` | `profile.gender` | string | enum: `male` \| `female` |
| `profile.education_status` | `profile.educationStatus` | string | enum |
| `profile.seminary_level` | `profile.seminaryLevel` | string | enum |
| `profile.university_level` | `profile.universityLevel` | string | enum |
| `profile.university_name` | `profile.universityName` | string |  |
| `profile.field_of_study` | `profile.fieldOfStudy` | string |  |
| `profile.avatar_id` | `profile.avatarId` | number \| null | Media id returned by `POST /media/upload` |

### Suggested response shape

```json
{
  "success": true,
  "message": "OK",
  "data": {
    "id": 5,
    "name": "Jane Student",
    "first_name": "Jane",
    "last_name": "Student",
    "full_name": "Jane Student",
    "email": "jane.student@example.com",
    "phone": "+989121234567",
    "phone_number": "09121234567",
    "national_code": "0079827498",
    "status": "approved",
    "roles": ["student"],
    "avatar_url": "http://localhost:8080/storage/users/5/avatar.jpg",
    "avatar_download_url": "http://localhost:8080/api/media/9/download",
    "created_at": "2026-02-01T10:00:00+00:00",
    "address": {
      "address": "...",
      "province": { "id": 1, "name": "تهران" },
      "city": { "id": 11, "name": "تهران" }
    },
    "profile": {
      "bio": "...",
      "birth_date": "1989-06-12T00:00:00.000Z",
      "marital_status": "married",
      "gender": "male",
      "education_status": "graduate",
      "seminary_level": "level_3",
      "university_level": "master",
      "university_name": "تهران",
      "field_of_study": "علوم قرآنی",
      "avatar_id": null
    }
  }
}
```

---

## GET `/admin/users` — list users (paginated)

Mock fixture: `src/services/mock/fixtures/admin-users.js` (`adminUsers`, `makeUser`). Mock route: `src/services/mock/routes/admin-users.js`. Frontend query: `useAdminUsersListQuery` in `src/services/query/admin-users.js`. Consumed by: `src/features/admin/users/pages/UsersListPage.vue`, `src/features/admin/users/components/UsersTable.vue`.

### Spec envelope

```json
{
  "success": true,
  "message": "OK",
  "data": {
    "items": [
      /* user objects */
    ],
    "meta": {
      "current_page": 1,
      "per_page": 20,
      "total": 3,
      "last_page": 1
    }
  }
}
```

Query params the UI already sends: `search`, `per_page`, `page`, `name`, `national_code`, `phone_number`, `role_id`, `from_date`, `to_date`. The spec only documents `search` + `per_page` — backend should accept the rest or the UI filters need to be reworked.

### Per-item spec (already implemented)

| Backend key           | UI key (camelCase)  | Type           |
| --------------------- | ------------------- | -------------- |
| `id`                  | `id`                | number         |
| `name`                | `name`              | string         |
| `email`               | `email`             | string         |
| `phone`               | `phone`             | string \| null |
| `roles`               | `roles`             | string[]       |
| `avatar_url`          | `avatarUrl`         | string \| null |
| `avatar_download_url` | `avatarDownloadUrl` | string \| null |
| `created_at`          | `createdAt`         | string         |

### Per-item missing — UI also needs

| Backend key (proposed) | UI key (camelCase) | Used by |
| --- | --- | --- |
| `first_name` | `firstName` | `UsersTable.vue` (display name), filters |
| `last_name` | `lastName` | `UsersTable.vue`, delete-confirm dialog |
| `phone_number` | `phoneNumber` | `UsersTable.vue` (local format display), `UserDetailsModal.vue` |
| `national_code` | `nationalCode` | `UsersTable.vue`, `UserDetailsModal.vue` |
| `status` | `status` | enum `pending` \| `approved` \| `blocked` — `UserDetailsModal.vue` badge |
| `role_id` | `roleId` | `UsersTable.vue` role-change dropdown |

---

## GET `/admin/users/:id` — show user

Mock route: same file as above. Frontend query: `useAdminUserQuery` in `src/services/query/admin-users.js`. Consumed by: `src/features/admin/users/components/modals/UserDetailsModal.vue` (opened from the users list — _not_ a route page).

### Spec envelope

```json
{
  "success": true,
  "message": "OK",
  "data": {
    /* user object */
  }
}
```

### Spec fields (already implemented)

Same as list-item spec above.

### Missing — UI also needs

Detail modal renders the full profile, so it needs the list-item missing fields **plus** the nested `address` and `profile` blocks:

| Backend key (proposed)  | UI key (camelCase)      | Type             |
| ----------------------- | ----------------------- | ---------------- |
| `address.address`       | `address.address`       | string           |
| `address.province.id`   | `address.province.id`   | number           |
| `address.province.name` | `address.province.name` | string           |
| `address.city.id`       | `address.city.id`       | number           |
| `address.city.name`     | `address.city.name`     | string           |
| `bio`                   | `bio`                   | string           |
| `birth_date`            | `birthDate`             | string (ISO)     |
| `fa_birth_date`         | `faBirthDate`           | string (Jalaali) |
| `marital_status`        | `maritalStatus`         | enum             |
| `gender`                | `gender`                | enum             |
| `education_status`      | `educationStatus`       | enum             |
| `seminary_level`        | `seminaryLevel`         | enum             |
| `university_level`      | `universityLevel`       | enum             |
| `university_name`       | `universityName`        | string           |
| `field_of_study`        | `fieldOfStudy`          | string           |
| `profile_completed`     | `profileCompleted`      | boolean          |

> The modal flattens these (`user.bio`, `user.birthDate`, …) rather than reading from `user.profile.*`, so backend can either return them flat at the top level or the FE select can flatten them — whichever is easier. Document this once decided.

### 404 — Not found

```json
{
  "success": false,
  "message": "Resource not found."
}
```

The FE `findOrThrow` mock throws a 404; the toast/error layer surfaces the `message` field — keep that key name.

---

## POST `/admin/users` — create user

Mock route: `src/services/mock/routes/admin-users.js`. API client: `apiAddAdminUser` in `src/services/api/admin-users.js`. Query: `useAddAdminUserMutation` in `src/services/query/admin-users.js`. Consumed by: `src/features/admin/users/pages/UserFormPage.vue` (add mode).

### Spec request body

```json
{
  "name": "New Teacher",
  "email": "teacher.new@example.com",
  "phone": "+989120000000",
  "password": "StrongPass123!",
  "roles": ["teacher"]
}
```

### What the UI form actually sends today

`UserFormPage.vue` posts a much richer payload (snake-cased by the HTTP interceptor before going out). The keys that map to the spec are marked with `→ spec`; the rest have no spec equivalent yet:

| FE form key (camelCase) | Wire key (snake_case)   | Maps to spec?                              |
| ----------------------- | ----------------------- | ------------------------------------------ |
| `firstName`             | `first_name`            | → derives `name` together with `lastName`  |
| `lastName`              | `last_name`             | → derives `name`                           |
| `phoneNumber`           | `phone_number`          | → maps to `phone` (needs `09…` → `+98…`)   |
| `roleId`                | `role_id`               | → maps to `roles` (single id → name array) |
| `password`              | `password`              | → `password`                               |
| `passwordConfirmation`  | `password_confirmation` | (spec doesn't document, FE still sends)    |
| `nationalCode`          | `national_code`         | missing                                    |
| `birthDate`             | `birth_date`            | missing                                    |
| `maritalStatus`         | `marital_status`        | missing                                    |
| `gender`                | `gender`                | missing                                    |
| `bio`                   | `bio`                   | missing                                    |
| `provinceId`            | `province_id`           | missing                                    |
| `cityId`                | `city_id`               | missing                                    |
| `address`               | `address`               | missing                                    |
| `avatarId`              | `avatar_id`             | missing (returned by `POST /media/upload`) |

> **The form has no `email` input.** The spec requires `email`. Either the backend should treat `email` as optional (auto-generate or accept null), or the FE form needs a new field. Decision pending.

### Spec response — `201 Created`

```json
{
  "success": true,
  "message": "User created.",
  "data": {
    /* user object, same shape as GET /admin/users/:id */
  }
}
```

The response should carry every field listed for `GET /admin/users/:id` (spec + "ui also needs" — list-item missing fields and the nested `address`/`profile` blocks), so the UI can refresh the cache without an extra round-trip.

### Spec response — `422 Unprocessable Entity`

```json
{
  "message": "The given data was invalid.",
  "errors": {
    "email": ["The email field is required."]
  }
}
```

The FE doesn't currently render per-field server errors — it would need a small hook into the form's `errors` ref. Out of scope for the backend, but worth flagging.

---

## PATCH `/admin/users/:id` — update user

Mock route: same file. API client: `apiUpdateAdminUser` — **changed from `PUT` to `PATCH`** to match the spec. Query: `useUpdateAdminUserMutation`. Consumed by: `src/features/admin/users/pages/UserFormPage.vue` (edit mode).

### Spec request body

```json
{
  "name": "Renamed User",
  "roles": ["teacher", "counselor"]
}
```

Spec implies partial update — only the keys present are changed.

### What the UI form actually sends today

Same rich payload as `POST` above (the same form is used in add + edit mode). Notable differences:

- No `password` / `password_confirmation` in edit mode.
- `national_code` is sent but is disabled in the UI when the user already has one.
- The FE still sends only a single role via `role_id`, not an array via `roles`. Backend must either accept `role_id` _or_ the FE must switch to sending `roles: [name]`. Decision pending.

### Spec response — `200 OK`

```json
{
  "success": true,
  "message": "User updated.",
  "data": {
    /* user object, same shape as GET /admin/users/:id */
  }
}
```

Same superset of fields as `POST` response — see `GET /admin/users/:id` table for the full list the UI needs back.

---

## DELETE `/admin/users/:id` — delete user

Mock route: `src/services/mock/routes/admin-users.js`. API client: `apiDeleteAdminUser` in `src/services/api/admin-users.js`. Query: `useDeleteAdminUserMutation` in `src/services/query/admin-users.js`. Consumed by: `src/features/admin/users/pages/UsersListPage.vue` (via `ConfirmModal` → `onConfirm`).

### Spec response — `200 OK`

```json
{
  "success": true,
  "message": "User deleted.",
  "data": null
}
```

The FE only checks for a non-error completion (the mutation's `onSuccess` invalidates the list cache); the response body isn't read. No additional fields needed.

> The previous mock returned a Persian success message (`حذف موفق`) under a `data.message` key. That has been replaced by the spec envelope above. If a localized toast is needed, the FE composes it client-side rather than reading `data.message`.

---

## PATCH `/admin/users/:id/role` — assign roles

Mock route: `src/services/mock/routes/admin-users.js`. API client: `apiUpdateAdminUserRole` — **changed from `POST` to `PATCH`** to match the spec. Query: `useUpdateAdminUserRoleMutation`. Consumed by: `src/features/admin/users/components/UsersTable.vue` (role-badge dropdown → `onChangeRole`).

### Spec request body

```json
{
  "roles": ["missionary"]
}
```

The FE now sends `{ roles: [targetRoleName] }` exactly per spec.

### Spec response — `200 OK`

```json
{
  "success": true,
  "message": "Roles updated.",
  "data": {
    /* user object, same shape as GET /admin/users/:id */
  }
}
```

The mock additionally writes `roleId` (legacy UI key) onto the user so the same record stays consistent in the in-memory store. The FE mutation only triggers `invalidateUsers` on success, so the envelope itself is not read.

> The mock also accepts legacy bodies (`{ roleId }` and `{ role }`) as a fallback in case other callers exist; new code should send `{ roles: [...] }`.

---

## Terms (`/terms`) — global notes

The previous FE wiring used `/admin/terms` paths; the spec uses `/terms`. All five term endpoints below moved to the `/terms` namespace. The admin-only auxiliary endpoints (`/admin/terms/:id/clone`, `/admin/terms/:id/status`, the term-students sub-tree, the term-courses sub-tree) **stayed** on `/admin/terms` until the backend specs them.

### ⚠️ Field name mismatches (apply to every terms endpoint below)

| FE / mock key (camelCase) | Spec key (snake_case) | Notes |
| --- | --- | --- |
| `image` | `cover_url` | FE reads `term.image` |
| `startDate` | `starts_at` | FE reads `term.startDate` |
| `endDate` | `ends_at` | FE reads `term.endDate` |
| `studentsCount` | _(missing)_ | Used in list view |
| `coursesCount` | _(missing)_ | Used in list view |
| `faStartDate` / `faEndDate` | _(missing)_ | Jalaali-formatted display strings; FE can compute client-side, but if backend provides them the FE saves a `formatJalaaliDate` call |

The mock currently writes **both** the spec key and the legacy UI key (`coverUrl` + `image`, `startsAt` + `startDate`, `endsAt` + `endDate`) onto every fixture and response. **Required action:** backend should either rename to the legacy keys, ship both, or the FE consumers (`TermItem.vue`, `TermFormPage.vue`, `TermsListPage.vue`) need to be migrated to read the spec keys. Decision pending.

---

## GET `/terms` — list terms (paginated)

Mock fixture: `src/services/mock/fixtures/admin-terms.js` (`adminTerms`). Mock route: `src/services/mock/routes/admin-terms.js`. Frontend query: `useAdminTermsListQuery`. Consumed by: `src/features/admin/terms/pages/TermsListPage.vue`, `src/features/admin/terms/components/TermItem.vue`.

### Spec envelope

```json
{
  "success": true,
  "message": "OK",
  "data": {
    "items": [
      /* term objects */
    ],
    "meta": { "current_page": 1, "per_page": 20, "total": 2, "last_page": 1 }
  }
}
```

### Per-item spec

| Backend key   | UI key        | Type           |
| ------------- | ------------- | -------------- |
| `id`          | `id`          | number         |
| `title`       | `title`       | string         |
| `description` | `description` | string         |
| `is_active`   | `isActive`    | boolean        |
| `starts_at`   | `startsAt`    | string (ISO)   |
| `ends_at`     | `endsAt`      | string (ISO)   |
| `cover_url`   | `coverUrl`    | string \| null |
| `created_at`  | `createdAt`   | string (ISO)   |

### Per-item missing — UI also needs

See "Field name mismatches" above + `students_count` and `courses_count` for the term list cards.

Query params the FE sends: `per_page`, `page`, `title` (free-text), `status` (0/1), `from_date`/`to_date`. The spec only documents `per_page` and `active_only=1` — backend should accept the rest or drop them from the FE.

### 401 — Unauthenticated

```json
{ "message": "Unauthenticated." }
```

The global HTTP layer already redirects to `/login` on 401 (see `addUnauthorizeInterceptor` in `src/services/api/http.js`).

---

## GET `/terms/:id` — show term

Mock route: same file. Frontend query: `useAdminTermQuery`. Consumed by: `src/features/admin/terms/pages/TermFormPage.vue` (edit mode), `src/features/admin/terms/components/modals/TermDetailsModal.vue`.

### Spec envelope

```json
{
  "success": true,
  "message": "OK",
  "data": {
    /* term object */
  }
}
```

Same fields as a list item. The detail modal needs the same UI-only keys (`image`, `startDate`, `endDate`, `studentsCount`, `coursesCount`).

### 404 — Not found

```json
{ "success": false, "message": "Resource not found." }
```

---

## POST `/terms` — create term

Mock route: same file. API client: `apiAddAdminTerm`. Query: `useAddAdminTermMutation`. Consumed by: `src/features/admin/terms/pages/TermFormPage.vue` (add).

### Spec request body

```json
{
  "title": "Spring 2026",
  "description": "Spring term covering intro & intermediate units.",
  "is_active": true,
  "starts_at": "2026-03-20",
  "ends_at": "2026-06-20"
}
```

### What the FE form sends today

| FE key (camelCase) | Wire key      | Maps to spec?                             |
| ------------------ | ------------- | ----------------------------------------- |
| `title`            | `title`       | → `title`                                 |
| `description`      | `description` | → `description`                           |
| `startDate`        | `start_date`  | → `starts_at` (key rename needed)         |
| `endDate`          | `end_date`    | → `ends_at` (key rename needed)           |
| `imageId`          | `image_id`    | not in spec body — see "Image flow" below |

**Image flow** (canonical): the FE uploads the file to `POST /media/upload`, gets a media `id` back, and submits it as `image_id` with the term create/update body. The backend should resolve `image_id` against the media table and populate `cover_url`. No separate per-resource upload endpoint exists — the previously documented `POST /terms/:id/image` was a mistake and has been removed.

`is_active` is required by the spec but the FE form doesn't expose a toggle on create (terms are forced to active). The mock defaults it to `true`; backend should treat `is_active` as optional with a default of `true`, or the FE needs a switch on the create form.

### Spec response — `201 Created`

```json
{
  "success": true,
  "message": "Term created.",
  "data": {
    /* term */
  }
}
```

The response should carry every "UI also needs" field from the list section so the FE can refresh the cache without an extra GET.

### Spec errors

- `403` `{ "success": false, "message": "This action is unauthorized." }`
- `422` `{ "message": "The given data was invalid.", "errors": { … } }`

FE does not yet render per-field server errors.

---

## PATCH `/terms/:id` — update term

Mock route: same file. API client: `apiUpdateAdminTerm` — **changed from `PUT` to `PATCH`**. Query: `useUpdateAdminTermMutation`. Consumed by: `src/features/admin/terms/pages/TermFormPage.vue` (edit).

### Spec request body — partial

```json
{ "title": "Spring 2026 (Revised)", "is_active": true }
```

### What the FE form sends today

Same payload as create (the same form handles add + edit). The mock applies a true partial update — keys that are `undefined` in the payload are left unchanged on the stored term.

### Spec response — `200 OK`

```json
{
  "success": true,
  "message": "Term updated.",
  "data": {
    /* term */
  }
}
```

### Spec errors

- `403` `{ "success": false, "message": "This action is unauthorized." }`

---

## DELETE `/terms/:id` — delete term

Mock route: same file. API client: `apiDeleteAdminTerm`. Query: `useDeleteAdminTermMutation`. Consumed by: `src/features/admin/terms/pages/TermsListPage.vue` (via `ConfirmModal`).

### Spec response — `200 OK`

```json
{ "success": true, "message": "Term deleted.", "data": null }
```

### Spec errors

- `403` `{ "success": false, "message": "This action is unauthorized." }`

The previous mock returned a Persian success message under `data.message` — replaced by the spec envelope. FE only invalidates the list cache on success and never reads the body.

---

## Courses (`/courses`) — global notes

These spec endpoints are for **offered courses** (an instance of a course template tied to a term + teacher). The previous FE wiring used `/admin/courses`; the spec uses `/courses`. All six course endpoints below moved to `/courses`. The admin-only `/admin/courses/:id/toggle-status` stays where it is until the backend specs it (or until the FE migrates to PATCHing `is_active`).

The separate **course templates** API (`/admin/course-templates`, `useAdminCourseTemplatesListQuery`, etc.) is not in the spec yet and was not touched.

### ⚠️ Field name mismatches (apply to every courses endpoint below)

| FE / mock key (camelCase) | Spec key (snake_case) | Notes |
| --- | --- | --- |
| `image` | `cover_url` | FE reads `course.image` |
| `teacher` (nested object) | _(only `teacher_id` in list)_ | FE list cards read `course.teacher` for name/avatar; need `teacher` nested on list too |
| `term` (nested object) | _(only `term_id` in list)_ | FE list cards read `course.term?.title`; need `term` nested on list too |
| `template` (nested object) | _(missing entirely)_ | FE-only concept (course-template link) |
| `templateId` | _(missing)_ | Course-template id |
| `prerequisitesCount` | _(missing)_ | Number badge on list cards |
| `startDate` / `endDate` | _(missing — read from `term.starts_at`/`ends_at`)_ | List cards currently store dates flat on the course; the FE could derive from the nested `term` instead |

The mock currently writes **both** the spec key and the legacy UI key (`coverUrl` + `image`) and includes nested `term`/`teacher` objects on both list and detail responses so the existing list cards keep rendering. **Required action:** backend should either include the nested objects + UI-only counters on every course response, or the FE needs migration. Decision pending.

### Nested `teacher` shape

The spec's detail response nests a full teacher user object using the top-level user spec (`name`, `email`, `phone`, `roles`, `avatar_url`, …). The UI cards already read `course.teacher.firstName` / `course.teacher.lastName` — so this is yet another instance of the [cross-cutting `name` vs `first_name`/`last_name`](#%EF%B8%8F-cross-cutting-name-vs-first_name--last_name) problem. The mock writes both forms on the nested teacher.

---

## GET `/courses` — list courses (paginated)

Mock fixture: `src/services/mock/fixtures/admin-courses.js` (`adminOfferedCourses`). Mock route: `src/services/mock/routes/admin-courses.js`. Frontend query: `useAdminCoursesListQuery`. Consumed by: `src/features/admin/courses/pages/CoursesListPage.vue`, `src/features/admin/courses/components/CourseItem.vue`.

### Spec envelope

```json
{
  "success": true,
  "message": "OK",
  "data": {
    "items": [
      /* course objects */
    ],
    "meta": { "current_page": 1, "per_page": 20, "total": 1, "last_page": 1 }
  }
}
```

### Per-item spec

| Backend key   | UI key        | Type           |
| ------------- | ------------- | -------------- |
| `id`          | `id`          | number         |
| `term_id`     | `termId`      | number         |
| `teacher_id`  | `teacherId`   | number         |
| `title`       | `title`       | string         |
| `description` | `description` | string         |
| `capacity`    | `capacity`    | number         |
| `is_active`   | `isActive`    | boolean        |
| `cover_url`   | `coverUrl`    | string \| null |

### Per-item missing — UI also needs

See "Field name mismatches" above. The list cards specifically need `term` (nested with at least `id`, `title`), `teacher` (nested with `id` + the user-shape so `firstName`/`lastName`/`name` are available), `prerequisitesCount`, and `image` (or the FE migrates to `cover_url`). `template` / `templateId` are FE-only — backend may ignore.

### Query params

Spec documents `term_id` and `per_page`. FE additionally sends `page`, `title` (free-text), `status` (0/1), `from_date`, `to_date`. Backend should accept or the FE filter set needs trimming.

---

## GET `/courses/:id` — show course

Mock route: same file. Frontend query: `useAdminCourseQuery`. Consumed by: `src/features/admin/courses/components/modals/CourseDetailsModal.vue` (opened from the list — _not_ a route page), `src/features/admin/courses/pages/CourseTemplateFormPage.vue` (when editing offered courses via the unified form).

### Spec envelope

```json
{
  "success": true,
  "message": "OK",
  "data": {
    "id": 1,
    "term_id": 1,
    "teacher_id": 2,
    "title": "…",
    "description": "…",
    "capacity": 50,
    "is_active": true,
    "cover_url": "…",
    "term": {
      "id": 1,
      "title": "…",
      "description": "…",
      "is_active": true,
      "starts_at": "…",
      "ends_at": "…",
      "cover_url": "…",
      "created_at": "…"
    },
    "teacher": {
      "id": 2,
      "name": "John Teacher",
      "email": "…",
      "phone": "…",
      "roles": ["teacher"],
      "avatar_url": null,
      "avatar_download_url": null,
      "created_at": "…"
    }
  }
}
```

### Missing — UI also needs

Same superset as list-item missing (template/templateId, prerequisitesCount, image, nested teacher carrying firstName/lastName/fullName).

### 404 — Not found

```json
{ "success": false, "message": "Resource not found." }
```

---

## POST `/courses` — create course

Mock route: same file. API client: `apiAddAdminCourse`. Query: `useAddAdminCourseMutation`. Consumed by: course-creation form (offered-course branch of the existing course form page / modal).

### Spec request body

```json
{
  "term_id": 1,
  "teacher_id": 2,
  "title": "Intro to Theology",
  "description": "Foundational course.",
  "capacity": 50,
  "is_active": true
}
```

### What the FE form sends today

| FE key (camelCase) | Wire key      | Maps to spec?                             |
| ------------------ | ------------- | ----------------------------------------- |
| `termId`           | `term_id`     | → `term_id`                               |
| `teacherId`        | `teacher_id`  | → `teacher_id`                            |
| `title`            | `title`       | → `title`                                 |
| `description`      | `description` | → `description`                           |
| `capacity`         | `capacity`    | → `capacity`                              |
| `isActive`         | `is_active`   | → `is_active`                             |
| `templateId`       | `template_id` | **does not exist in spec** — FE-only      |
| `imageId`          | `image_id`    | not in spec body — see "Image flow" below |

**Image flow** (canonical): the FE uploads the file to `POST /media/upload`, gets a media `id` back, and submits it as `image_id` with the course create/update body. Backend resolves `image_id` against the media table and populates `cover_url`. No separate per-resource upload endpoint exists — the previously documented `POST /courses/:id/image` was a mistake and has been removed.

### Spec response — `201 Created`

```json
{
  "success": true,
  "message": "Course created.",
  "data": {
    /* course */
  }
}
```

The response should carry every list-item "UI also needs" field (nested `term`, nested `teacher`, `image`, `template`, `templateId`, `prerequisitesCount`) so the FE cache refresh has all it needs.

### Spec errors

- `403` `{ "success": false, "message": "This action is unauthorized." }`
- `422` `{ "message": "The given data was invalid.", "errors": { … } }`

---

## PATCH `/courses/:id` — update course

Mock route: same file. API client: `apiUpdateAdminCourse` — **changed from `PUT` to `PATCH`**. Query: `useUpdateAdminCourseMutation`.

### Spec request body — partial

```json
{ "title": "Intro to Theology (v2)", "capacity": 60 }
```

### What the FE form sends today

Same payload shape as create (the same form handles add + edit). The mock now applies a true partial update — only keys present in the payload are written.

### Spec response — `200 OK`

```json
{
  "success": true,
  "message": "Course updated.",
  "data": {
    /* course */
  }
}
```

---

## DELETE `/courses/:id` — delete course

Mock route: same file. API client: `apiDeleteAdminCourse`. Query: `useDeleteAdminCourseMutation`. Consumed by: `src/features/admin/courses/pages/CoursesListPage.vue` (via `ConfirmModal`).

### Spec response — `200 OK`

```json
{ "success": true, "message": "Course deleted.", "data": null }
```

Previous mock returned a Persian message under `data.message` — replaced by the envelope. The FE only invalidates the list cache on success.

---

## Sessions (`/sessions`) — global notes

The previous FE wiring used `/admin/sessions`; the spec uses `/sessions`. All six session endpoints below moved to `/sessions`. The admin-only `/admin/sessions/:id/toggle-status` and `/admin/sessions/:sessionId/attendances` stay where they are until the backend specs them.

### ⚠️ Structural mismatch: course vs course-template

The spec says a session belongs to an **offered course** (`course_id`). The FE models sessions as belonging to a **course template** (`course_template_id`), where a single session can be reused across multiple terms/offered courses (`session.used_in_terms`).

- The mock now stores **both** `courseId` (spec) and `courseTemplate` (UI nested object). When the FE creates a session via `course_template_id`, the mock auto-derives `course_id` from the first offered course with that template (best-effort).
- Backend should decide: either expose `course_template_id` on the session endpoints, or the FE needs to migrate to picking an offered course directly when creating a session. Decision pending.

### ⚠️ Enum mismatch: `type` (spec) vs `session_type` (FE)

Spec values for `type`: **`online` | `offline` | `content`** (3 values). FE values for `session_type` (enum `SESSION_TYPE`): **`in_person` | `online` | `video` | `audio` | `text` | `slide` | `pdf`** (7 values).

The mock derives `type` from `session_type` like this (see `SESSION_TYPE_TO_SPEC` in the fixture):

| FE `session_type`                            | Spec `type` |
| -------------------------------------------- | ----------- |
| `in_person`                                  | `offline`   |
| `online`                                     | `online`    |
| `video` / `audio` / `text` / `slide` / `pdf` | `content`   |

The FE form picks from the 7-value enum and renders different config blocks per value (`SessionFormPage.vue`). Backend should either:

1. Adopt the 7-value enum and map to its internal 3-value domain server-side, **or**
2. Add a sub-type column (`session_subtype` etc.) so the FE can still pick `video`/`audio`/`pdf` while `type` stays in the 3-value spec set.

Decision pending.

### ⚠️ Flat fields vs `session_config`

The spec **flattens** what the FE keeps nested in `session_config`:

| Spec key (top-level) | FE key (nested)                                    |
| -------------------- | -------------------------------------------------- |
| `starts_at`          | `session_config.start_time`                        |
| `location`           | `session_config.location` (in_person)              |
| `link`               | `session_config.meeting_link` (online)             |
| _(missing)_          | `session_config.platform` (online)                 |
| _(missing)_          | `session_config.min_watched_percent` (video/audio) |
| _(missing)_          | `session_config.min_read_percent` (text/slide/pdf) |
| _(missing)_          | `session_config.must_complete_before_next`         |

The mock now writes the spec keys flat AND keeps the legacy `session_config` nested block so `SessionDetailsModal.vue` (which reads `session.session_config.*`) keeps rendering. **Required action:** backend should either accept the nested `session_config` and return it back, or the FE detail modal needs to be migrated to read the flat keys + the missing config knobs need a new home (probably a JSON column).

### Other UI-only fields the spec doesn't cover

`image`, `duration_minutes`, `order`, `materials`, `used_in_terms`, `course_template` (nested). The FE list cards and detail modal all read these — backend should include them or the FE needs migration. The mock keeps them all.

---

## GET `/sessions` — list sessions (paginated)

Mock fixture: `src/services/mock/fixtures/admin-sessions.js` (`adminSessions`). Mock route: `src/services/mock/routes/admin-sessions.js`. Frontend query: `useAdminSessionsListQuery`. Consumed by: `src/features/admin/sessions/pages/SessionsListPage.vue`, `src/features/admin/sessions/components/SessionItem.vue`.

### Spec envelope

```json
{
  "success": true,
  "message": "OK",
  "data": {
    "items": [
      /* session objects */
    ],
    "meta": { "current_page": 1, "per_page": 20, "total": 1, "last_page": 1 }
  }
}
```

### Per-item spec

| Backend key   | UI key        | Type                                     |
| ------------- | ------------- | ---------------------------------------- |
| `id`          | `id`          | number                                   |
| `course_id`   | `courseId`    | number                                   |
| `title`       | `title`       | string                                   |
| `description` | `description` | string                                   |
| `type`        | `type`        | enum: `online` \| `offline` \| `content` |
| `starts_at`   | `startsAt`    | string (ISO with tz)                     |
| `location`    | `location`    | string \| null                           |
| `link`        | `link`        | string \| null                           |

### Per-item missing — UI also needs

See "Structural", "Enum", and "Flat vs session_config" callouts above. Concretely the list card reads: `image`, `courseTemplate.title`, `durationMinutes`, `sessionType` / `sessionTypeFa`, `usedInTerms[]`. None of those are in the spec yet.

### Query params

Spec documents `course_id` + `per_page`. FE additionally sends `title`, `course_template_id`, `session_type`, `from_date`, `to_date`, `page`. Backend should accept or the FE filter set needs trimming.

---

## GET `/sessions/:id` — show session

Mock route: same file. Frontend query: `useAdminSessionQuery`. Consumed by: `src/features/admin/sessions/components/modals/SessionDetailsModal.vue`, `src/features/admin/sessions/pages/SessionFormPage.vue` (edit mode).

### Spec envelope

```json
{
  "success": true,
  "message": "OK",
  "data": {
    "id": 1,
    "course_id": 1,
    "title": "…",
    "description": "…",
    "type": "online",
    "starts_at": "…",
    "location": null,
    "link": "…",
    "media": [
      {
        "id": 15,
        "collection_name": "attachments",
        "file_name": "…",
        "mime_type": "…",
        "file_size": 204800,
        "url": "…",
        "download_url": "…"
      }
    ],
    "course": {
      /* full offered-course object, same shape as GET /courses/:id flat fields */
    }
  }
}
```

### Missing — UI also needs

Detail modal needs: `session_config.*` (with the config knobs not in spec — `platform`, `min_watched_percent`, etc.), `course_template` (nested), `image`, `duration_minutes`, `order`, `materials[]`, `used_in_terms[]`. The mock writes all of them.

The spec's `media[]` and the FE's `materials[]` are **different arrays** — `media` is the new attachments collection (PDFs, slides, recordings); `materials` is a legacy FE concept. Backend can either collapse them into one field or keep both — flagged for decision.

### 404 — Not found

```json
{ "success": false, "message": "Resource not found." }
```

---

## POST `/sessions` — create session

Mock route: same file. API client: `apiAddAdminSession`. Query: `useAddAdminSessionMutation`. Consumed by: `src/features/admin/sessions/pages/SessionFormPage.vue` (add).

### Spec request body

```json
{
  "course_id": 1,
  "title": "Week 1 — Overview",
  "description": "Opening session.",
  "type": "online",
  "starts_at": "2026-03-25T18:00:00+03:30",
  "location": null,
  "link": "https://meet.example.com/abc"
}
```

### What the FE form sends today

| FE key (camelCase) | Wire key | Maps to spec? |
| --- | --- | --- |
| `title` | `title` | → `title` |
| `description` | `description` | → `description` |
| `courseTemplateId` | `course_template_id` | **not in spec** — see structural callout |
| `sessionType` | `session_type` | maps to spec `type` via the table above |
| `durationMinutes` | `duration_minutes` | missing in spec |
| `order` | `order` | missing in spec |
| `sessionConfig.startTime` | `session_config.start_time` | maps to spec `starts_at` |
| `sessionConfig.location` | `session_config.location` | maps to spec `location` |
| `sessionConfig.meetingLink` | `session_config.meeting_link` | maps to spec `link` |
| `sessionConfig.platform` | `session_config.platform` | missing in spec |
| `sessionConfig.minWatchedPercent` | `session_config.min_watched_percent` | missing in spec |
| `sessionConfig.minReadPercent` | `session_config.min_read_percent` | missing in spec |
| `sessionConfig.mustCompleteBeforeNext` | `session_config.must_complete_before_next` | missing in spec |
| `imageId` | `image_id` | media id from `POST /media/upload`; backend should set `cover_url` |
| `materials` | `materials[]` | array of media ids (each from `POST /media/upload`); backend attaches them as session media; overlaps with spec `media[]` — see callout |

The mock accepts both naming conventions on input. **The FE form needs a flat `course_id` selector** to fully match the spec (or backend must accept `course_template_id`).

### Spec response — `201 Created`

```json
{
  "success": true,
  "message": "Session created.",
  "data": {
    /* session */
  }
}
```

### Spec errors

- `422` `{ "message": "The given data was invalid.", "errors": { "course_id": [...], "title": [...], "type": [...] } }`

---

## PATCH `/sessions/:id` — update session

Mock route: same file. API client: `apiUpdateAdminSession` — **changed from `PUT` to `PATCH`**. Query: `useUpdateAdminSessionMutation`.

### Spec request body — partial

```json
{ "title": "…", "starts_at": "…" }
```

### What the FE sends today

Same payload shape as create. The mock now applies a true partial update — only keys present in the payload are written.

### Spec response — `200 OK`

```json
{
  "success": true,
  "message": "Session updated.",
  "data": {
    /* session */
  }
}
```

---

## DELETE `/sessions/:id` — delete session

Mock route: same file. API client: `apiDeleteAdminSession`. Query: `useDeleteAdminSessionMutation`.

### Spec response — `200 OK`

```json
{ "success": true, "message": "Session deleted.", "data": null }
```

Previous mock returned a Persian message under `data.message` — replaced by the envelope. The FE only invalidates the list cache on success.

---

<!-- Append further routes below as they are wired up. -->
