feat(rating): multi-dimensional ratings, rich comments, eligibility guard
Rebuild the doctor rating/review system to power the public site's rich
review UI, and restrict who may submit.
Ratings:
- Rate entity holds five 0–100 dimensions (waiting time, diagnosis
accuracy, behaviour, cleanliness, expertise) instead of a single score.
- GET /rate/{uuid} returns aggregate {point, satisfaction, averages[]}.
- POST /rate upserts all five dimensions and returns the new aggregate.
Comments:
- Comment gains parent/replies (threaded) and a rich toArray with author,
like_status (like/dislike counts + current user's vote) and nested
approved replies. POST /comment accepts {comment, parent}.
- Likes are directional (value 1=like, -1=dislike) with toggle/replace;
POST /like/{uuid} returns like_count/dislike_count/current_user_like.
Eligibility:
- Only a user with a confirmed appointment in the last 30 days may rate or
comment (AppointmentRepository::hasRecentConfirmed); otherwise
403 ERR_RATING_NOT_ELIGIBLE. New GET /rate/{uuid}/eligibility for the UI.
- security.yaml: narrow the public rate pattern so /eligibility stays auth'd.
Also updates admin rates listing to the new dimensions and the rating/admin
API docs. Includes migration for the new columns.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
# امتیاز چندبُعدی + نظرات غنی (نویسنده، لایک/دیسلایک، پاسخ) برای صفحه پزشک
|
||||
|
||||
## پروژه
|
||||
|
||||
`clinicpro` (Backend — منبع حقیقت).
|
||||
|
||||
> **Cross-repo:** سایت عمومی `nobat724_front` این قرارداد را مصرف میکند تا UI غنیِ نظرات/امتیاز صفحه پزشک را پر کند (پرامپت همتا: `nobat724_front/.claude/prompt/rating-rich-ui-wiring.md`). این پرامپت **اول** اجرا شود.
|
||||
>
|
||||
> این کار قانونِ «نوبت تاییدشده در ۳۰ روز گذشته» را که قبلاً اضافه شد **نگه میدارد**؛ فقط مدل دادهی امتیاز/نظر را غنیتر میکند.
|
||||
|
||||
## زمینه و چرایی
|
||||
|
||||
UI صفحه پزشک در `nobat724_front` (طراحی موجود و تأییدشده) یک نمای غنی دارد که باید حفظ شود:
|
||||
- چارت امتیاز با **۵ بُعد** درصدی: زمان انتظار در مطب، تشخیص درست، برخورد مناسب پزشک، نظافت مطب، مهارت پزشک.
|
||||
- یک **دایرهی «مجموع رضایت کاربران»** (درصد).
|
||||
- امتیاز ستارهای کلی (`point` از ۵).
|
||||
- هر نظر با **نام و عکس نویسنده**، **لایک و دیسلایک** (با تعداد و وضعیت کاربر فعلی)، و **پاسخها (replies)**.
|
||||
|
||||
مدل فعلی بکاند خیلی ساده است (تک`score` ۱–۵، نظرِ بدون نویسنده/دیسلایک/پاسخ)، پس UI نمیتواند پر شود. هدف: بکاند این قرارداد غنی را تأمین کند.
|
||||
|
||||
## قرارداد هدف (دقیقاً آنچه UI انتظار دارد)
|
||||
|
||||
### الف) `GET /api/v1/rate/{doctorUuid}` (عمومی) — تجمیع امتیاز
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"point": 4.4,
|
||||
"satisfaction": 89,
|
||||
"averages": [
|
||||
{ "name": "waiting_time_at_clinic", "label": "زمان انتظار در مطب", "progress": 70 },
|
||||
{ "name": "accuracy_of_diagnosis", "label": "تشخیص درست", "progress": 88 },
|
||||
{ "name": "doctor_behavior", "label": "برخورد مناسب پزشک", "progress": 90 },
|
||||
{ "name": "clinic_cleanliness", "label": "نظافت مطب", "progress": 72 },
|
||||
{ "name": "doctor_expertise", "label": "مهارت پزشک", "progress": 90 }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
- `progress` هر بُعد: میانگین آن بُعد روی همهی امتیازها، بهصورت درصد ۰–۱۰۰ (گرد).
|
||||
- `point`: میانگین کلیِ همهی ابعاد، روی مقیاس ۰–۵ (مثلاً `mean(progress)/20`).
|
||||
- `satisfaction`: همان میانگین کلی بهصورت درصد ۰–۱۰۰.
|
||||
- اگر هیچ امتیازی نباشد: `point=0`, `satisfaction=0`, ابعاد با `progress=0`.
|
||||
|
||||
### ب) `POST /api/v1/rate` (AUTH + گارد واجد بودن موجود) — ثبت امتیاز چندبُعدی
|
||||
Request body:
|
||||
```json
|
||||
{
|
||||
"doctor_uuid": "…",
|
||||
"waiting_time_at_clinic": 80,
|
||||
"accuracy_of_diagnosis": 100,
|
||||
"doctor_behavior": 100,
|
||||
"clinic_cleanliness": 60,
|
||||
"doctor_expertise": 100
|
||||
}
|
||||
```
|
||||
- هر بُعد عددی ۰–۱۰۰ (UI با Rating ۵ ستاره مقدار `progress = stars*20` میفرستد). Validation: هر کدام بین ۰ و ۱۰۰.
|
||||
- Upsert per (user, doctor): اگر امتیاز قبلی بود، بهروزرسانی شود.
|
||||
|
||||
### ج) `GET /api/v1/comments/{doctorUuid}` (عمومی) — هر آیتم نظر
|
||||
```json
|
||||
{
|
||||
"uuid": "…",
|
||||
"comment": "متن نظر",
|
||||
"created": 1700000000,
|
||||
"parent": null,
|
||||
"author": { "real_name": "میثم امیری", "picture": [{ "url": "/path.jpg" }] },
|
||||
"like_status": {
|
||||
"like_count": 6,
|
||||
"dislike_count": 1,
|
||||
"current_user_like": { "like": false, "dislike": false }
|
||||
},
|
||||
"replies": [ /* همان ساختار، بهصورت تو در تو */ ]
|
||||
}
|
||||
```
|
||||
- کلید متن `comment` است (نه `body`). تاریخ `created` (نه `created_at`).
|
||||
- `author.real_name` از `User.realName`؛ `author.picture` آرایهای از `{url}` (اگر کاربر عکس ندارد، آرایهی خالی یا با url پیشفرض — هماهنگ با `imageUrl()` فرانت که fallback دارد).
|
||||
- `current_user_like`: اگر درخواست با توکن باشد، وضعیت رأی کاربر فعلی؛ اگر بدون توکن، هر دو `false`.
|
||||
- `replies`: نظرهایی که `parent` آنها این نظر است؛ فقط نظرهای ریشه (parent=null) در سطح بالا برگردند و پاسخها داخل `replies` تو در تو بیایند.
|
||||
|
||||
### د) `POST /api/v1/comment` (AUTH + گارد واجد بودن موجود) — ثبت نظر/پاسخ
|
||||
Request body:
|
||||
```json
|
||||
{ "doctor_uuid": "…", "comment": "متن", "parent": "<uuid نظر والد یا null>" }
|
||||
```
|
||||
- کلید `comment` (نه `body`). اگر `parent` داده شد، نظرِ پاسخ زیر آن والد ساخته شود.
|
||||
- نکتهی سازگاری: فرانت در برخی نقاط `doctor_id`/`comment` میفرستد؛ ولی قرارداد رسمی ما `doctor_uuid` است — در پرامپت فرانت همتا، فرستادن `doctor_uuid` تضمین میشود. بکاند فقط `doctor_uuid` را بپذیرد.
|
||||
|
||||
### ه) `POST /api/v1/like/{commentUuid}` (AUTH) — رأی لایک/دیسلایک
|
||||
Request body:
|
||||
```json
|
||||
{ "value": 1 } // 1 = like ، -1 = dislike
|
||||
```
|
||||
- toggle: اگر همان رأی دوباره زده شد حذف شود؛ اگر رأی مخالف بود جایگزین شود.
|
||||
- Response: `{ success, data: { like_count, dislike_count, current_user_like: { like, dislike } } }`.
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | کار |
|
||||
|---|---|
|
||||
| `src/Rating/Entity/Rate.php` | جایگزینی تک`score` با ۵ ستون بُعدی + متد میانگینها |
|
||||
| `src/Rating/Entity/Comment.php` | افزودن `parent` (self ManyToOne)، `replies` (OneToMany)، `toArray` غنی با author/like_status/replies |
|
||||
| `src/Rating/Entity/Like.php` | افزودن `value` (۱ یا ۱-) برای تفکیک like/dislike |
|
||||
| `src/Rating/Repository/RateRepository.php` | `getAggregate(Doctor): array` (point/satisfaction/averages) |
|
||||
| `src/Rating/Repository/CommentRepository.php` | `findApprovedRootsByDoctor` (parent IS NULL)، شمارش لایک/دیسلایک |
|
||||
| `src/Rating/Repository/LikeRepository.php` | تطبیق با `value` |
|
||||
| `src/Rating/Controller/RatingController.php` | بهروزرسانی `rate`/`getAverage`/`createComment`/`listComments`/`toggleLike` طبق قرارداد بالا؛ گارد واجد بودن حفظ شود؛ `#[CurrentUser] ?User $user` برای خواندن وضعیت رأی کاربر در endpointهای عمومی |
|
||||
| `src/Auth/Entity/User.php` | بررسی وجود `realName` و فیلد عکس (picture/avatar) — برای ساخت `author` |
|
||||
| migrations | افزودن ستونهای Rate، `parent_id` در comments، `value` در likes |
|
||||
| `docs/api/rating.md` | بازنویسی کامل قراردادها |
|
||||
|
||||
## وضعیت فعلی (کد واقعی)
|
||||
|
||||
`Rate` فقط `score` دارد (۱–۵). `Comment::toArray()` فقط `{uuid, doctor_uuid, user_uuid, body, status, likes(count), created_at}` میدهد — بدون author/like_status/replies/parent. `Like` رأی دودویی بدون جهت دارد (toggleLike فقط وجود/عدم وجود). `getAverage` فقط `{average}` میدهد.
|
||||
|
||||
> ابعاد و labelها (منبع، از فرانت قدیمی `data/progress_detail.json`):
|
||||
> ```
|
||||
> waiting_time_at_clinic → زمان انتظار در مطب
|
||||
> accuracy_of_diagnosis → تشخیص درست
|
||||
> doctor_behavior → برخورد مناسب پزشک
|
||||
> clinic_cleanliness → نظافت مطب
|
||||
> doctor_expertise → مهارت پزشک
|
||||
> ```
|
||||
> labelها را بهصورت ثابت (مثلاً یک const map در `Rate` یا یک enum/array در Controller) نگهدار و در `averages` همراه `name` برگردان.
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. Entity `Rate` چندبُعدی
|
||||
- پنج ستون `smallint` (۰–۱۰۰): `waitingTimeAtClinic`, `accuracyOfDiagnosis`, `doctorBehavior`, `clinicCleanliness`, `doctorExpertise`.
|
||||
- constructor و setterها مقادیر را به ۰–۱۰۰ clamp کنند.
|
||||
- متد کمکی `overallPercent(): float` = میانگین ۵ بُعد.
|
||||
- `score` قدیمی را حذف کن (یا تبدیل کن). migration بساز.
|
||||
|
||||
### ۲. Entity `Comment` — parent/replies + toArray غنی
|
||||
- `#[ORM\ManyToOne(targetEntity: Comment::class)] private ?Comment $parent` با ستون `parent_id` nullable + `onDelete: CASCADE`.
|
||||
- `#[ORM\OneToMany(mappedBy: 'parent', targetEntity: Comment::class)] private Collection $replies`.
|
||||
- constructor یک پارامتر اختیاری `?Comment $parent = null` بگیرد.
|
||||
- `toArray(?User $currentUser = null): array` غنی طبق قرارداد (ج) — شامل `author`, `like_status`, `replies` (بازگشتی، فقط replies تأییدشده).
|
||||
|
||||
### ۳. Entity `Like` جهتدار
|
||||
- ستون `value` (`smallint`: ۱ یا ۱-).
|
||||
- constructor `value` بگیرد. Repository متدی برای شمارش `like_count`/`dislike_count` یک نظر و یافتن رأی کاربر فعلی.
|
||||
|
||||
### ۴. Repository ها
|
||||
- `RateRepository::getAggregate(Doctor $d): array` → `['point'=>…, 'satisfaction'=>…, 'averages'=>[…]]` با DQL AVG روی هر ستون (اگر صفر رکورد، همه ۰).
|
||||
- `CommentRepository::findApprovedRootsByDoctor(Doctor $d): Comment[]` (status=approved AND parent IS NULL).
|
||||
- `LikeRepository`: `countByComment`, `findByUserAndComment` سازگار با `value`.
|
||||
|
||||
### ۵. Controller
|
||||
- `rate()`: پنج بُعد را بخوان/validate (۰–۱۰۰)، گارد `hasRecentConfirmed` را حفظ کن، upsert.
|
||||
- `getAverage()`: خروجی `getAggregate` را برگردان (point/satisfaction/averages).
|
||||
- `createComment()`: `comment` + `parent` اختیاری؛ گارد حفظ شود.
|
||||
- `listComments()`: ریشهها را با `toArray($currentUser)` برگردان؛ کاربر فعلی را با `#[CurrentUser] ?User $user` بخوان (روی endpoint عمومی، nullable).
|
||||
- `toggleLike()`: `value` بخوان (۱/۱-)؛ toggle/replace؛ شمارشها را برگردان.
|
||||
|
||||
> چون `listComments` و `getAverage` عمومیاند ولی برای `current_user_like` به کاربر اختیاری نیاز دارند: `#[CurrentUser] ?User $user = null` کار میکند چون firewall عمومی توکن را پردازش نمیکند — در این صورت همیشه null خواهد بود. **اگر میخواهی وضعیت رأی کاربر در لیست عمومی نمایش داده شود، این endpointها باید توکن اختیاری را بپذیرند.** سادهترین راه بدون پیچیدگی firewall: یک پارامتر کوئری یا هدر بررسی نشود؛ بهجایش فرانت پس از لاگین، رأیها را سمت کلاینت مدیریت کند. **تصمیم پیشفرض:** `current_user_like` را در لیست عمومی همیشه `{like:false,dislike:false}` برگردان و بهروزرسانی واقعی را به پاسخِ `toggleLike` بسپار (UI optimistic). این از تغییر firewall جلوگیری میکند.
|
||||
|
||||
### ۶. Migration ها
|
||||
بعد از تغییر Entityها: `doctrine:migrations:diff` و `migrate`. دادهی موجود `rates.score` در صورت وجود → میتوان در migration به ابعاد map کرد (یا چون دادهی واقعی نیست، drop/recreate سادهتر است).
|
||||
|
||||
### ۷. مستندسازی `docs/api/rating.md`
|
||||
همهی قراردادهای الف–ه را با مثال JSON واقعی بازنویسی کن.
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **گارد واجد بودن (`hasRecentConfirmed`) روی `rate` و `comment` حفظ شود** — قبلاً اضافه و تست شده.
|
||||
- `User.realName` و فیلد عکس را قبل از استفاده در `author` تأیید کن (فایل `src/Auth/Entity/User.php`). اگر عکس روی User نیست، `picture: []` برگردان (فرانت fallback دارد).
|
||||
- تاریخها Unix ثانیه؛ کلید `created` (نه `created_at`) در نظرها — مطابق UI.
|
||||
- پاسخها (`replies`) فقط تأییدشدهها؛ عمق بازگشت یک سطح کافی است (UI تو در توی عمیق ندارد).
|
||||
- همه پاسخها از `BaseController`.
|
||||
- بعد از تغییر route/Entity: `cache:clear` + `migrate` + تست با `debug:router`.
|
||||
- `docs/api/rating.md` در همین session بهروز شود (Standing Rule).
|
||||
@@ -0,0 +1,168 @@
|
||||
# محدودسازی ثبت نظر/امتیاز به کاربرانِ دارای نوبت تاییدشده در ۳۰ روز گذشته
|
||||
|
||||
## پروژه
|
||||
|
||||
`clinicpro` (Backend — منبع حقیقت).
|
||||
|
||||
> **Cross-repo:** قرارداد این endpointها توسط سایت عمومی `nobat724_front` مصرف میشود (پرامپت همتا: `nobat724_front/.claude/prompt/rating-eligibility-ui.md`). این پرامپت **اول** اجرا شود؛ سپس فرانت.
|
||||
|
||||
## زمینه
|
||||
|
||||
در حال حاضر هر کاربر احرازهویتشدهای میتواند به هر پزشکی نظر و امتیاز بدهد. `RatingController::rate()` و `createComment()` فقط `score`/`body` و وجود پزشک را اعتبارسنجی میکنند و **هیچ بررسیای** روی سابقهی نوبت کاربر نزد آن پزشک ندارند. خواستهی محصول: فقط کاربری که در **۳۰ روز گذشته** نزد آن پزشک نوبتِ **تاییدشده (`confirmed`)** داشته، اجازهی ثبت نظر و امتیاز دارد.
|
||||
|
||||
چون این منبع حقیقت است، قانون باید سمت بکاند اجرا شود (UI بهتنهایی کافی نیست — کاربر میتواند مستقیم به API بزند).
|
||||
|
||||
## هدف
|
||||
|
||||
۱. قانون «نوبت تاییدشده در ۳۰ روز گذشته» روی `POST /api/v1/rate` و `POST /api/v1/comment` اعمال شود؛ در صورت نقض، خطای مجوز با کد و پیام فارسی برگردد.
|
||||
۲. یک endpoint عمومیِ سبک برای فرانت که بگوید کاربر فعلی نسبت به این پزشک واجد شرایط هست یا نه (تا UI دکمهی ثبت نظر را نشان/مخفی کند).
|
||||
|
||||
## تعریف دقیق «واجد بودن»
|
||||
|
||||
کاربر `U` نسبت به پزشک `D` واجد شرایط است اگر حداقل یک `Appointment` وجود داشته باشد که:
|
||||
- `appointment.user = U`
|
||||
- `appointment.doctor = D`
|
||||
- `appointment.status = Appointment::STATUS_CONFIRMED`
|
||||
- `appointment.slotStart` در بازهی `[now - 30*86400, now]` باشد (Unix ثانیه؛ یعنی نوبت در ۳۰ روز گذشته بوده).
|
||||
|
||||
> توجه: `slotStart` تایماستمپ ثانیه است. «۳۰ روز» = `30 * 86400` ثانیه. کف بازه شامل و سقف `now` است.
|
||||
|
||||
## فایلهای مرتبط
|
||||
|
||||
| فایل | نقش |
|
||||
|---|---|
|
||||
| `src/Appointment/Repository/AppointmentRepository.php` | افزودن متد `hasRecentConfirmed(User, Doctor, int $sinceDays = 30): bool` |
|
||||
| `src/Rating/Controller/RatingController.php` | اعمال گارد در `rate()` و `createComment()`؛ افزودن endpoint `eligibility` |
|
||||
| `src/Shared/Constant/ErrorCodes.php` | افزودن کد خطای جدید (در صورت نبود کد مناسب) |
|
||||
| `docs/api/rating.md` | مستندسازی گارد جدید + endpoint جدید |
|
||||
|
||||
## وضعیت فعلی (کد واقعی)
|
||||
|
||||
`RatingController::rate()` — هیچ گاردی ندارد:
|
||||
|
||||
```php
|
||||
$doctor = $this->doctorRepo->findByUuid($doctorUuid);
|
||||
if ($doctor === null) { return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'دکتر یافت نشد', 404); }
|
||||
$existing = $this->rateRepo->findByUserAndDoctor($user, $doctor);
|
||||
// ... upsert
|
||||
```
|
||||
|
||||
`createComment()` مشابه است.
|
||||
|
||||
`AppointmentRepository` الگوی کوئری موجود (برای تقلید):
|
||||
|
||||
```php
|
||||
public function findExpiredPending(int $before): array
|
||||
{
|
||||
return $this->createQueryBuilder('a')
|
||||
->where('a.status = :status')
|
||||
->andWhere('a.slotStart < :before')
|
||||
->setParameter('status', Appointment::STATUS_PENDING)
|
||||
->setParameter('before', $before)
|
||||
->getQuery()->getResult();
|
||||
}
|
||||
```
|
||||
|
||||
ثابتهای وضعیت: `Appointment::STATUS_CONFIRMED = 'confirmed'`. فیلد زمان: `slotStart` (Unix ثانیه). روابط: `a.user`, `a.doctor`.
|
||||
|
||||
## وظایف
|
||||
|
||||
### ۱. متد Repository — `hasRecentConfirmed`
|
||||
|
||||
در `AppointmentRepository`:
|
||||
|
||||
```php
|
||||
/** آیا کاربر در sinceDays روز گذشته نزد این پزشک نوبت تاییدشده داشته؟ */
|
||||
public function hasRecentConfirmed(User $user, Doctor $doctor, int $sinceDays = 30): bool
|
||||
{
|
||||
$now = time();
|
||||
$since = $now - $sinceDays * 86400;
|
||||
|
||||
$count = (int) $this->createQueryBuilder('a')
|
||||
->select('COUNT(a.id)')
|
||||
->where('a.user = :user')
|
||||
->andWhere('a.doctor = :doctor')
|
||||
->andWhere('a.status = :status')
|
||||
->andWhere('a.slotStart >= :since')
|
||||
->andWhere('a.slotStart <= :now')
|
||||
->setParameter('user', $user)
|
||||
->setParameter('doctor', $doctor)
|
||||
->setParameter('status', Appointment::STATUS_CONFIRMED)
|
||||
->setParameter('since', $since)
|
||||
->setParameter('now', $now)
|
||||
->getQuery()->getSingleScalarResult();
|
||||
|
||||
return $count > 0;
|
||||
}
|
||||
```
|
||||
|
||||
`use App\Auth\Entity\User;` و `use App\Doctor\Entity\Doctor;` را اگر نبود اضافه کن.
|
||||
|
||||
### ۲. گارد در `rate()` و `createComment()`
|
||||
|
||||
`AppointmentRepository` را به constructor `RatingController` تزریق کن (`private readonly AppointmentRepository $appointmentRepo`). بلافاصله **بعد از** پیداشدن `$doctor` (و قبل از upsert/ساخت)، در هر دو متد:
|
||||
|
||||
```php
|
||||
if (!$this->appointmentRepo->hasRecentConfirmed($user, $doctor)) {
|
||||
return $this->error(
|
||||
ErrorCodes::ERR_RATING_NOT_ELIGIBLE,
|
||||
'برای ثبت نظر یا امتیاز باید در یک ماه گذشته نوبت تاییدشده نزد این پزشک داشته باشید',
|
||||
403
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### ۳. کد خطا
|
||||
|
||||
در `src/Shared/Constant/ErrorCodes.php` اگر کد مناسبی نیست، اضافه کن (الگوی نامگذاری موجود را رعایت کن، مثل `ERR_RATING_NOT_ELIGIBLE` با پیام فارسی متناظر). اول فایل را بخوان تا الگو و گروهبندی موجود را ببینی.
|
||||
|
||||
### ۴. endpoint بررسی واجد بودن (برای UI)
|
||||
|
||||
برای اینکه فرانت بتواند دکمهی «ثبت نظر» را شرطی نشان دهد، یک GET سبک اضافه کن:
|
||||
|
||||
```php
|
||||
#[Route('/api/v1/rate/{doctorUuid}/eligibility', name: 'app_rating_rating_eligibility', methods: ['GET'])]
|
||||
#[IsGranted('IS_AUTHENTICATED_FULLY')]
|
||||
public function eligibility(string $doctorUuid, #[CurrentUser] User $user): JsonResponse
|
||||
{
|
||||
$doctor = $this->doctorRepo->findByUuid($doctorUuid);
|
||||
if ($doctor === null) {
|
||||
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'دکتر یافت نشد', 404);
|
||||
}
|
||||
return $this->success(['eligible' => $this->appointmentRepo->hasRecentConfirmed($user, $doctor)]);
|
||||
}
|
||||
```
|
||||
|
||||
> این مسیر باید `IS_AUTHENTICATED_FULLY` باشد (نه عمومی) چون به کاربر فعلی وابسته است. فرانت فقط وقتی صدایش میزند که کاربر لاگین باشد. پاسخ: `{ success, data: { eligible: bool } }`.
|
||||
> مراقب باش annotation مسیر با `GET /api/v1/rate/{doctorUuid}` (getAverage) تداخل نکند — مسیر جدید پسوند `/eligibility` دارد، پس مجزاست.
|
||||
|
||||
### ۵. مستندسازی — `docs/api/rating.md`
|
||||
|
||||
- زیر `POST /api/v1/rate` و `POST /api/v1/comment`: قانون جدید واجد بودن + پاسخ خطای ۴۰۳ با کد `ERR_RATING_NOT_ELIGIBLE` را مستند کن.
|
||||
- بخش جدید برای `GET /api/v1/rate/{doctorUuid}/eligibility`: method/path/permission/response با مثال JSON واقعی.
|
||||
|
||||
## تست
|
||||
|
||||
```bash
|
||||
ddev exec php -l src/Appointment/Repository/AppointmentRepository.php
|
||||
ddev exec php -l src/Rating/Controller/RatingController.php
|
||||
ddev exec php bin/console cache:clear
|
||||
ddev exec php bin/console debug:router | grep -i "rating_eligibility"
|
||||
```
|
||||
|
||||
تست رفتاری (با کاربر لاگینشدهای که نوبت confirmed اخیر دارد و یکی که ندارد):
|
||||
- بدون نوبت اخیر → `POST /rate` و `POST /comment` باید **۴۰۳** با کد `ERR_RATING_NOT_ELIGIBLE` بدهند.
|
||||
- با نوبت confirmed در ۳۰ روز گذشته → باید موفق شوند.
|
||||
- `GET /rate/{uuid}/eligibility` برای هر دو حالت `eligible: true/false` درست برگرداند.
|
||||
- یک نوبت confirmed با `slotStart` قدیمیتر از ۳۰ روز → نباید واجد شرایط محسوب شود.
|
||||
|
||||
> برای ساخت دادهی تست میتوانی مستقیم در DB یک `appointment` با `status='confirmed'` و `slot_start` در بازهی اخیر برای کاربر/پزشک تست درج کنی (مشابه روشی که قبلاً برای تست expiry استفاده شد). از `TEST_USERS.md` برای شناسهها کمک بگیر.
|
||||
|
||||
## نکات مهم
|
||||
|
||||
- **منبع حقیقت بکاند است**؛ گارد باید روی خود `rate`/`comment` باشد، نه فقط endpoint eligibility (که صرفاً برای UX است).
|
||||
- `slotStart` ثانیه است؛ از `time()` و `* 86400` استفاده کن، نه DateTime.
|
||||
- وضعیت معیار **فقط `confirmed`** است (نه `completed`/`pending`/سایر) — طبق خواسته.
|
||||
- بازه: نوبت در ۳۰ روزِ **گذشته** (`since <= slotStart <= now`). نوبت آینده واجد شرایط نیست.
|
||||
- همهی پاسخها از `BaseController` (`$this->error/$this->success`).
|
||||
- طبق Standing Rule، `docs/api/rating.md` در همین session بهروز شود.
|
||||
Reference in New Issue
Block a user