Files
clinicpro/docs/tasks/task-10-appointment/task.md
T
hamed de1a78a235 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.
2026-06-09 22:00:34 +03:30

8.5 KiB

تسک ۱۰: ماژول نوبت‌دهی

توضیح

سیستم رزرو نوبت شامل نمایش اسلات‌های خالی، رزرو نوبت، لغو نوبت، روزهای غیرقابل رزرو و لیست نوبت‌های کاربر.

Endpoint ها

متد مسیر توضیح نیاز به Auth
GET /api/v1/appointment-slots اسلات‌های خالی دکتر در تاریخ خیر
POST /api/v1/appointment رزرو نوبت بله
GET /api/v1/appointment/not-available/{doctorId} روزهای غیرقابل رزرو خیر
GET /api/v1/appointment/my-appointments/{userId} نوبت‌های من بله
PATCH /api/v1/appointment/{uuid}/cancel لغو نوبت توسط کاربر بله (Owner)
PATCH /api/v1/appointment/{uuid}/status تغییر وضعیت نوبت بله (Doctor/Secretary/Admin)

پیش‌نیازها

  • تسک ۰۱، ۰۲، ۰۵ (Doctor)، ۰۹ (تنظیمات)، ۱۵ (Payment)

زمان تخمینی

۱۲ تا ۱۵ ساعت


Status Machine نوبت

[ایجاد نوبت]
      ↓
waiting_for_payment ──→ (پرداخت موفق) ──→ reserved
      ↓                                        ↓
   (لغو)                          ┌────────────┤
      ↓                           │            │
  cancelled_by_patient      checked_in    (لغو دکتر)
                                  ↓            ↓
                               waiting   cancelled_by_doctor
                                  ↓
                            in_progress
                                  ↓
                    ┌─────────────┴─────────────┐
                    ↓                           ↓
                 visited                    no_show
                    ↓
                completed

وضعیت‌ها:

وضعیت توضیح چه کسی تغییر می‌دهد
waiting_for_payment منتظر پرداخت سیستم — بعد از رزرو
reserved رزرو شده — پرداخت موفق سیستم — بعد از تأیید پرداخت
checked_in بیمار به مطب رسیده منشی/دکتر
waiting در صف انتظار مطب منشی/دکتر
in_progress ویزیت در حال انجام منشی/دکتر
visited ویزیت انجام شد منشی/دکتر
no_show بیمار نیامد منشی/دکتر
completed کامل شد سیستم
cancelled_by_patient لغو توسط بیمار بیمار (Owner)
cancelled_by_doctor لغو توسط دکتر دکتر/Admin
postponed به تعویق افتاده دکتر/Admin

فلوی کامل رزرو + پرداخت

POST /api/v1/appointment
    ↓
1. بررسی اسلات: آیا time در آن date خالی است؟
2. بررسی holiday/date_override
3. ایجاد appointment با status=waiting_for_payment
4. بازگشت uuid نوبت به کلاینت
    ↓
POST /api/v1/payment (در task-15)
    { appointment_uuid: "...", payment_method: "mellat" }
    ↓
5. ایجاد payment با status=pending
6. دریافت payment_url از درگاه
7. redirect کاربر به درگاه
    ↓
[Callback از درگاه بانک]
    ↓
8. تأیید پرداخت → payments.status = 'received'
9. appointments.status = 'reserved'
10. واریز کمیسیون به کیف پول نماینده (اگر از دامنه نماینده)

⚠ نکته: اگر در ۳۰ دقیقه پرداخت نشود → waiting_for_payment به cancelled_by_system تغییر کند (job)


GET /api/v1/appointment-slots

Query params:
  doctor_uuid (الزامی)
  date (الزامی) — فرمت: YYYY-MM-DD
{
  "success": true,
  "data": {
    "date": "2024-03-20",
    "doctor": { "uuid": "...", "name": "دکتر احمدی" },
    "slots": [
      { "time": "09:00", "available": true, "duration": 30 },
      { "time": "09:30", "available": false, "duration": 30 },
      { "time": "10:00", "available": true, "duration": 30 }
    ]
  }
}

منطق محاسبه اسلات‌های خالی:

1. بارگذاری weekly_schedule دکتر برای روز هفته مربوطه
2. بررسی date_override برای تاریخ مشخص
3. بررسی holiday (اگر تاریخ در بازه تعطیلی است → همه اسلات‌ها unavailable)
4. خواندن نوبت‌های موجود با status ≠ cancelled → آن اسلات‌ها unavailable
5. بازگشت لیست اسلات‌ها با وضعیت available/unavailable

POST /api/v1/appointment

// Request
{
  "doctor_uuid": "61be915b-...",
  "date": "2024-03-20",
  "time": "09:00",
  "address_id": 39,
  "insurance_type_id": null,
  "notes": "درد معده دارم"
}

// Response 201
{
  "success": true,
  "data": {
    "uuid": "...",
    "doctor": { "uuid": "...", "name": "دکتر احمدی" },
    "date": "2024-03-20",
    "time": "09:00",
    "status": "waiting_for_payment",
    "created_at": 1748000000
  }
}

// Response 409 — اسلات گرفته شده
{
  "success": false,
  "errors": [{ "code": "ERR_APPOINTMENT_001", "message": "اسلات انتخاب‌شده در دسترس نیست" }]
}

PATCH /api/v1/appointment/{uuid}/cancel — لغو نوبت

// Request
{ "reason": "به دلیل بیماری نمی‌توانم بیایم" }

// Response 200
{
  "success": true,
  "data": {
    "uuid": "...",
    "status": "cancelled_by_patient",
    "refund_status": "pending"
  }
}

// Response 400 — نوبت قابل لغو نیست
{
  "success": false,
  "errors": [{ "code": "ERR_APPOINTMENT_002", "message": "نوبت در وضعیت فعلی قابل لغو نیست" }]
}

قوانین لغو:

  • فقط نوبت‌های با status waiting_for_payment یا reserved قابل لغو هستند
  • اگر پرداخت شده (reserved) → payments.status = 'refund' و refund شروع می‌شود
  • لغو بعد از checked_in فقط توسط Admin/Doctor مجاز است

PATCH /api/v1/appointment/{uuid}/status

// Request (Doctor/Secretary/Admin)
{ "status": "checked_in" }

// Response 200
{
  "success": true,
  "data": {
    "uuid": "...",
    "status": "checked_in",
    "updated_at": 1748000000
  }
}

Transition های مجاز:

reserved        → checked_in      (Doctor/Secretary)
checked_in      → waiting         (Doctor/Secretary)
waiting         → in_progress     (Doctor/Secretary)
in_progress     → visited         (Doctor/Secretary)
in_progress     → no_show         (Doctor/Secretary)
visited         → completed       (System/Doctor)
reserved        → cancelled_by_doctor (Doctor/Admin)
reserved        → postponed       (Doctor/Admin)

GET /api/v1/appointment/not-available/{doctorId}

{
  "success": true,
  "data": {
    "not_available_dates": [
      "2024-03-20",
      "2024-03-21",
      "2024-04-01"
    ]
  }
}

منطق:

  • روزهایی که holiday هستند
  • روزهایی که date_override با active=false تعریف شده
  • روزهایی که همه اسلات‌ها پر هستند

GET /api/v1/appointment/my-appointments/{userId}

Query params:
  status (اختیاری) — فیلتر بر اساس وضعیت
  page (اختیاری، پیش‌فرض 1)
  limit (اختیاری، پیش‌فرض 10)
{
  "success": true,
  "data": [
    {
      "uuid": "...",
      "doctor": {
        "uuid": "...",
        "name": "دکتر احمدی",
        "specialty": "قلب و عروق",
        "img": [{ "url": "..." }]
      },
      "date": "2024-03-20",
      "time": "09:00",
      "status": "reserved",
      "payment_status": "received",
      "created_at": 1748000000
    }
  ],
  "meta": { "totalRecords": 12, "totalPages": 2, "currentPage": 1 }
}

نکات مهم

  • Optimistic Locking: هنگام رزرو اسلات، از Transaction + Lock استفاده شود تا race condition نباشد
  • Expiry Job: نوبت‌های waiting_for_payment بعد از ۳۰ دقیقه باید auto-cancel شوند (Symfony Scheduler)
  • N+1 Prevention: در لیست نوبت‌ها، دکتر و وضعیت پرداخت با eager loading بارگذاری شوند
  • Timestamps: همه تاریخ/زمان‌ها Unix timestamp (INT) ذخیره می‌شوند