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:
hamed
2026-06-16 00:25:59 +03:30
co-authored by Claude Opus 4.8
parent a5fca5d1ba
commit 45242a3128
16 changed files with 846 additions and 108 deletions
+16 -9
View File
@@ -1091,7 +1091,8 @@ class AdminApiController extends BaseController
$qb = $this->em->createQueryBuilder()
->select(
'r.uuid, r.score, r.createdAt',
'r.uuid, r.createdAt',
'r.waitingTimeAtClinic, r.accuracyOfDiagnosis, r.doctorBehavior, r.clinicCleanliness, r.doctorExpertise',
'u.realName as patient_name, u.mobileNumber as patient_mobile',
'd.name as doctor_name',
)
@@ -1110,14 +1111,20 @@ class AdminApiController extends BaseController
$rows = $qb->setFirstResult(($page - 1) * $limit)->setMaxResults($limit)
->getQuery()->getArrayResult();
$items = array_map(fn(array $r) => [
'uuid' => $r['uuid'],
'patient_name' => $r['patient_name'] ?? $r['patient_mobile'],
'doctor_name' => $r['doctor_name'],
'overall' => (int) $r['score'],
'score' => (int) $r['score'],
'created_at' => date('c', (int) $r['createdAt']),
], $rows);
$items = array_map(function (array $r) {
$overall = (int) round((
$r['waitingTimeAtClinic'] + $r['accuracyOfDiagnosis'] + $r['doctorBehavior']
+ $r['clinicCleanliness'] + $r['doctorExpertise']
) / 5);
return [
'uuid' => $r['uuid'],
'patient_name' => $r['patient_name'] ?? $r['patient_mobile'],
'doctor_name' => $r['doctor_name'],
'overall' => $overall,
'score' => (int) round($overall / 20),
'created_at' => date('c', (int) $r['createdAt']),
];
}, $rows);
return $this->paginated($items, (int) $total, $page, $limit);
}