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

285 lines
8.5 KiB
Markdown

# تسک ۱۰: ماژول نوبت‌دهی
## توضیح
سیستم رزرو نوبت شامل نمایش اسلات‌های خالی، رزرو نوبت، لغو نوبت،
روزهای غیرقابل رزرو و لیست نوبت‌های کاربر.
## 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
```
```json
{
"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
```json
// 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 — لغو نوبت
```json
// 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
```json
// 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}
```json
{
"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)
```
```json
{
"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) ذخیره می‌شوند