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,130 @@
# معماری — تسک ۰۹: ماژول تنظیمات نوبت‌دهی
## ساختار فایل‌ها
```
src/Module/AppointmentSettings/
├── Controller/
│ ├── WeeklyScheduleController.php
│ ├── DateOverrideController.php
│ └── HolidayController.php
├── Service/
│ ├── WeeklyScheduleService.php
│ ├── DateOverrideService.php
│ └── HolidayService.php
├── Repository/
│ ├── WeeklyScheduleRepository.php
│ ├── DateOverrideRepository.php
│ └── HolidayRepository.php
├── Entity/
│ ├── WeeklySchedule.php
│ ├── DateOverride.php
│ └── Holiday.php
└── DTO/
├── Request/
│ ├── CreateWeeklyScheduleRequest.php
│ ├── CreateDateOverrideRequest.php
│ └── CreateHolidayRequest.php
└── Response/
├── WeeklyScheduleResponse.php
└── DateOverrideResponse.php
```
## Entity: WeeklySchedule
```php
#[ORM\Entity]
#[ORM\Table(name: 'weekly_schedules')]
class WeeklySchedule
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private int $id;
#[ORM\Column(type: UuidType::NAME, unique: true)]
private Uuid $uuid;
#[ORM\OneToOne(targetEntity: Doctor::class)]
private Doctor $doctor;
// هر روز هفته یک JSON: {active, slots: [{start, end, duration}]}
#[ORM\Column(type: 'json')]
private array $saturday = ['active' => false, 'slots' => []];
#[ORM\Column(type: 'json')]
private array $sunday = ['active' => false, 'slots' => []];
#[ORM\Column(type: 'json')]
private array $monday = ['active' => false, 'slots' => []];
#[ORM\Column(type: 'json')]
private array $tuesday = ['active' => false, 'slots' => []];
#[ORM\Column(type: 'json')]
private array $wednesday = ['active' => false, 'slots' => []];
#[ORM\Column(type: 'json')]
private array $thursday = ['active' => false, 'slots' => []];
#[ORM\Column(type: 'json')]
private array $friday = ['active' => false, 'slots' => []];
// TimestampableTrait
}
```
## Entity: DateOverride
```php
#[ORM\Entity]
#[ORM\Table(name: 'date_overrides')]
class DateOverride
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private int $id;
#[ORM\Column(type: UuidType::NAME, unique: true)]
private Uuid $uuid;
#[ORM\ManyToOne(targetEntity: Doctor::class)]
private Doctor $doctor;
#[ORM\Column(type: 'date')]
private \DateTimeInterface $date;
#[ORM\Column(type: 'boolean', default: false)]
private bool $active;
#[ORM\Column(length: 200, nullable: true)]
private ?string $reason;
#[ORM\Column(type: 'json', nullable: true)]
private ?array $customSlots; // [{start, end, duration}]
// TimestampableTrait
}
```
## Entity: Holiday
```php
#[ORM\Entity]
#[ORM\Table(name: 'holidays')]
class Holiday
{
#[ORM\Id, ORM\GeneratedValue, ORM\Column]
private int $id;
#[ORM\Column(type: UuidType::NAME, unique: true)]
private Uuid $uuid;
#[ORM\ManyToOne(targetEntity: Doctor::class)]
private Doctor $doctor;
#[ORM\Column(type: 'date')]
private \DateTimeInterface $startDate;
#[ORM\Column(type: 'date')]
private \DateTimeInterface $endDate;
#[ORM\Column(length: 200, nullable: true)]
private ?string $reason;
// TimestampableTrait
}
```
@@ -0,0 +1,121 @@
# پایگاه داده — تسک ۰۹: ماژول تنظیمات نوبت‌دهی
## مهم: ساختار واقعی field_setting از DB backup
**تفاوت اساسی با طراحی اولیه:**
- یک فیلد JSON به نام `field_setting` کل برنامه هفتگی را ذخیره می‌کند
- ساختار: **آرایه ۷ المان** (ایندکس 0=شنبه تا 6=جمعه)
- هر روز دو نوبت **صبح** و **عصر** دارد (نه slot‌های آرایه‌ای)
## جدول: weekly_schedules
_(entity_type=appointment_settings, bundle=weekly_schedule)_
| ستون | نوع | توضیح |
|------|-----|-------|
| id | INT UNSIGNED AUTO_INCREMENT PK | |
| uuid | CHAR(36) UNIQUE NOT NULL | |
| doctor_id | INT FK → doctors.id UNIQUE | یک رکورد به‌ازای هر دکتر |
| setting | LONGTEXT NOT NULL | JSON برنامه کامل هفتگی |
| created_at | INT NOT NULL | Unix timestamp |
| updated_at | INT NOT NULL | Unix timestamp |
## ساختار واقعی JSON فیلد `setting` (از DB backup)
```json
[
{
"morning": {
"active": 1,
"location_id": 48,
"start_time": "08:00",
"end_time": "12:00",
"patient_limit": 10,
"duration_per_patient": 15,
"has_rest": true,
"rest_interval": 60,
"time_to_rest": 10
},
"evening": {
"active": 0
}
},
{
"morning": { "active": 0 },
"evening": {
"active": 1,
"location_id": 49,
"start_time": "15:00",
"end_time": "18:00",
"patient_limit": 8,
"duration_per_patient": 20,
"has_rest": false
}
},
...
]
```
ایندکس روزها:
| ایندکس | روز |
|--------|-----|
| 0 | شنبه |
| 1 | یکشنبه |
| 2 | دوشنبه |
| 3 | سه‌شنبه |
| 4 | چهارشنبه |
| 5 | پنجشنبه |
| 6 | جمعه |
فیلدهای هر session (morning/evening):
| فیلد | نوع | توضیح |
|------|-----|-------|
| active | 0/1 | آیا این نوبت فعال است |
| location_id | int | ID آدرس مطب (→ doctor_addresses) |
| start_time | "HH:MM" | ساعت شروع |
| end_time | "HH:MM" | ساعت پایان |
| patient_limit | int | حداکثر تعداد بیمار |
| duration_per_patient | int (دقیقه) | مدت هر ویزیت |
| has_rest | boolean | آیا استراحت دارد |
| rest_interval | int (دقیقه) | فاصله استراحت |
| time_to_rest | int (دقیقه) | مدت استراحت |
## جدول: date_overrides
_(entity_type=appointment_settings, bundle=date_override)_
| ستون | نوع | توضیح |
|------|-----|-------|
| id | INT UNSIGNED AUTO_INCREMENT PK | |
| uuid | CHAR(36) UNIQUE NOT NULL | |
| doctor_id | INT FK → doctors.id | دکتر |
| date | INT NOT NULL | تاریخ (Unix timestamp) — field_date |
| active | TINYINT(1) DEFAULT 0 | آیا کار می‌کند — field_active |
| setting | LONGTEXT NULL | JSON اسلات‌های سفارشی (همان ساختار field_setting) |
| created_at | INT NOT NULL | |
| updated_at | INT NOT NULL | |
## جدول: holidays
_(entity_type=appointment_settings, bundle=holidays)_
| ستون | نوع | توضیح |
|------|-----|-------|
| id | INT UNSIGNED AUTO_INCREMENT PK | |
| uuid | CHAR(36) UNIQUE NOT NULL | |
| doctor_id | INT FK → doctors.id | دکتر |
| start_date | INT NOT NULL | تاریخ شروع (Unix timestamp) |
| end_date | INT NOT NULL | تاریخ پایان (Unix timestamp) |
| active | TINYINT(1) DEFAULT 1 | field_active |
| created_at | INT NOT NULL | |
| updated_at | INT NOT NULL | |
## ایندکس‌ها
```sql
CREATE UNIQUE INDEX idx_weekly_schedules_doctor ON weekly_schedules(doctor_id);
CREATE INDEX idx_date_overrides_doctor_date ON date_overrides(doctor_id, date);
CREATE INDEX idx_holidays_doctor_range ON holidays(doctor_id, start_date, end_date);
```
## نکات مهم
- **GET /appointment-settings/{uuid}** — uuid دکتر است، نه uuid schedule
- `setting[0]` ایندکس 0=شنبه تا 6=جمعه (هفته ایرانی)
- هر روز دقیقاً ۲ نوبت (morning و evening) دارد
- session غیرفعال فقط `{"active": 0}` است، بقیه فیلدها ندارد
@@ -0,0 +1,72 @@
# نکات پیاده‌سازی — تسک ۰۹: ماژول تنظیمات نوبت‌دهی
## اولویت‌بندی تنظیمات (از Manual — بخش ۲.۸.۴)
هنگام محاسبه اسلات‌های خالی (تسک ۱۰):
```
1. DateOverride (بالاترین) → اگر Override فعال برای این تاریخ وجود دارد، تعطیلی نادیده گرفته می‌شود
2. Holiday → اگر تاریخ تعطیل است AND override ندارد → روز بسته است
3. WeeklySchedule (پایین) → در صورت نبود override و تعطیلی → برنامه هفتگی
```
## UUID در URL endpoint لیست override ها
مسیر `GET /api/v1/appointment-settings/date-override/list/{uuid}`
→ این `uuid` برابر است با UUID دکتر (نه DateOverride)
## ساختار واقعی هر session در weekly schedule (از Manual و API request)
```json
{
"active": 1,
"number_of_turns": 10, تعداد نوبت (نه patient_limit!)
"turn_time": 10, مدت هر نوبت به دقیقه (نه duration_per_patient!)
"location": { "id": 48 }, آدرس مطب (object، نه فقط ID)
"start_time": "10:00",
"end_time": "13:00"
}
```
ساختار کامل weekly schedule (7 روز — از "0"=شنبه تا "6"=جمعه):
```json
{
"0": {
"morning": { "active": 1, "number_of_turns": 10, "turn_time": 10, "location": {"id": 48}, "start_time": "10:00", "end_time": "13:00" },
"evening": { "active": 0 }
},
"1": { "morning": {"active": 0}, "evening": { "active": 1, ... } },
...
}
```
**مهم:** در DB backup، فیلدهای `patient_limit` و `duration_per_patient` استفاده شده بود.
در API (کلاینت) از `number_of_turns` و `turn_time` استفاده می‌شود.
در Symfony باید هر دو نام را پشتیبانی کنی یا از نام‌های API استفاده کنی.
## مهم: ذخیره‌سازی field_setting (از کد واقعی)
```php
// در Drupal:
$normalized['field_setting'] = json_encode($data['setting'][0]);
// یعنی اولین المان آرایه‌ای که فرانت می‌فرستد ذخیره می‌شود
// در Symfony هم همین رویکرد:
$weeklySchedule->setSetting(json_encode($request->getSetting()[0]));
```
## GET از UUID دکتر (نه UUID schedule)
```php
// weeklyScheduleService.get($uuid) در Drupal:
// 1. ابتدا دکتر با این uuid را پیدا کن → $doctorEntity
// 2. سپس schedule با field_doctor_id = $doctorId را پیدا کن
// در Symfony:
$doctor = $this->doctorRepo->findByUuid($uuid);
$schedule = $this->scheduleRepo->findByDoctor($doctor);
```
## مجوزها
تمام endpoint های این ماژول نیاز به احراز هویت دارند:
```
POST/PATCH/DELETE → دکتر مرتبط (owner) یا ROLE_ADMIN
GET → دکتر مرتبط یا ROLE_ADMIN یا منشی دکتر
```
## user_flow
جریان کامل در فایل جداگانه user_flow.md توضیح داده شده است.
@@ -0,0 +1,66 @@
# تسک ۰۹: ماژول تنظیمات نوبت‌دهی
## توضیح
پیاده‌سازی سیستم تنظیمات نوبت‌دهی دکتر شامل برنامه هفتگی،
override روزهای خاص و تعطیلات.
## Endpoint ها
| متد | مسیر | توضیح | نیاز به Auth |
|-----|------|-------|-------------|
| POST | `/api/v1/appointment-settings/weekly-schedule` | ایجاد برنامه هفتگی | بله (Doctor) |
| PATCH | `/api/v1/appointment-settings/weekly-schedule/{uuid}` | ویرایش برنامه | بله |
| GET | `/api/v1/appointment-settings/weekly-schedule/{uuid}` | دریافت برنامه | بله |
| DELETE | `/api/v1/booking-setting/{uuid}` | حذف تنظیمات | بله |
| GET | `/api/v1/appointment-settings/date-override/list/{uuid}` | لیست override ها | بله |
| POST | `/api/v1/appointment-settings/date-override` | ایجاد override | بله (Doctor) |
| PATCH | `/api/v1/appointment-settings/date-override/{uuid}` | ویرایش override | بله |
| DELETE | `/api/v1/appointment-settings/date-override/{uuid}` | حذف override | بله |
| GET | `/api/v1/appointment-settings/date-override/{uuid}` | دریافت override | بله |
| POST | `/api/v1/appointment-settings/holidays` | ثبت تعطیلات | بله (Doctor) |
| PATCH | `/api/v1/appointment-settings/holidays/{uuid}` | ویرایش تعطیلات | بله |
## پیش‌نیازها
- تسک ۰۱، ۰۲، ۰۵ (Doctor)
## زمان تخمینی
۱۰ تا ۱۲ ساعت
## نمونه Request
### POST /api/v1/appointment-settings/weekly-schedule
```json
{
"doctor_uuid": "61be915b-...",
"schedule": {
"saturday": { "active": true, "slots": [{"start": "09:00", "end": "13:00", "duration": 30}] },
"sunday": { "active": true, "slots": [{"start": "09:00", "end": "13:00", "duration": 30}] },
"monday": { "active": false, "slots": [] },
"tuesday": { "active": true, "slots": [{"start": "14:00", "end": "18:00", "duration": 20}] },
"wednesday": { "active": false, "slots": [] },
"thursday": { "active": true, "slots": [{"start": "09:00", "end": "12:00", "duration": 30}] },
"friday": { "active": false, "slots": [] }
}
}
```
### POST /api/v1/appointment-settings/date-override
```json
{
"doctor_uuid": "...",
"date": "2024-03-20",
"active": false,
"reason": "مرخصی",
"custom_slots": []
}
```
### POST /api/v1/appointment-settings/holidays
```json
{
"doctor_uuid": "...",
"start_date": "2024-03-20",
"end_date": "2024-03-27",
"reason": "نوروز"
}
```
@@ -0,0 +1,67 @@
# جریان کاربری — تسک ۰۹: تنظیمات نوبت‌دهی
## جریان تنظیم اولیه نوبت‌دهی توسط دکتر
```
دکتر وارد پنل می‌شود
POST /api/v1/appointment-settings/weekly-schedule
{ doctor_uuid, schedule: { saturday: {...}, sunday: {...}, ... } }
└─► ذخیره برنامه هفتگی پایه
```
## جریان ثبت مرخصی یا تعطیلات
```
دکتر تعطیلات را ثبت می‌کند
POST /api/v1/appointment-settings/holidays
{ doctor_uuid, start_date, end_date, reason }
└─► در بازه تعطیلات، هیچ نوبتی نمی‌توان گرفت
```
## جریان override یک روز خاص
```
دکتر می‌خواهد یک روز خاص را سفارشی کند
├─► غیرفعال کردن یک روز:
│ POST /date-override { date: "2024-03-15", active: false }
└─► اسلات سفارشی برای یک روز:
POST /date-override {
date: "2024-03-15",
active: true,
custom_slots: [{ start: "10:00", end: "12:00", duration: 20 }]
}
```
## الگوریتم محاسبه اسلات‌های خالی (تسک ۱۰ از این استفاده می‌کند)
```
برای تاریخ درخواست‌شده:
آیا در بازه Holiday است؟
بله → نوبت موجود نیست
خیر │
آیا DateOverride برای این تاریخ وجود دارد؟
بله → active=false: نوبت موجود نیست
active=true: از custom_slots استفاده کن
خیر │
از WeeklySchedule روز هفته مربوطه استفاده کن
active=false: نوبت موجود نیست
active=true: اسلات‌های slots را محاسبه کن
اسلات‌های رزروشده را حذف کن (از جدول appointments)
لیست اسلات‌های خالی را برگردان
```