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,71 @@
|
||||
# معماری — تسک ۱۰: ماژول نوبتدهی
|
||||
|
||||
## ساختار فایلها
|
||||
```
|
||||
src/Module/Appointment/
|
||||
├── Controller/
|
||||
│ └── AppointmentController.php
|
||||
├── Service/
|
||||
│ ├── AppointmentService.php
|
||||
│ └── SlotCalculatorService.php ← محاسبه اسلاتهای خالی
|
||||
├── Repository/
|
||||
│ └── AppointmentRepository.php
|
||||
├── Entity/
|
||||
│ └── Appointment.php
|
||||
├── DTO/
|
||||
│ ├── Request/
|
||||
│ │ └── CreateAppointmentRequest.php
|
||||
│ └── Response/
|
||||
│ ├── AppointmentResponse.php
|
||||
│ └── SlotResponse.php
|
||||
└── Voter/
|
||||
└── AppointmentVoter.php
|
||||
```
|
||||
|
||||
## Entity: Appointment
|
||||
```php
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'appointments')]
|
||||
class Appointment
|
||||
{
|
||||
#[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: 'date')]
|
||||
private \DateTimeInterface $appointmentDate;
|
||||
|
||||
#[ORM\Column(length: 10)]
|
||||
private string $appointmentTime; // HH:MM
|
||||
|
||||
// pending, confirmed, cancelled, completed
|
||||
#[ORM\Column(length: 20, default: 'pending')]
|
||||
private string $status;
|
||||
|
||||
#[ORM\Column(length: 30, nullable: true)]
|
||||
private ?string $insuranceType;
|
||||
|
||||
#[ORM\Column(type: 'text', nullable: true)]
|
||||
private ?string $notes;
|
||||
|
||||
#[ORM\OneToOne(targetEntity: Payment::class, mappedBy: 'appointment')]
|
||||
private ?Payment $payment;
|
||||
|
||||
// TimestampableTrait
|
||||
}
|
||||
```
|
||||
|
||||
## SlotCalculatorService
|
||||
این سرویس با استفاده از WeeklySchedule، DateOverride و Holiday
|
||||
اسلاتهای خالی را برای یک دکتر در یک تاریخ مشخص محاسبه میکند:
|
||||
```
|
||||
calculateAvailableSlots(Doctor $doctor, \DateTimeInterface $date): array
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
# پایگاه داده — تسک ۱۰: ماژول نوبتدهی
|
||||
|
||||
## جدول: appointments
|
||||
_(entity_type=appointment — از DB backup تأیید شده)_
|
||||
|
||||
| ستون | نوع | نام Drupal | توضیح |
|
||||
|------|-----|-----------|-------|
|
||||
| id | INT UNSIGNED AUTO_INCREMENT PK | id | |
|
||||
| uuid | CHAR(36) UNIQUE NOT NULL | uuid | |
|
||||
| patient_id | INT FK → users.id NOT NULL | uid | بیمار (owner) |
|
||||
| doctor_id | INT FK → doctors.id NOT NULL | field_doctor_id | entity ref → clinic_pro |
|
||||
| address_id | INT FK → doctor_addresses.id NULL | field_address | entity ref → clinic_pro |
|
||||
| representation_id | INT FK → representations.id NULL | field_representation | entity ref → clinic_pro |
|
||||
| start_time | INT NOT NULL | field_start_time | Unix timestamp (Asia/Tehran) |
|
||||
| end_time | INT NOT NULL | field_end_time | Unix timestamp |
|
||||
| slot | LONGTEXT NULL | field_slot | JSON (ساختار زیر) |
|
||||
| status | VARCHAR(40) DEFAULT 'waiting_for_payment' | field_status | وضعیت |
|
||||
| visited_at | INT NULL | field_visited_at | زمان ویزیت (Unix timestamp) |
|
||||
| info | LONGTEXT NULL | field_info | یادداشت |
|
||||
| created_at | INT NOT NULL | created | Unix timestamp |
|
||||
| updated_at | INT NOT NULL | changed | Unix timestamp |
|
||||
|
||||
## ساختار واقعی JSON فیلد `slot` (از DB backup)
|
||||
```json
|
||||
{
|
||||
"time": "17:00",
|
||||
"status": "available",
|
||||
"start_time_timestamp": 1763472600,
|
||||
"end_time_timestamp": 1763473800,
|
||||
"duration_per_patient": 20,
|
||||
"location_id": 38
|
||||
}
|
||||
```
|
||||
|
||||
## وضعیتهای کامل (field_status) — از config
|
||||
```
|
||||
waiting_for_payment → پیشفرض — منتظر پرداخت
|
||||
reserved → رزروشده (پرداخت انجام شده)
|
||||
auto_cancel_unpaid → لغو خودکار (عدم پرداخت)
|
||||
cancelled_by_patient → لغو توسط بیمار
|
||||
cancelled_by_doctor → لغو توسط دکتر
|
||||
checked_in → بیمار آمده
|
||||
waiting → در صف انتظار
|
||||
in_progress → در حال ویزیت
|
||||
visited → ویزیت تمام شده
|
||||
no_show → غایب
|
||||
postponed → به تعویق افتاده
|
||||
completed → تکمیل شده
|
||||
```
|
||||
|
||||
⚠️ وضعیتهایی که slot را آزاد میکنند (قابل رزرو مجدد):
|
||||
`auto_cancel_unpaid`, `cancelled_by_patient`, `cancelled_by_doctor`
|
||||
|
||||
## ایندکسها
|
||||
```sql
|
||||
CREATE INDEX idx_appointments_patient ON appointments(patient_id);
|
||||
CREATE INDEX idx_appointments_doctor ON appointments(doctor_id);
|
||||
CREATE INDEX idx_appointments_doctor_time ON appointments(doctor_id, start_time);
|
||||
CREATE INDEX idx_appointments_status ON appointments(status);
|
||||
CREATE INDEX idx_appointments_representation ON appointments(representation_id);
|
||||
```
|
||||
|
||||
## نمونه داده واقعی از DB backup
|
||||
```
|
||||
id=1, uuid='617f78af-...', uid=32, doctor_id=29
|
||||
start_time=1763472600, end_time=1763473800
|
||||
slot: {"time":"17:00","status":"available","start_time_timestamp":1763472600,
|
||||
"end_time_timestamp":1763473800,"duration_per_patient":20,"location_id":38}
|
||||
```
|
||||
|
||||
## روابط
|
||||
- `appointments.patient_id` → `users.id`
|
||||
- `appointments.doctor_id` → `doctors.id` (clinic_pro entity)
|
||||
- `appointments.address_id` → `doctor_addresses.id` (clinic_pro entity)
|
||||
- `appointments.representation_id` → `representations.id` (clinic_pro entity)
|
||||
- `appointments` ← `payments.field_reference_id` (OneToOne)
|
||||
|
||||
## نکات مهم
|
||||
- تایمزون: `Asia/Tehran`
|
||||
- `start_time` و `end_time` هر دو Unix timestamp هستند (INT)
|
||||
- `slot.time` ساعت شروع برای نمایش است (HH:MM)
|
||||
- نوبت ابتدا `waiting_for_payment` → بعد پرداخت → `reserved`
|
||||
@@ -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 یا دکتر مرتبط
|
||||
```
|
||||
@@ -0,0 +1,284 @@
|
||||
# تسک ۱۰: ماژول نوبتدهی
|
||||
|
||||
## توضیح
|
||||
سیستم رزرو نوبت شامل نمایش اسلاتهای خالی، رزرو نوبت، لغو نوبت،
|
||||
روزهای غیرقابل رزرو و لیست نوبتهای کاربر.
|
||||
|
||||
## 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) ذخیره میشوند
|
||||
@@ -0,0 +1,48 @@
|
||||
# جریان کاربری — تسک ۱۰: نوبتدهی
|
||||
|
||||
## جریان کامل رزرو نوبت
|
||||
|
||||
```
|
||||
کاربر دکتر را انتخاب میکند
|
||||
│
|
||||
▼
|
||||
GET /api/v1/appointment/not-available/{doctorId}
|
||||
→ دریافت تاریخهای غیر قابل رزرو (برای کالندار)
|
||||
│
|
||||
▼
|
||||
کاربر تاریخ مورد نظر را انتخاب میکند
|
||||
│
|
||||
▼
|
||||
GET /api/v1/appointment-slots?doctor_uuid=...&date=...
|
||||
→ دریافت اسلاتهای خالی آن روز
|
||||
│
|
||||
▼
|
||||
کاربر ساعت مورد نظر را انتخاب میکند
|
||||
│
|
||||
▼
|
||||
POST /api/v1/appointment
|
||||
{ doctor_uuid, date, time, insurance_type, notes }
|
||||
│
|
||||
├─► بررسی موجود بودن اسلات
|
||||
├─► ایجاد appointment با status=pending
|
||||
└─► ایجاد payment با status=pending (تسک ۱۵)
|
||||
│
|
||||
▼
|
||||
کاربر به درگاه پرداخت هدایت میشود (تسک ۱۵)
|
||||
│
|
||||
▼
|
||||
بعد از پرداخت موفق:
|
||||
appointment.status = confirmed
|
||||
payment.status = paid
|
||||
│
|
||||
▼
|
||||
ارسال پیامک تأیید به کاربر و دکتر
|
||||
```
|
||||
|
||||
## جریان مشاهده نوبتهای من
|
||||
|
||||
```
|
||||
GET /api/v1/appointment/my-appointments/{userId}
|
||||
→ لیست همه نوبتها (گذشته و آینده)
|
||||
→ به صورت صعودی بر اساس تاریخ مرتبشده
|
||||
```
|
||||
Reference in New Issue
Block a user