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
+4 -2
View File
@@ -33,7 +33,7 @@ security:
provider: api_doc_provider
public_endpoints:
pattern: ^/(api/v1/user/(send-code|verify-code|register|otp-login|reset-password)|oauth/token$|session/token|api/v1/categorys/|api/v1/doctors$|api/v1/clinics$|api/v1/clinic/doctor-list/|api/v1/clinic/[^/]+/addresses$|api/v1/clinic-pro/doctor-addresses/|api/v1/appointment-slots|api/v1/appointment-settings/month-availability/|api/v1/comments/|api/v1/rate/|api/v1/blogs$|api/v1/clinic-invitation/|api/v1/pre-registration$)
pattern: ^/(api/v1/user/(send-code|verify-code|register|otp-login|reset-password)|oauth/token$|session/token|api/v1/categorys/|api/v1/doctors$|api/v1/clinics$|api/v1/clinic/doctor-list/|api/v1/clinic/[^/]+/addresses$|api/v1/clinic-pro/doctor-addresses/|api/v1/appointment-slots|api/v1/appointment-settings/month-availability/|api/v1/comments/|api/v1/rate/[^/]+$|api/v1/blogs$|api/v1/clinic-invitation/|api/v1/pre-registration$)
stateless: true
security: false
@@ -62,7 +62,9 @@ security:
- { path: ^/api/v1/appointment-slots, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/appointment-settings/month-availability/, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/comments/, roles: PUBLIC_ACCESS }
- { path: ^/api/v1/rate/, roles: PUBLIC_ACCESS }
- path: '^/api/v1/rate/[^/]+$'
methods: [GET]
roles: PUBLIC_ACCESS
- { path: ^/api/v1/blogs$, roles: PUBLIC_ACCESS }
- path: '^/api/v1/blog/[^/]+$'
methods: [GET]