Files
clinicpro/docs/api/rating.md
T
hamed b05aeaf58b Refactor doctor name handling across the application
- Removed the "دکتر" prefix from doctor names in various components and API responses to ensure consistency and clarity.
- Updated the AppointmentDetailPage, CommentsPage, DashboardPage, RatingsPage, SecretariesPage, and other relevant files to reflect the changes in doctor name formatting.
- Adjusted API documentation to align with the new naming conventions.
- Implemented validation to prevent the creation of clinics without a name and restricted users to a single clinic.
- Added tests to verify that doctor names are stored without titles and that clinic creation adheres to the new validation rules.
2026-07-19 16:09:55 +03:30

366 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `0100`:
```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 | ✅ | 0100 |
| `accuracy_of_diagnosis` | integer | ✅ | 0100 |
| `doctor_behavior` | integer | ✅ | 0100 |
| `clinic_cleanliness` | integer | ✅ | 0100 |
| `doctor_expertise` | integer | ✅ | 0100 |
### 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 0100 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 05 scale (`satisfaction / 20`).
- `satisfaction`: mean of all dimensions, percent `0100`.
- `averages[].progress`: per-dimension mean, percent `0100`.
- 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": "<root-uuid>",
"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 |