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:
@@ -0,0 +1,122 @@
|
||||
# نکات پیادهسازی — تسک ۱۰: ماژول نوبتدهی
|
||||
|
||||
## وضعیتهای واقعی نوبت (از Drupal)
|
||||
```
|
||||
waiting_for_payment → وضعیت پیشفرض هنگام ثبت نوبت
|
||||
confirmed → بعد از پرداخت موفق
|
||||
auto_cancel_unpaid → لغو خودکار به دلیل عدم پرداخت
|
||||
cancelled_by_patient → لغو توسط بیمار
|
||||
cancelled_by_doctor → لغو توسط دکتر
|
||||
```
|
||||
⚠️ در طراحی اولیه `pending/cancelled/completed` بود — اینها **اشتباه** بودند.
|
||||
|
||||
## تشخیص نماینده از HTTP Host (Multi-tenant)
|
||||
```php
|
||||
// در AppointmentService.php Drupal:
|
||||
// نماینده از domain_name=host پیدا میشود
|
||||
private function getRepresentation(string $host): ?int {
|
||||
return $this->representationRepo->findByDomainName($host)?->getId();
|
||||
}
|
||||
// در Symfony: از $request->getHost() استفاده کن
|
||||
$host = $request->getSchemeAndHttpHost() . '/'; // e.g. http://yasuj-nobat.localhost:3000/
|
||||
$representation = $this->representationRepo->findByDomainName($host);
|
||||
```
|
||||
|
||||
## فیلدهای واقعی نوبت (از کد Drupal)
|
||||
```
|
||||
field_doctor_id → entity reference به doctor
|
||||
field_start_time → Unix timestamp (Asia/Tehran)
|
||||
field_end_time → Unix timestamp (Asia/Tehran)
|
||||
field_address → entity reference به doctor_address
|
||||
field_slot → JSON: {start_time_timestamp, end_time_timestamp, location_id, start, end, duration}
|
||||
field_representation → entity reference به representation
|
||||
field_status → string (waiting_for_payment, confirmed, ...)
|
||||
field_visited_at → Unix timestamp (بعد از ویزیت)
|
||||
field_info → یادداشت
|
||||
```
|
||||
|
||||
## اعتبارسنجی slot (از کد Drupal)
|
||||
```php
|
||||
// بررسی start_time معتبر بودن (در آینده، نه گذشته)
|
||||
$checkStartTime = $this->isTimestampValid($startTime, 10); // 10 دقیقه حداقل
|
||||
$checkEndTime = $this->isTimestampValid($endTime, 10);
|
||||
|
||||
// بررسی تداخل (conflict check)
|
||||
$unacceptableStatus = ['auto_cancel_unpaid', 'cancelled_by_patient', 'cancelled_by_doctor'];
|
||||
// اگر نوبتی برای همین doctor + slot وجود داشت که status آن در لیست بالا نبود → خطا
|
||||
```
|
||||
|
||||
## جلوگیری از Race Condition
|
||||
از database transaction + pessimistic write lock استفاده کن:
|
||||
```php
|
||||
$this->entityManager->beginTransaction();
|
||||
try {
|
||||
$existing = $this->repo->findConflictingAppointment(
|
||||
$doctorId, $startTime, $endTime,
|
||||
lockMode: LockMode::PESSIMISTIC_WRITE
|
||||
);
|
||||
if ($existing) throw new SlotAlreadyTakenException();
|
||||
|
||||
$appointment = new Appointment(...);
|
||||
$this->entityManager->persist($appointment);
|
||||
$this->entityManager->flush();
|
||||
$this->entityManager->commit();
|
||||
} catch (\Exception $e) {
|
||||
$this->entityManager->rollback();
|
||||
throw $e;
|
||||
}
|
||||
```
|
||||
|
||||
## Response کامل نوبت (از finalizedData Drupal)
|
||||
```json
|
||||
{
|
||||
"id": 1,
|
||||
"uuid": "...",
|
||||
"status": "waiting_for_payment",
|
||||
"start_time": 1716000000,
|
||||
"end_time": 1716001800,
|
||||
"visited_at": null,
|
||||
"info": null,
|
||||
"slot": {
|
||||
"start_time_timestamp": 1716000000,
|
||||
"end_time_timestamp": 1716001800,
|
||||
"location_id": 42,
|
||||
"start": "09:00",
|
||||
"end": "09:30",
|
||||
"duration": 30
|
||||
},
|
||||
"doctor": {
|
||||
"id": 5,
|
||||
"uuid": "...",
|
||||
"name": "دکتر محمدی",
|
||||
"specialty": {"id": 3, "uuid": "...", "name": "متخصص قلب"}
|
||||
},
|
||||
"address": {
|
||||
"id": 42, "uuid": "...", "name": "مطب شیراز",
|
||||
"address": "...", "phone": "071...",
|
||||
"map": {"latitude": 29.6, "longitude": 52.5}
|
||||
},
|
||||
"patient": {
|
||||
"id": 10, "uuid": "...",
|
||||
"name": "علی رضایی",
|
||||
"mobile": "09120000000",
|
||||
"profile": {"id": 8, "uuid": "..."}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## روزهای غیر قابل رزرو
|
||||
endpoint `GET /appointment/not-available/{doctorId}` تاریخهایی را برمیگرداند که در آنها نوبت خالی نیست:
|
||||
- روزهایی در Holiday جای گرفتهاند
|
||||
- روزهایی که DateOverride با active=false دارند
|
||||
- روزهایی که WeeklySchedule آنها active=false است
|
||||
- روزهایی که همه slotهایشان رزرو فعال دارند
|
||||
|
||||
## مجوزها
|
||||
```
|
||||
POST /appointment → احراز هویتشده
|
||||
GET /appointment-slots/{doctorId} → عمومی
|
||||
GET /appointment/not-available/{id} → عمومی
|
||||
GET /appointment/my-appointments/{id} → owner یا ROLE_ADMIN
|
||||
PATCH /appointment/{uuid}/status → ROLE_ADMIN یا دکتر مرتبط
|
||||
```
|
||||
Reference in New Issue
Block a user