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);
}
@@ -80,6 +80,30 @@ class AppointmentRepository extends ServiceEntityRepository
return $this->findBy($criteria, ['slotStart' => 'DESC']);
}
/** Whether the user had a confirmed appointment with this doctor within the last $sinceDays days. */
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;
}
/** @return Appointment[] pending bookings whose 15-minute payment window has lapsed */
public function findPaymentExpired(int $now): array
{
+101 -21
View File
@@ -2,6 +2,7 @@
namespace App\Rating\Controller;
use App\Appointment\Repository\AppointmentRepository;
use App\Auth\Entity\User;
use App\Doctor\Repository\DoctorRepository;
use App\Rating\Entity\Comment;
@@ -27,6 +28,7 @@ class RatingController extends BaseController
private readonly CommentRepository $commentRepo,
private readonly LikeRepository $likeRepo,
private readonly DoctorRepository $doctorRepo,
private readonly AppointmentRepository $appointmentRepo,
) {}
// ── Ratings ───────────────────────────────────────────────────────────────
@@ -67,10 +69,14 @@ class RatingController extends BaseController
{
$data = json_decode($request->getContent(), true) ?? [];
$doctorUuid = trim($data['doctor_uuid'] ?? '');
$score = (int) ($data['score'] ?? 0);
if ($score < 1 || $score > 5) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'امتیاز باید بین ۱ تا ۵ باشد', 422);
$dimensions = [];
foreach (array_keys(Rate::DIMENSIONS) as $name) {
$value = (int) ($data[$name] ?? -1);
if ($value < 0 || $value > 100) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'هر امتیاز باید بین ۰ تا ۱۰۰ باشد', 422);
}
$dimensions[$name] = $value;
}
$doctor = $this->doctorRepo->findByUuid($doctorUuid);
@@ -78,17 +84,21 @@ class RatingController extends BaseController
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'دکتر یافت نشد', 404);
}
$existing = $this->rateRepo->findByUserAndDoctor($user, $doctor);
if ($existing !== null) {
$existing->setScore($score);
$this->rateRepo->save($existing);
return $this->success(['data' => $existing->toArray()]);
if (!$this->appointmentRepo->hasRecentConfirmed($user, $doctor)) {
return $this->error(ErrorCodes::ERR_RATING_NOT_ELIGIBLE, ErrorCodes::message(ErrorCodes::ERR_RATING_NOT_ELIGIBLE), 403);
}
$rate = new Rate($user, $doctor, $score);
$existing = $this->rateRepo->findByUserAndDoctor($user, $doctor);
if ($existing !== null) {
$existing->setDimensions($dimensions);
$this->rateRepo->save($existing);
return $this->success(['data' => $this->rateRepo->getAggregate($doctor)]);
}
$rate = new Rate($user, $doctor, $dimensions);
$this->rateRepo->save($rate);
return $this->success(['data' => $rate->toArray()], 201);
return $this->success(['data' => $this->rateRepo->getAggregate($doctor)], 201);
}
#[OA\Get(
@@ -125,7 +135,47 @@ class RatingController extends BaseController
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'دکتر یافت نشد', 404);
}
return $this->success(['average' => $this->rateRepo->getAverageScore($doctor)]);
return $this->success(['data' => $this->rateRepo->getAggregate($doctor)]);
}
#[OA\Get(
path: '/api/v1/rate/{doctorUuid}/eligibility',
summary: 'Whether the current user may rate/comment on this doctor',
security: [['bearerAuth' => []]],
parameters: [
new OA\Parameter(name: 'doctorUuid', in: 'path', required: true, schema: new OA\Schema(type: 'string')),
],
responses: [
new OA\Response(
response: 200,
description: 'Eligibility status',
content: new OA\JsonContent(
properties: [
new OA\Property(property: 'success', type: 'boolean', example: true),
new OA\Property(
property: 'data',
properties: [
new OA\Property(property: 'eligible', type: 'boolean'),
],
type: 'object'
),
]
)
),
new OA\Response(response: 401, description: 'Unauthorized'),
new OA\Response(response: 404, description: 'Doctor not found'),
]
)]
#[Route('/api/v1/rate/{doctorUuid}/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)]);
}
// ── Comments ──────────────────────────────────────────────────────────────
@@ -166,7 +216,8 @@ class RatingController extends BaseController
{
$data = json_decode($request->getContent(), true) ?? [];
$doctorUuid = trim($data['doctor_uuid'] ?? '');
$body = trim($data['body'] ?? '');
$body = trim($data['comment'] ?? '');
$parentUuid = trim($data['parent'] ?? '');
if (empty($body)) {
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'متن نظر الزامی است', 422);
@@ -177,10 +228,22 @@ class RatingController extends BaseController
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'دکتر یافت نشد', 404);
}
$comment = new Comment($user, $doctor, $body);
if (!$this->appointmentRepo->hasRecentConfirmed($user, $doctor)) {
return $this->error(ErrorCodes::ERR_RATING_NOT_ELIGIBLE, ErrorCodes::message(ErrorCodes::ERR_RATING_NOT_ELIGIBLE), 403);
}
$parent = null;
if ($parentUuid !== '') {
$parent = $this->commentRepo->findByUuid($parentUuid);
if ($parent === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'نظر والد یافت نشد', 404);
}
}
$comment = new Comment($user, $doctor, $body, $parent);
$this->commentRepo->save($comment);
return $this->success(['data' => $comment->toArray()], 201);
return $this->success(['data' => $comment->toArray($user)], 201);
}
#[OA\Get(
@@ -213,7 +276,7 @@ class RatingController extends BaseController
$comments = array_map(
fn(Comment $c) => $c->toArray(),
$this->commentRepo->findApprovedByDoctor($doctor)
$this->commentRepo->findApprovedRootsByDoctor($doctor)
);
return $this->success(['data' => $comments]);
@@ -412,21 +475,38 @@ class RatingController extends BaseController
)]
#[IsGranted('IS_AUTHENTICATED_FULLY')]
#[Route('/api/v1/like/{commentUuid}', methods: ['POST'])]
public function toggleLike(string $commentUuid, #[CurrentUser] User $user): JsonResponse
public function toggleLike(string $commentUuid, Request $request, #[CurrentUser] User $user): JsonResponse
{
$comment = $this->commentRepo->findByUuid($commentUuid);
if ($comment === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'نظر یافت نشد', 404);
}
$data = json_decode($request->getContent(), true) ?? [];
$value = ((int) ($data['value'] ?? 1)) >= 0 ? 1 : -1;
$existing = $this->likeRepo->findByUserAndComment($user, $comment);
if ($existing !== null) {
$this->likeRepo->remove($existing);
return $this->success(['liked' => false, 'likes' => $comment->getLikes()->count() - 1]);
if ($existing->getValue() === $value) {
$this->likeRepo->remove($existing);
} else {
$existing->setValue($value);
$this->likeRepo->save($existing);
}
} else {
$this->likeRepo->save(new Like($user, $comment, $value));
}
$like = new Like($user, $comment);
$this->likeRepo->save($like);
return $this->success(['liked' => true, 'likes' => $comment->getLikes()->count()], 201);
$counts = $this->likeRepo->countByComment($comment);
$current = $this->likeRepo->findByUserAndComment($user, $comment);
return $this->success([
'like_count' => $counts['like_count'],
'dislike_count' => $counts['dislike_count'],
'current_user_like' => [
'like' => $current !== null && $current->getValue() >= 0,
'dislike' => $current !== null && $current->getValue() < 0,
],
]);
}
}
+54 -9
View File
@@ -34,6 +34,13 @@ class Comment
#[ORM\JoinColumn(name: 'doctor_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
private Doctor $doctor;
#[ORM\ManyToOne(targetEntity: Comment::class, inversedBy: 'replies')]
#[ORM\JoinColumn(name: 'parent_id', referencedColumnName: 'id', nullable: true, onDelete: 'CASCADE')]
private ?Comment $parent = null;
#[ORM\OneToMany(targetEntity: Comment::class, mappedBy: 'parent')]
private Collection $replies;
#[ORM\Column(type: 'text')]
private string $body;
@@ -49,13 +56,15 @@ class Comment
#[ORM\Column(name: 'updated_at', type: 'integer')]
private int $updatedAt;
public function __construct(User $user, Doctor $doctor, string $body)
public function __construct(User $user, Doctor $doctor, string $body, ?Comment $parent = null)
{
$this->uuid = Uuid::v4()->toRfc4122();
$this->user = $user;
$this->doctor = $doctor;
$this->body = $body;
$this->parent = $parent;
$this->likes = new ArrayCollection();
$this->replies = new ArrayCollection();
$this->createdAt = time();
$this->updatedAt = time();
}
@@ -67,22 +76,58 @@ class Comment
public function getBody(): string { return $this->body; }
public function getStatus(): string { return $this->status; }
public function getLikes(): Collection { return $this->likes; }
public function getParent(): ?Comment { return $this->parent; }
public function getReplies(): Collection { return $this->replies; }
public function setBody(string $v): self { $this->body = $v; $this->updatedAt = time(); return $this; }
public function approve(): self { $this->status = self::STATUS_APPROVED; $this->updatedAt = time(); return $this; }
public function reject(): self { $this->status = self::STATUS_REJECTED; $this->updatedAt = time(); return $this; }
public function toArray(): array
public function toArray(?User $currentUser = null): array
{
$likeCount = 0;
$dislikeCount = 0;
$userLike = false;
$userDislike = false;
foreach ($this->likes as $like) {
if ($like->getValue() >= 0) {
$likeCount++;
} else {
$dislikeCount++;
}
if ($currentUser !== null && $like->getUser()->getId() === $currentUser->getId()) {
$userLike = $like->getValue() >= 0;
$userDislike = $like->getValue() < 0;
}
}
$replies = [];
foreach ($this->replies as $reply) {
if ($reply->getStatus() === self::STATUS_APPROVED) {
$replies[] = $reply->toArray($currentUser);
}
}
return [
'uuid' => $this->uuid,
'doctor_uuid' => $this->doctor->getUuid(),
'user_uuid' => $this->user->getUuid(),
'body' => $this->body,
'status' => $this->status,
'likes' => $this->likes->count(),
'created_at' => $this->createdAt,
'uuid' => $this->uuid,
'comment' => $this->body,
'created' => $this->createdAt,
'parent' => $this->parent?->getUuid(),
'author' => [
'real_name' => $this->user->getRealName() ?? 'کاربر نوبت‌۷۲۴',
'picture' => [],
],
'like_status' => [
'like_count' => $likeCount,
'dislike_count' => $dislikeCount,
'current_user_like' => [
'like' => $userLike,
'dislike' => $userDislike,
],
],
'replies' => $replies,
];
}
}
+10 -1
View File
@@ -27,14 +27,19 @@ class Like
#[ORM\JoinColumn(name: 'comment_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
private Comment $comment;
/** 1 = like, -1 = dislike */
#[ORM\Column(type: 'smallint')]
private int $value;
#[ORM\Column(name: 'created_at', type: 'integer')]
private int $createdAt;
public function __construct(User $user, Comment $comment)
public function __construct(User $user, Comment $comment, int $value)
{
$this->uuid = Uuid::v4()->toRfc4122();
$this->user = $user;
$this->comment = $comment;
$this->value = $value >= 0 ? 1 : -1;
$this->createdAt = time();
}
@@ -42,6 +47,9 @@ class Like
public function getUuid(): string { return $this->uuid; }
public function getUser(): User { return $this->user; }
public function getComment(): Comment { return $this->comment; }
public function getValue(): int { return $this->value; }
public function setValue(int $v): self { $this->value = $v >= 0 ? 1 : -1; return $this; }
public function toArray(): array
{
@@ -49,6 +57,7 @@ class Like
'uuid' => $this->uuid,
'comment_uuid' => $this->comment->getUuid(),
'user_uuid' => $this->user->getUuid(),
'value' => $this->value,
'created_at' => $this->createdAt,
];
}
+53 -13
View File
@@ -12,6 +12,15 @@ use Symfony\Component\Uid\Uuid;
#[ORM\UniqueConstraint(name: 'idx_rates_user_doctor', columns: ['user_id', 'doctor_id'])]
class Rate
{
/** Dimension column => Persian label. Drives the public aggregate response. */
public const DIMENSIONS = [
'waiting_time_at_clinic' => 'زمان انتظار در مطب',
'accuracy_of_diagnosis' => 'تشخیص درست',
'doctor_behavior' => 'برخورد مناسب پزشک',
'clinic_cleanliness' => 'نظافت مطب',
'doctor_expertise' => 'مهارت پزشک',
];
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
@@ -28,8 +37,20 @@ class Rate
#[ORM\JoinColumn(name: 'doctor_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
private Doctor $doctor;
#[ORM\Column(type: 'smallint')]
private int $score;
#[ORM\Column(name: 'waiting_time_at_clinic', type: 'smallint')]
private int $waitingTimeAtClinic = 0;
#[ORM\Column(name: 'accuracy_of_diagnosis', type: 'smallint')]
private int $accuracyOfDiagnosis = 0;
#[ORM\Column(name: 'doctor_behavior', type: 'smallint')]
private int $doctorBehavior = 0;
#[ORM\Column(name: 'clinic_cleanliness', type: 'smallint')]
private int $clinicCleanliness = 0;
#[ORM\Column(name: 'doctor_expertise', type: 'smallint')]
private int $doctorExpertise = 0;
#[ORM\Column(name: 'created_at', type: 'integer')]
private int $createdAt;
@@ -37,31 +58,50 @@ class Rate
#[ORM\Column(name: 'updated_at', type: 'integer')]
private int $updatedAt;
public function __construct(User $user, Doctor $doctor, int $score)
/** @param array<string,int> $dimensions keyed by DIMENSIONS keys (0100) */
public function __construct(User $user, Doctor $doctor, array $dimensions)
{
$this->uuid = Uuid::v4()->toRfc4122();
$this->user = $user;
$this->doctor = $doctor;
$this->score = max(1, min(5, $score));
$this->createdAt = time();
$this->updatedAt = time();
$this->setDimensions($dimensions);
}
public function getId(): ?int { return $this->id; }
public function getUuid(): string { return $this->uuid; }
public function getUser(): User { return $this->user; }
public function getDoctor(): Doctor { return $this->doctor; }
public function getScore(): int { return $this->score; }
public function setScore(int $v): self { $this->score = max(1, min(5, $v)); $this->updatedAt = time(); return $this; }
public function getWaitingTimeAtClinic(): int { return $this->waitingTimeAtClinic; }
public function getAccuracyOfDiagnosis(): int { return $this->accuracyOfDiagnosis; }
public function getDoctorBehavior(): int { return $this->doctorBehavior; }
public function getClinicCleanliness(): int { return $this->clinicCleanliness; }
public function getDoctorExpertise(): int { return $this->doctorExpertise; }
public function toArray(): array
/** @param array<string,int> $dimensions keyed by DIMENSIONS keys (0100) */
public function setDimensions(array $dimensions): self
{
return [
'uuid' => $this->uuid,
'doctor_uuid' => $this->doctor->getUuid(),
'score' => $this->score,
'created_at' => $this->createdAt,
];
$clamp = static fn($v) => max(0, min(100, (int) $v));
$this->waitingTimeAtClinic = $clamp($dimensions['waiting_time_at_clinic'] ?? $this->waitingTimeAtClinic);
$this->accuracyOfDiagnosis = $clamp($dimensions['accuracy_of_diagnosis'] ?? $this->accuracyOfDiagnosis);
$this->doctorBehavior = $clamp($dimensions['doctor_behavior'] ?? $this->doctorBehavior);
$this->clinicCleanliness = $clamp($dimensions['clinic_cleanliness'] ?? $this->clinicCleanliness);
$this->doctorExpertise = $clamp($dimensions['doctor_expertise'] ?? $this->doctorExpertise);
$this->updatedAt = time();
return $this;
}
/** Overall percentage (0100): mean of the five dimensions. */
public function overallPercent(): float
{
return (
$this->waitingTimeAtClinic
+ $this->accuracyOfDiagnosis
+ $this->doctorBehavior
+ $this->clinicCleanliness
+ $this->doctorExpertise
) / 5;
}
}
@@ -19,6 +19,15 @@ class CommentRepository extends ServiceEntityRepository
return $this->findBy(['doctor' => $doctor, 'status' => Comment::STATUS_APPROVED], ['createdAt' => 'DESC']);
}
/** @return Comment[] approved root comments (no parent) for a doctor */
public function findApprovedRootsByDoctor(Doctor $doctor): array
{
return $this->findBy(
['doctor' => $doctor, 'status' => Comment::STATUS_APPROVED, 'parent' => null],
['createdAt' => 'DESC']
);
}
/** @return Comment[] */
public function findPending(): array
{
+23
View File
@@ -14,6 +14,29 @@ class LikeRepository extends ServiceEntityRepository
public function findByUserAndComment(User $user, Comment $comment): ?Like { return $this->findOneBy(['user' => $user, 'comment' => $comment]); }
/** @return array{like_count:int,dislike_count:int} */
public function countByComment(Comment $comment): array
{
$rows = $this->createQueryBuilder('l')
->select('l.value as value, COUNT(l.id) as cnt')
->where('l.comment = :comment')
->setParameter('comment', $comment)
->groupBy('l.value')
->getQuery()->getResult();
$likeCount = 0;
$dislikeCount = 0;
foreach ($rows as $row) {
if ((int) $row['value'] >= 0) {
$likeCount = (int) $row['cnt'];
} else {
$dislikeCount = (int) $row['cnt'];
}
}
return ['like_count' => $likeCount, 'dislike_count' => $dislikeCount];
}
public function save(Like $e, bool $flush = true): void { $this->getEntityManager()->persist($e); if ($flush) $this->getEntityManager()->flush(); }
public function remove(Like $e, bool $flush = true): void { $this->getEntityManager()->remove($e); if ($flush) $this->getEntityManager()->flush(); }
}
+30 -4
View File
@@ -14,14 +14,40 @@ class RateRepository extends ServiceEntityRepository
public function findByUserAndDoctor(User $user, Doctor $doctor): ?Rate { return $this->findOneBy(['user' => $user, 'doctor' => $doctor]); }
public function getAverageScore(Doctor $doctor): float
/**
* Aggregate the five rating dimensions for a doctor.
*
* @return array{point: float, satisfaction: int, averages: list<array{name:string,label:string,progress:int}>}
*/
public function getAggregate(Doctor $doctor): array
{
$result = $this->createQueryBuilder('r')
->select('AVG(r.score) as avg, COUNT(r.id) as cnt')
$row = $this->createQueryBuilder('r')
->select(
'AVG(r.waitingTimeAtClinic) as waiting_time_at_clinic',
'AVG(r.accuracyOfDiagnosis) as accuracy_of_diagnosis',
'AVG(r.doctorBehavior) as doctor_behavior',
'AVG(r.clinicCleanliness) as clinic_cleanliness',
'AVG(r.doctorExpertise) as doctor_expertise'
)
->where('r.doctor = :doctor')
->setParameter('doctor', $doctor)
->getQuery()->getSingleResult();
return round((float)($result['avg'] ?? 0), 1);
$averages = [];
$sum = 0;
foreach (Rate::DIMENSIONS as $name => $label) {
$progress = (int) round((float) ($row[$name] ?? 0));
$averages[] = ['name' => $name, 'label' => $label, 'progress' => $progress];
$sum += $progress;
}
$satisfaction = (int) round($sum / count(Rate::DIMENSIONS));
return [
'point' => round($satisfaction / 20, 1),
'satisfaction' => $satisfaction,
'averages' => $averages,
];
}
public function save(Rate $e, bool $flush = true): void { $this->getEntityManager()->persist($e); if ($flush) $this->getEntityManager()->flush(); }
+4
View File
@@ -69,6 +69,9 @@ class ErrorCodes
// Rate Limit
public const ERR_RATE_LIMIT_001 = 'ERR_RATE_LIMIT_001';
// Rating
public const ERR_RATING_NOT_ELIGIBLE = 'ERR_RATING_NOT_ELIGIBLE';
public static function message(string $code): string
{
return match ($code) {
@@ -105,6 +108,7 @@ class ErrorCodes
self::ERR_PATIENT_NOT_FOUND => 'پرونده بیمار یافت نشد',
self::ERR_SESSION_NOT_FOUND => 'مراجعه یافت نشد',
self::ERR_SMS_WALLET_INSUFFICIENT => 'موجودی کیف پیامک کافی نیست',
self::ERR_RATING_NOT_ELIGIBLE => 'برای ثبت نظر یا امتیاز باید در یک ماه گذشته نوبت تایید‌شده نزد این پزشک داشته باشید',
default => 'خطای ناشناخته',
};
}