feat: Implement SMS sending functionality with KavehNegar and Rangineh providers

- Add SendSmsMessage class for encapsulating SMS message data.
- Create KavehNegarProvider and RanginehProvider classes implementing SmsProviderInterface for sending SMS.
- Implement SmsLogRepository and SmsTemplateRepository for managing SMS logs and templates.
- Develop SendSmsHandler for handling SMS sending messages.
- Create SmsService to manage SMS dispatching and logging.
- Add UserProfileController for managing user profiles with CRUD operations.
- Implement UserProfile entity and repository for user profile data management.
- Update symfony.lock and bootstrap.php for project dependencies and environment setup.
This commit is contained in:
hamed
2026-06-09 22:00:34 +03:30
commit de1a78a235
222 changed files with 36388 additions and 0 deletions
@@ -0,0 +1,89 @@
# معماری — تسک ۱۲: ماژول امتیاز و نظرات
## ساختار فایل‌ها
```
src/Module/Rating/
├── Controller/
│ ├── RatingController.php
│ └── CommentController.php
├── Service/
│ ├── RatingService.php ← آپدیت average_rating دکتر
│ └── CommentService.php
├── Repository/
│ ├── RatingRepository.php
│ └── CommentRepository.php
├── Entity/
│ ├── Rating.php
│ └── Comment.php
├── DTO/
│ ├── Request/
│ │ ├── CreateRatingRequest.php
│ │ ├── CreateCommentRequest.php
│ │ └── ConfirmCommentRequest.php
│ └── Response/
│ ├── RatingResponse.php
│ └── CommentResponse.php
└── Voter/
├── RatingVoter.php
└── CommentVoter.php
```
## Entity: Rating
```php
#[ORM\Entity]
#[ORM\Table(name: 'ratings')]
class Rating
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private int $id;
#[ORM\Column(type: UuidType::NAME, unique: true)]
private Uuid $uuid;
#[ORM\ManyToOne(targetEntity: User::class)]
private User $patient;
#[ORM\ManyToOne(targetEntity: Doctor::class)]
private Doctor $doctor;
#[ORM\Column(type: 'integer')]
private int $score; // 1 تا 5
#[ORM\OneToOne(targetEntity: Appointment::class, nullable: true)]
private ?Appointment $appointment;
// TimestampableTrait
}
```
## Entity: Comment
```php
#[ORM\Entity]
#[ORM\Table(name: 'comments')]
class Comment
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private int $id;
#[ORM\Column(type: UuidType::NAME, unique: true)]
private Uuid $uuid;
#[ORM\ManyToOne(targetEntity: User::class)]
private User $author;
#[ORM\ManyToOne(targetEntity: Doctor::class)]
private Doctor $doctor;
#[ORM\Column(type: 'text')]
private string $text;
// pending, approved, rejected
#[ORM\Column(length: 20, default: 'pending')]
private string $status;
#[ORM\ManyToOne(targetEntity: Rating::class, nullable: true)]
private ?Rating $rating;
// TimestampableTrait
}
```
@@ -0,0 +1,71 @@
# پایگاه داده — تسک ۱۲: ماژول امتیاز و نظرات
## مهم: نام فیلد از DB
فیلد ستاره در Drupal **`field_starts`** است (نه `field_stars`!) — از config تأیید شد.
## جدول: ratings
_(entity_type=clinic_pro_comment, bundle=rate)_
| ستون | نوع | نام Drupal | توضیح |
|------|-----|-----------|-------|
| id | INT UNSIGNED AUTO_INCREMENT PK | id | |
| uuid | CHAR(36) UNIQUE NOT NULL | uuid | |
| user_id | INT FK → users.id NOT NULL | uid | امتیاز‌دهنده |
| doctor_id | INT FK → doctors.id NOT NULL | field_doctor_id | entity ref → clinic_pro |
| doctor_behavior | INT NOT NULL | field_doctor_behavior | برخورد مناسب (0-100) |
| accuracy_of_diagnosis | INT NOT NULL | field_accuracy_of_diagnosis | تشخیص درست (0-100) |
| waiting_time_at_clinic | INT NOT NULL | field_waiting_time_at_clinic | زمان انتظار (0-100) |
| doctor_expertise | INT NOT NULL | field_doctor_expertise | مهارت (0-100) |
| clinic_cleanliness | INT NOT NULL | field_clinic_cleanliness | نظافت (0-100) |
| starts | DECIMAL(10,2) NOT NULL | field_starts | ستاره محاسبه‌شده (0-5) — ⚠️ `starts` نه `stars`! |
| percent | FLOAT NOT NULL | field_percent | درصد محاسبه‌شده (0-100) |
| created_at | INT NOT NULL | created | Unix timestamp |
| updated_at | INT NOT NULL | changed | Unix timestamp |
## جدول: comments
_(entity_type=clinic_pro_comment, bundle=comments)_
| ستون | نوع | نام Drupal | توضیح |
|------|-----|-----------|-------|
| id | INT UNSIGNED AUTO_INCREMENT PK | id | |
| uuid | CHAR(36) UNIQUE NOT NULL | uuid | |
| user_id | INT FK → users.id NOT NULL | uid | نویسنده |
| doctor_id | INT FK → doctors.id NOT NULL | field_doctor_id | دکتر |
| comment | LONGTEXT NOT NULL | field_comment | متن نظر |
| approved | TINYINT(1) DEFAULT 0 | field_approved | تأیید شده (نه ENUM بلکه boolean) |
| parent_id | INT FK → comments.id NULL | field_parent | نظر پدر (پاسخ به نظر) |
| created_at | INT NOT NULL | created | Unix timestamp |
| updated_at | INT NOT NULL | changed | Unix timestamp |
## جدول: likes
_(entity_type=clinic_pro_comment, bundle=like)_
| ستون | نوع | نام Drupal | توضیح |
|------|-----|-----------|-------|
| id | INT UNSIGNED AUTO_INCREMENT PK | id | |
| uuid | CHAR(36) UNIQUE NOT NULL | uuid | |
| user_id | INT FK → users.id NOT NULL | uid | کاربر |
| comment_id | INT FK → comments.id NOT NULL | field_comment_id | نظر مورد لایک |
| is_like | TINYINT(1) NOT NULL | field_like | لایک (1) یا دیس‌لایک (0) |
| created_at | INT NOT NULL | created | Unix timestamp |
| updated_at | INT NOT NULL | changed | Unix timestamp |
## ایندکس‌ها
```sql
-- هر کاربر فقط یک امتیاز برای هر دکتر
CREATE UNIQUE INDEX idx_ratings_user_doctor ON ratings(user_id, doctor_id);
-- هر کاربر فقط یک لایک/دیس‌لایک برای هر نظر
CREATE UNIQUE INDEX idx_likes_user_comment ON likes(user_id, comment_id);
CREATE INDEX idx_ratings_doctor ON ratings(doctor_id);
CREATE INDEX idx_comments_doctor_approved ON comments(doctor_id, approved);
CREATE INDEX idx_comments_parent ON comments(parent_id);
```
## نکته‌های مهم
- فیلد ستاره `starts` است (نه `stars`) — همان‌طور که در config تأیید شد
- `approved` boolean است (0/1)، نه ENUM
- `percent` نوع FLOAT است (نه DECIMAL) — از config تأیید شد
- `starts` نوع DECIMAL(10,2) است — از config تأیید شد
- `comment.status` در جدول پایه Drupal وجود دارد (tinyint) اما از `field_approved` استفاده می‌شود
@@ -0,0 +1,164 @@
# نکات پیاده‌سازی — تسک ۱۲: ماژول امتیاز و نظرات
## ⚠ تناقض نام فیلدها: API vs DB (بسیار مهم!)
فیلدهایی که **کلاینت ارسال می‌کند** با نام فیلدهای **پایگاه داده** متفاوت هستند:
| نام در Request (API) | نام در DB (Drupal field) | توضیح |
|---------------------|------------------------|-------|
| `correct_diagnosis` | `accuracy_of_diagnosis` | دقت تشخیص |
| `doctor_skill` | `doctor_expertise` | مهارت پزشک |
| `behavior_doctor` | `doctor_behavior` | برخورد پزشک |
| `office_cleaning` | `clinic_cleanliness` | نظافت مطب |
| `time_in_office` | `waiting_time_at_clinic` | زمان انتظار |
| `doctor` | `doctor_id` | شناسه دکتر (integer) |
| `rate` | `starts` | امتیاز ستاره‌ای (DECIMAL 10,2) |
**در Symfony باید:**
- ورودی را با نام‌های API دریافت کن (`correct_diagnosis`, ...)
- در Entity و DB با نام‌های Drupal ذخیره کن (`accuracy_of_diagnosis`, ...)
## نمونه واقعی Request — POST /api/v1/clinicpro/rate
```json
{
"correct_diagnosis": 100,
"doctor_skill": 100,
"behavior_doctor": 100,
"office_cleaning": 100,
"time_in_office": 100,
"doctor": 1,
"rate": 2
}
```
## نمونه واقعی Request — PATCH /api/v1/clinicpro/rate/{uuid}
```json
{
"correct_diagnosis": 50,
"doctor_skill": 60,
"behavior_doctor": 70,
"office_cleaning": 80,
"time_in_office": 90,
"doctor": 1,
"rate": 2
}
```
---
## سیستم امتیازدهی وزنی
Rating در Drupal **5 معیار جداگانه** دارد که هر کدام مقدار 0-100 می‌گیرند
و با وزن‌های متفاوت محاسبه می‌شوند:
```php
$weights = [
"doctor_behavior" => 1.5, // behavior_doctor در API
"accuracy_of_diagnosis" => 3.0, // correct_diagnosis در API
"waiting_time_at_clinic" => 1.0, // time_in_office در API
"doctor_expertise" => 2.0, // doctor_skill در API
"clinic_cleanliness" => 1.0, // office_cleaning در API
];
// فرمول محاسبه:
$weightedAverage = SUM(value * weight) / SUM(weights); // از 100
$stars = ($weightedAverage / 100) * 5; // از 5
```
### پیاده‌سازی calculateDoctorRating در Symfony
```php
public function calculateRating(array $apiScores): array
{
// نگاشت نام‌های API به نام‌های DB
$mapped = [
'accuracy_of_diagnosis' => $apiScores['correct_diagnosis'] ?? 0,
'doctor_expertise' => $apiScores['doctor_skill'] ?? 0,
'doctor_behavior' => $apiScores['behavior_doctor'] ?? 0,
'clinic_cleanliness' => $apiScores['office_cleaning'] ?? 0,
'waiting_time_at_clinic' => $apiScores['time_in_office'] ?? 0,
];
$weights = [
'doctor_behavior' => 1.5,
'accuracy_of_diagnosis' => 3.0,
'waiting_time_at_clinic' => 1.0,
'doctor_expertise' => 2.0,
'clinic_cleanliness' => 1.0,
];
$totalScore = 0.0;
$totalWeight = 0.0;
foreach ($weights as $key => $weight) {
$totalScore += $mapped[$key] * $weight;
$totalWeight += $weight;
}
$weightedAverage = $totalWeight > 0 ? $totalScore / $totalWeight : 0;
$stars = ($weightedAverage / 100) * 5;
return [
'percent' => round($weightedAverage, 1),
'starts' => round(min(5.0, max(0.0, $stars)), 2),
// ⚠️ نام فیلد DB: "starts" است نه "stars"!
];
}
```
---
## آمار دکتر — GET /api/v1/clinicpro-comment/doctor-rate/{doctorUuid}
```
URL: /api/v1/clinicpro-comment/doctor-rate/{uuid_دکتر}
Auth: عمومی (بدون احراز هویت)
```
```json
{
"average_stars": 4.3,
"total_rates": 87,
"averages": {
"average_doctor_behavior": 82.1,
"average_accuracy_of_diagnosis": 88.5,
"average_waiting_time_at_clinic": 65.3,
"average_doctor_expertise": 90.2,
"average_clinic_cleanliness": 78.4
}
}
```
---
## تأیید نظرات
نظرات با `approved=0` ذخیره می‌شوند.
ادمین آن‌ها را از `GET /api/v1/clinicpro/unverified-comments/{doctorId}` می‌بیند.
سپس با `PATCH /api/v1/clinicpro/unverified-comments/{commentId}` تأیید می‌کند.
---
## اعتبارسنجی مقادیر Rating
هر معیار باید بین 0 تا 100 باشد:
```php
#[Assert\Range(min: 0, max: 100)]
```
---
## مجوزها
```
POST /api/v1/clinicpro/rate → احراز هویت‌شده
PATCH /api/v1/clinicpro/rate/{uuid} → owner (هر کاربر فقط یک امتیاز برای هر دکتر)
DELETE /api/v1/rate/doctor/{uuid} → ROLE_ADMIN
GET /api/v1/clinicpro/rate/{uuid} → owner (امتیاز کاربر برای دکتر مشخص)
GET /api/v1/clinicpro-comment/doctor-rate/{uuid} → عمومی (آمار کلی دکتر)
POST /api/v1/clinicpro/comment → احراز هویت‌شده
PATCH /api/v1/clinicpro/comment/{uuid} → owner یا ROLE_ADMIN
DELETE /api/v1/clinicpro/comment/{uuid} → owner یا ROLE_ADMIN
GET /api/v1/clinicpro/comment/{uuid} → احراز هویت‌شده
GET /api/v1/clinicpro/comments/{doctorId} → احراز هویت‌شده (فقط approved)
GET /api/v1/clinicpro/unverified-comments/{doctorId} → ROLE_ADMIN
PATCH /api/v1/clinicpro/unverified-comments/{id} → ROLE_ADMIN (تأیید/رد نظر)
POST /api/v1/clinicpro/like → احراز هویت‌شده
PATCH /api/v1/clinicpro/like/{uuid} → owner
```
+99
View File
@@ -0,0 +1,99 @@
# تسک ۱۲: ماژول امتیاز و نظرات
## توضیح
سیستم امتیازدهی (rate) و نظرات (comment) و لایک کاربران برای دکترها،
شامل تأیید نظرات توسط ادمین.
## Endpoint ها (واقعی از Drupal)
### امتیازدهی (Rate)
| متد | مسیر | توضیح | نیاز به Auth |
|-----|------|-------|-------------|
| POST | `/api/v1/clinicpro/rate` | ثبت امتیاز جدید | بله |
| PATCH | `/api/v1/clinicpro/rate/{uuid}` | ویرایش امتیاز | بله (Owner) |
| DELETE | `/api/v1/rate/doctor/{uuid}` | حذف امتیاز | بله (Admin) |
| GET | `/api/v1/clinicpro/rate/{uuid}` | امتیاز من برای دکتر | بله |
| GET | `/api/v1/clinicpro-comment/doctor-rate/{doctor_uuid}` | آمار کلی امتیازهای دکتر | خیر |
### نظرات (Comment)
| متد | مسیر | توضیح | نیاز به Auth |
|-----|------|-------|-------------|
| POST | `/api/v1/clinicpro/comment` | ثبت نظر جدید | بله |
| PATCH | `/api/v1/clinicpro/comment/{uuid}` | ویرایش نظر | بله (Owner/Admin) |
| DELETE | `/api/v1/clinicpro/comment/{uuid}` | حذف نظر | بله (Owner/Admin) |
| GET | `/api/v1/clinicpro/comment/{uuid}` | دریافت یک نظر | بله |
| GET | `/api/v1/clinicpro/comments/{doctorId}` | لیست نظرات دکتر (با page/limit) | بله |
| GET | `/api/v1/clinicpro/unverified-comments/{doctorId}` | نظرات تأییدنشده (با page/limit) | بله (Admin) |
| PATCH | `/api/v1/clinicpro/unverified-comments/{commentId}` | تأیید/رد نظر | بله (Admin) |
### لایک (Like)
| متد | مسیر | توضیح | نیاز به Auth |
|-----|------|-------|-------------|
| POST | `/api/v1/clinicpro/like` | ثبت لایک/دیس‌لایک | بله |
| PATCH | `/api/v1/clinicpro/like/{uuid}` | ویرایش لایک | بله (Owner) |
## پیش‌نیازها
- تسک ۰۱، ۰۲، ۰۵ (Doctor)
## زمان تخمینی
۸ تا ۱۰ ساعت
---
## ⚠ نام فیلدهای Request (متفاوت از DB!)
| فیلد در Request | معادل در DB | توضیح |
|----------------|------------|-------|
| `correct_diagnosis` | `accuracy_of_diagnosis` | دقت تشخیص (0-100) |
| `doctor_skill` | `doctor_expertise` | مهارت پزشک (0-100) |
| `behavior_doctor` | `doctor_behavior` | برخورد پزشک (0-100) |
| `office_cleaning` | `clinic_cleanliness` | نظافت مطب (0-100) |
| `time_in_office` | `waiting_time_at_clinic` | زمان انتظار (0-100) |
| `doctor` | `doctor_id` | شناسه دکتر |
| `rate` | `starts` | امتیاز ستاره (ذخیره محاسبه‌شده) |
---
## نمونه واقعی Request — POST /api/v1/clinicpro/rate
```json
{
"correct_diagnosis": 100,
"doctor_skill": 100,
"behavior_doctor": 100,
"office_cleaning": 100,
"time_in_office": 100,
"doctor": 1,
"rate": 2
}
```
## نمونه واقعی Request — PATCH /api/v1/clinicpro/rate/{uuid}
```json
{
"correct_diagnosis": 50,
"doctor_skill": 60,
"behavior_doctor": 70,
"office_cleaning": 80,
"time_in_office": 90,
"doctor": 1,
"rate": 2
}
```
## نمونه واقعی Request — PATCH /api/v1/clinicpro/unverified-comments/{uuid}
_(تأیید نظر — بدنه خالی یا فقط `approved`)_
```json
{}
```
---
## نکات مهم
- هر کاربر فقط **یک امتیاز** برای هر دکتر می‌تواند ثبت کند (UNIQUE user_id + doctor_id)
- نظرات با `approved=0` ذخیره می‌شوند و باید توسط ادمین تأیید شوند
- لایک فقط برای **نظرات** است (نه بلاگ یا دکتر)
- هر کاربر فقط **یک لایک** برای هر نظر می‌تواند ثبت کند
- URL کامنت‌های تأییدنشده: `{doctorId}` در URL است اما فقط ROLE_ADMIN بررسی می‌شود