# Rating & Comments API > **Prefix:** `/api/v1/rate`, `/api/v1/comment`, `/api/v1/like` --- ## POST `/api/v1/rate` Submit a multi-dimensional rating for a doctor. Upsert — re-submitting overwrites the user's previous rating. **Permission:** `AUTH` > **Eligibility rule:** The user must have had a **confirmed** appointment (`status = confirmed`) with this doctor whose `slot_start` falls within the **last 30 days**. Otherwise the request is rejected with `403 ERR_RATING_NOT_ELIGIBLE`. Use [`GET /api/v1/rate/{doctorUuid}/eligibility`](#get-apiv1ratedoctoruuideligibility) to check before showing the rating UI. ### Request Body (`application/json`) Five dimensions, each an integer percentage `0–100`: ```json { "doctor_uuid": "550e8400-...", "waiting_time_at_clinic": 80, "accuracy_of_diagnosis": 100, "doctor_behavior": 100, "clinic_cleanliness": 60, "doctor_expertise": 100 } ``` | Field | Type | Required | Validation | |-------|------|----------|------------| | `doctor_uuid` | string (UUID) | ✅ | Must exist | | `waiting_time_at_clinic` | integer | ✅ | 0–100 | | `accuracy_of_diagnosis` | integer | ✅ | 0–100 | | `doctor_behavior` | integer | ✅ | 0–100 | | `clinic_cleanliness` | integer | ✅ | 0–100 | | `doctor_expertise` | integer | ✅ | 0–100 | ### Response `201` / `200` Returns the **updated aggregate** for the doctor (same shape as `GET /api/v1/rate/{doctorUuid}`): ```json { "success": true, "data": { "data": { "point": 4.4, "satisfaction": 88, "averages": [ { "name": "waiting_time_at_clinic", "label": "زمان انتظار در مطب", "progress": 80 }, { "name": "accuracy_of_diagnosis", "label": "تشخیص درست", "progress": 100 }, { "name": "doctor_behavior", "label": "برخورد مناسب پزشک", "progress": 100 }, { "name": "clinic_cleanliness", "label": "نظافت مطب", "progress": 60 }, { "name": "doctor_expertise", "label": "مهارت پزشک", "progress": 100 } ] } } } ``` > Note: response is double-nested (`data.data`) — `success(['data' => $aggregate])`. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_RATING_NOT_ELIGIBLE` | 403 | No confirmed appointment with this doctor in the last 30 days | | `ERR_NOT_FOUND_001` | 404 | Doctor not found | | `ERR_VALIDATION_001` | 422 | A dimension is out of the 0–100 range | --- ## GET `/api/v1/rate/{doctorUuid}` Get the aggregate (multi-dimensional) rating for a doctor: overall star point, satisfaction percent, and per-dimension averages. **Permission:** `PUBLIC` ### Path Parameters | Param | Type | Description | |-------|------|-------------| | `doctorUuid` | string (UUID) | Doctor UUID | ### Response `200` ```json { "success": true, "data": { "data": { "point": 4.4, "satisfaction": 88, "averages": [ { "name": "waiting_time_at_clinic", "label": "زمان انتظار در مطب", "progress": 80 }, { "name": "accuracy_of_diagnosis", "label": "تشخیص درست", "progress": 100 }, { "name": "doctor_behavior", "label": "برخورد مناسب پزشک", "progress": 100 }, { "name": "clinic_cleanliness", "label": "نظافت مطب", "progress": 60 }, { "name": "doctor_expertise", "label": "مهارت پزشک", "progress": 100 } ] } } } ``` - `point`: overall rating on a 0–5 scale (`satisfaction / 20`). - `satisfaction`: mean of all dimensions, percent `0–100`. - `averages[].progress`: per-dimension mean, percent `0–100`. - If the doctor has no ratings: `point=0`, `satisfaction=0`, every `progress=0`. - Response is double-nested (`data.data`). ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_NOT_FOUND_001` | 404 | Doctor not found | --- ## GET `/api/v1/rate/{doctorUuid}/eligibility` Whether the **current authenticated user** is allowed to rate/comment on this doctor — i.e. had a confirmed appointment with them in the last 30 days. Intended for the public site to conditionally show the "submit review" UI. **Permission:** `AUTH` (`IS_AUTHENTICATED_FULLY`) ### Path Parameters | Param | Type | Description | |-------|------|-------------| | `doctorUuid` | string (UUID) | Doctor UUID | ### Response `200` ```json { "success": true, "data": { "eligible": true } } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_NOT_FOUND_001` | 404 | Doctor not found | --- ## POST `/api/v1/comment` Submit a comment/review for a doctor. **Permission:** `AUTH` > Comments require admin approval before appearing publicly. > > **Eligibility rule:** Same as `POST /api/v1/rate` — the user must have had a **confirmed** appointment with this doctor within the **last 30 days**, otherwise `403 ERR_RATING_NOT_ELIGIBLE`. ### Request Body (`application/json`) ```json { "doctor_uuid": "550e8400-...", "comment": "پزشک بسیار مؤدب و متخصص بودند", "parent": null } ``` | Field | Type | Required | Validation | |-------|------|----------|------------| | `doctor_uuid` | string (UUID) | ✅ | Must exist | | `comment` | string | ✅ | Non-empty | | `parent` | string (UUID) \| null | ❌ | If set, this comment is a reply to the parent comment | ### Response `201` Returns the created comment in the **rich shape** (see `GET /comments` below). New comments are `pending` until an admin approves them, so they will not appear in the public list yet. **Comment Status Values:** `pending` (awaiting review) · `approved` (public) · `rejected`. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_RATING_NOT_ELIGIBLE` | 403 | No confirmed appointment with this doctor in the last 30 days | | `ERR_NOT_FOUND_001` | 404 | Doctor (or parent comment) not found | | `ERR_VALIDATION_002` | 422 | Comment text empty | --- ## GET `/api/v1/comments/{doctorUuid}` Get approved **root** comments for a doctor (replies are nested under each root via `replies`). **Permission:** `PUBLIC` > **صفحه‌بندی:** `?page` و `?limit` (پیش‌فرض ۵۰، حداکثر ۱۰۰)؛ پاسخ شامل `data.meta` (`totalRecords`/`totalPages`/`currentPage`) است. آرایه‌ی نظرات همچنان در `data.data` است. همین صفحه‌بندی روی `GET /api/v1/admin/comments/pending` هم اعمال می‌شود. ### Path Parameters | Param | Type | Description | |-------|------|-------------| | `doctorUuid` | string (UUID) | Doctor UUID | ### Response `200` Response is double-nested (`data.data`). Each item: ```json { "success": true, "data": { "data": [ { "uuid": "...", "comment": "پزشک بسیار مؤدب...", "created": 1717000000, "parent": null, "author": { "real_name": "میثم امیری", "picture": [] }, "like_status": { "like_count": 6, "dislike_count": 1, "current_user_like": { "like": false, "dislike": false } }, "replies": [ { "uuid": "...", "comment": "پاسخ ...", "created": 1717000500, "parent": "", "author": { "real_name": "امیر حبیبی", "picture": [] }, "like_status": { "like_count": 0, "dislike_count": 0, "current_user_like": { "like": false, "dislike": false } }, "replies": [] } ] } ] } } ``` - `comment` (not `body`); `created` (not `created_at`); both Unix seconds. - `author.real_name` from the user (falls back to «کاربر نوبت‌۷۲۴» if unset). `author.picture` is always `[]` (no user avatar field) — frontend uses a default image. - `current_user_like` is always `{false,false}` on this public endpoint (no token is processed); the real per-user state comes from the `POST /like` response — keep the UI optimistic. - Only `approved` comments/replies are returned. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_NOT_FOUND_001` | 404 | Doctor not found | --- ## DELETE `/api/v1/comment/{uuid}` Delete a comment. **Permission:** `AUTH` — must be the comment author or `ROLE_ADMIN` ### Response `200` ```json { "success": true, "data": { "message": "نظر حذف شد" } } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_FORBIDDEN_001` | 403 | Not the author | | `ERR_NOT_FOUND_001` | 404 | Comment not found | --- ## GET `/api/v1/admin/comments/pending` Get all pending comments waiting for review. **Permission:** `ROLE_ADMIN` ### Response `200` ```json { "success": true, "data": [ { "uuid": "...", "body": "...", "doctor": { "uuid": "...", "title": "دکتر علی احمدی" }, "user": { "uuid": "...", "real_name": "..." }, "status": "pending", "created_at": 1717000000 } ] } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | --- ## POST `/api/v1/admin/comment/{uuid}/approve` Approve a pending comment (makes it public). **Permission:** `ROLE_ADMIN` ### Response `200` Updated comment object with `status: "approved"`. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_NOT_FOUND_001` | 404 | Comment not found | --- ## POST `/api/v1/admin/comment/{uuid}/reject` Reject a pending comment. **Permission:** `ROLE_ADMIN` ### Response `200` Updated comment object with `status: "rejected"`. ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_NOT_FOUND_001` | 404 | Comment not found | --- ## POST `/api/v1/like/{commentUuid}` Cast a like or dislike on a comment. Toggling logic: - Same vote sent again → vote is **removed**. - Opposite vote sent → vote is **replaced** (e.g. like → dislike). - No existing vote → vote is **added**. **Permission:** `AUTH` ### Path Parameters | Param | Type | Description | |-------|------|-------------| | `commentUuid` | string (UUID) | Comment UUID | ### Request Body (`application/json`) ```json { "value": 1 } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `value` | integer | ❌ (default 1) | `1` = like, `-1` = dislike | ### Response `200` ```json { "success": true, "data": { "like_count": 6, "dislike_count": 1, "current_user_like": { "like": true, "dislike": false } } } ``` ### Errors | Code | HTTP | Description | |------|------|-------------| | `ERR_AUTH_001` | 401 | Missing token | | `ERR_NOT_FOUND_001` | 404 | Comment not found |