Files
clinicpro/docs/api/rating.md
T
hamedandClaude Opus 4.8 61ac775175 perf(settlement,rating): paginate 3 unbounded list endpoints (M10-M12)
- M10 GET /settlement: was unbounded; add page/limit + countByUser + data.meta.
- M11 GET /admin/comments/pending: paginate findPending + countPending.
- M12 GET /comments/{doctor}: paginate the fetch-joined roots query via
  Paginator(fetchJoinCollection) + countApprovedRootsByDoctor.

All keep the existing { data: { data: [...] } } envelope and add data.meta
(backward compatible). Default limit 50 / max 100.

Regressions: SettlementListPaginationTest, CommentPaginationTest (both fail
without the limits). Also de-flaked SendCodeMobileRateLimitTest (randomised the
IP block so the persistent per-IP limiter buckets don't accumulate across runs).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 20:36:41 +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 |