# ClinicPro — مستندات کامل پروژه

> **آخرین به‌روزرسانی:** ۱۴۰۵/۰۳/۲۱ | **Symfony:** 7.x | **PHP:** ≥ 8.2 | **React:** 19

---

## فهرست مطالب

1. [معرفی پروژه](#۱-معرفی-پروژه)
2. [تکنولوژی Stack](#۲-تکنولوژی-stack)
3. [راه‌اندازی محیط توسعه](#۳-راه‌اندازی-محیط-توسعه)
4. [آدرس‌های مهم](#۴-آدرس‌های-مهم)
5. [متغیرهای محیطی](#۵-متغیرهای-محیطی)
6. [دستورات Console](#۶-دستورات-console)
7. [امنیت و احراز هویت](#۷-امنیت-و-احراز-هویت)
8. [پایگاه داده — جداول و موجودیت‌ها](#۸-پایگاه-داده--جداول-و-موجودیت‌ها)
9. [API Endpoints](#۹-api-endpoints)
10. [پنل ادمین React](#۱۰-پنل-ادمین-react)
11. [Migrations](#۱۱-migrations)
12. [سرویس‌های خارجی](#۱۲-سرویس‌های-خارجی)
13. [ساختار دایرکتوری](#۱۳-ساختار-دایرکتوری)

---

## ۱. معرفی پروژه

**ClinicPro** سیستم جامع مدیریت کلینیک و نوبت‌دهی پزشکی است — مهاجرت‌یافته از Drupal به **Symfony 7**. شامل یک REST API کامل و یک پنل ادمین React SPA داخل Symfony.

### ویژگی‌های اصلی

- احراز هویت OTP + رمز عبور + JWT
- مدیریت پزشکان با پروفایل کامل، تخصص، آدرس‌های مطب
- مدیریت کلینیک‌ها با گالری، نقشه، دعوت پزشک
- سیستم نوبت‌دهی با اسلات هفتگی، روزهای استثناء، تعطیلات
- درگاه پرداخت (ملت + سپ) با کیف پول و تسویه‌حساب
- مدیریت نمایندگان (agents) با کمیسیون
- منشی با دسترسی‌های دانه‌ای (fine-grained permissions)
- سیستم دعوتنامه پزشک به کلینیک با توکن ۷۲ ساعته
- ارسال پیامک async از طریق KaveNegar / Rangineh
- امتیاز و نظرات پزشکان
- بلاگ با مدیریت کامل
- داشبورد آمار real-time
- Swagger UI محافظت‌شده با HTTP Basic Auth

---

## ۲. تکنولوژی Stack

### Backend

| لایه          | تکنولوژی                         |
| ------------- | -------------------------------- |
| Framework     | Symfony 7.x                      |
| PHP           | ≥ 8.2                            |
| ORM           | Doctrine ORM                     |
| Auth          | LexikJWT + OTP                   |
| Queue         | Symfony Messenger                |
| Cache         | Redis                            |
| Database      | MariaDB 11.8                     |
| API Docs      | NelmioApiDocBundle + swagger-php |
| Rate Limiting | Symfony Rate Limiter             |

### Frontend (Admin SPA)

| لایه          | تکنولوژی              |
| ------------- | --------------------- |
| Framework     | React 19 + TypeScript |
| Routing       | React Router DOM v7   |
| Server State  | TanStack Query v5     |
| Client State  | Zustand v5 (persist)  |
| Forms         | React Hook Form + Zod |
| Charts        | Recharts              |
| Maps          | React Leaflet 5       |
| Icons         | Heroicons v2          |
| Notifications | Sonner                |
| Date          | jalaali-js            |
| Build         | Webpack Encore 6      |

---

## ۳. راه‌اندازی محیط توسعه

### پیش‌نیازها

- [ddev](https://ddev.readthedocs.io/) نصب‌شده

### مراحل

```bash
# ۱. clone و وارد شدن به پروژه
git clone <repo-url> clinic-pro-symfony
cd clinic-pro-symfony

# ۲. راه‌اندازی ddev
ddev start

# ۳. نصب وابستگی‌های PHP
ddev exec composer install

# ۴. تولید کلیدهای JWT
ddev exec php bin/console lexik:jwt:generate-keypair

# ۵. اجرای migration های پایگاه داده
ddev exec php bin/console doctrine:migrations:migrate --no-interaction

# ۶. seed دسته‌بندی‌ها (استان، شهر، تخصص، خدمات پزشک) از data/seed/*.json
ddev exec php bin/console app:seed-categories

# ۷. نصب وابستگی‌های Node و بیلد فرانت‌اند
ddev exec yarn install
ddev exec yarn dev

# ۸. ایجاد اولین ادمین (اختیاری)
ddev exec php bin/console app:create-admin
```

### دستورات روزانه

```bash
# بیلد فرانت‌اند (یکبار)
ddev exec yarn dev

# حالت watch (هنگام توسعه)
ddev exec yarn watch

# پاک کردن cache بعد از تغییر backend
ddev exec php bin/console cache:clear

# اجرای queue worker
ddev exec php bin/console messenger:consume async
```

---

## ۴. آدرس‌های مهم

| سرویس            | آدرس                                 |
| ---------------- | ------------------------------------ |
| **وب‌سایت**      | https://clinic-pro.ddev.site         |
| **پنل ادمین**    | https://clinic-pro.ddev.site/admin   |
| **Swagger UI**   | https://clinic-pro.ddev.site/api/doc |
| **Health Check** | https://clinic-pro.ddev.site/health  |

### ورود به Swagger UI

> آدرس: `https://clinic-pro.ddev.site/api/doc`

| فیلد           | مقدار       |
| -------------- | ----------- |
| **نام کاربری** | `admin`     |
| **رمز عبور**   | `clinic123` |

پس از ورود، برای احراز هویت API درون Swagger ابتدا از `/api/v1/user/login` توکن بگیرید، سپس دکمه **Authorize** را بزنید و `Bearer <token>` وارد کنید.

---

## ۵. متغیرهای محیطی

فایل `.env` (مقادیر پیش‌فرض توسعه):

```env
# ── برنامه ─────────────────────────────────────────────────────
APP_ENV=dev
APP_SECRET=clinic_pro_secret_change_in_prod
DEFAULT_URI=https://clinic-pro.ddev.site
APP_BASE_URL=https://clinic-pro.ddev.site

# ── پایگاه داده ──────────────────────────────────────────────────
DATABASE_URL="mysql://db:db@db:3306/db?serverVersion=8.0&charset=utf8mb4"

# ── JWT ─────────────────────────────────────────────────────────
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
JWT_PASSPHRASE=<رشته تصادفی>

# ── CORS ────────────────────────────────────────────────────────
CORS_ALLOW_ORIGIN='^https?://(clinic-pro\.ddev\.site|localhost)(:[0-9]+)?$'
ALLOWED_FRONTEND_HOSTS=clinic-pro.ddev.site,localhost
# پروداکشن چند-دامنه‌ای (فرانتِ هر شهر): مقدار این دو متغیر را با اسکریپت بساز —
#   ddev exec php docker/gen-cors-env.php
# دامنه‌ها از docker/frontend-domains.json خوانده می‌شوند؛ برای افزودن شهر، آن فایل را
# ویرایش و دوباره اجرا کن و خروجی را در env سرور بگذار.

# ── صف پیام و Cache ──────────────────────────────────────────────
MESSENGER_TRANSPORT_DSN=redis://redis:6379/messages
REDIS_URL=redis://redis:6379

# ── توکن‌ها ──────────────────────────────────────────────────────
REFRESH_TOKEN_TTL=2592000   # ۳۰ روز (ثانیه)
OTP_TTL=1200                # ۲۰ دقیقه (ثانیه)

# ── پیامک ────────────────────────────────────────────────────────
SMS_PROVIDER=kavenegar
KAVENEGAR_API_KEY=<کلید>
KAVENEGAR_SENDER=<شماره فرستنده>
RANGINEH_API_KEY=<کلید>
RANGINEH_SENDER=<شماره فرستنده>

# ── فایل آپلود ───────────────────────────────────────────────────
MAX_FILE_SIZE_BYTES=5242880   # 5MB
UPLOAD_DIR=var/uploads

# ── درگاه پرداخت ─────────────────────────────────────────────────
MELLAT_TERMINAL_ID=<شناسه>
MELLAT_USERNAME=<کاربری>
MELLAT_PASSWORD=<رمز>
SEP_TERMINAL_ID=<شناسه>

# ── Swagger UI ───────────────────────────────────────────────────
API_DOC_USERNAME=admin
API_DOC_PASSWORD=<bcrypt hash>   # plain text: admin@admin
```

---

## ۶. دستورات Console

```bash
# ── Cache ────────────────────────────────────────────────────────
ddev exec php bin/console cache:clear

# ── Database ─────────────────────────────────────────────────────
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
ddev exec php bin/console doctrine:migrations:diff --no-interaction   # بعد از تغییر entity
ddev exec php bin/console doctrine:migrations:status

# ── Debug ────────────────────────────────────────────────────────
ddev exec php bin/console debug:router | grep api
ddev exec php bin/console debug:container | grep -i service

# ── CORS چند-دامنه‌ای (تولید CORS_ALLOW_ORIGIN + ALLOWED_FRONTEND_HOSTS) ──
ddev exec php docker/gen-cors-env.php   # خروجی را در env سرور (Liara/Coolify) بگذار

# ── Queue ────────────────────────────────────────────────────────
ddev exec php bin/console messenger:consume async

# ── Test ─────────────────────────────────────────────────────────
ddev exec php bin/phpunit
ddev exec php bin/phpunit tests/SomeTest.php

# ── Static Analysis ──────────────────────────────────────────────
ddev exec php vendor/bin/phpstan analyse

# ── Frontend ─────────────────────────────────────────────────────
ddev exec yarn dev          # بیلد یکبار
ddev exec yarn watch        # حالت watch
ddev exec yarn build        # پروداکشن
ddev exec npx tsc --noEmit  # فقط type check
```

---

## ۷. امنیت و احراز هویت

### جریان احراز هویت

```
۱. POST /api/v1/user/send-code   → ارسال OTP به موبایل
۲. POST /api/v1/user/verify-code → تأیید OTP → دریافت uuid
۳. POST /oauth/token             → ارسال uuid → دریافت JWT + refresh token
۴. Authorization: Bearer <jwt>  → همراه هر درخواست محافظت‌شده
```

### جریان Login با رمز عبور

```
POST /api/v1/user/login
{ "mobile_number": "09...", "password": "..." }
→ { "access_token": "...", "refresh_token": "..." }
```

### نقش‌های کاربری

| نقش              | توضیح              | دسترسی                   |
| ---------------- | ------------------ | ------------------------ |
| `ROLE_USER`      | پایه — همه کاربران | نوبت‌گیری، پروفایل       |
| `ROLE_ADMIN`     | مدیر کل            | پنل ادمین کامل           |
| `ROLE_CLINIC`    | صاحب کلینیک        | مدیریت کلینیک خود        |
| `ROLE_DOCTOR`    | پزشک               | مدیریت نوبت‌ها، پروفایل  |
| `ROLE_SECRETARY` | منشی               | بر اساس permissions دکتر |

### دسترسی‌های منشی (JSON)

```json
{
    "version": 1,
    "resources": {
        "appointments": {
            "view": true,
            "create": true,
            "cancel": false,
            "update_status": true
        },
        "addresses": {
            "view": true,
            "create": false,
            "update": false,
            "delete": false
        },
        "clinic_info": { "view": true, "update": false },
        "insurances": {
            "view": true,
            "create": false,
            "update": false,
            "delete": false
        }
    }
}
```

### JWT

- مدت اعتبار access token: **۱ ساعت**
- Refresh token: **۳۰ روز** (ذخیره در Redis)
- پیلود: `{ username: mobile_number, roles: [...], iat, exp }`
- تجدید: `POST /oauth/token/refresh`

### Firewall های security.yaml

| Firewall           | Pattern                        | توضیح                               |
| ------------------ | ------------------------------ | ----------------------------------- |
| `api_doc`          | `^/api/doc`                    | HTTP Basic Auth (admin / clinic123) |
| `public_endpoints` | OTP، OAuth، endpoint های عمومی | بدون احراز هویت                     |
| `payment_callback` | `/api/v1/payment/callback/`    | بدون احراز هویت                     |
| `api`              | `^/(api\|oauth\|file/)`        | JWT stateless                       |

---

## ۸. پایگاه داده — جداول و موجودیت‌ها

### جداول اصلی

| جدول                        | موجودیت                | توضیح                                                                                |
| --------------------------- | ---------------------- | ------------------------------------------------------------------------------------ |
| `users`                     | User                   | uuid، mobile_number (unique)، roles (JSON)، status                                   |
| `doctors`                   | Doctor                 | user_id (OneToOne)، degree، doctor_rate، active_doctor_appointment                   |
| `doctor_addresses`          | DoctorAddress          | doctor_id، name، latitude، longitude                                                 |
| `clinics`                   | Clinic                 | user_id (owner)، name، telephone، is_24_7، latitude، longitude، images_clinic (JSON) |
| `clinic_doctor_invitations` | ClinicDoctorInvitation | token (unique 96char hex)، status، expires_at (+72h)، mobile، uuid                   |
| `appointments`              | Appointment            | doctor_id، user_id، slot_start، slot_end، status، price                              |
| `weekly_schedules`          | WeeklySchedule         | doctor_id (OneToOne)، setting (JSON)                                                 |
| `date_overrides`            | DateOverride           | doctor_id، date، active، setting (JSON)                                              |
| `holidays`                  | Holiday                | doctor_id، start_date، end_date                                                      |
| `payments`                  | Payment                | order_id (unique)، amount_rials، status، gateway (mellat\|sep)                       |
| `settlements`               | Settlement             | user_id، amount_rials، status، bank_account (JSON)                                   |
| `wallet_transactions`       | WalletTransaction      | user_id، amount_rials، type (credit\|debit)، balance_after                           |
| `representations`           | Representation         | full_name، commission_percent، city_id                                               |
| `doctor_secretaries`        | DoctorSecretary        | doctor_id، secretary_id، permissions (JSON)، active                                  |
| `rates`                     | Rate                   | doctor_id، user_id، overall، diagnosis_accuracy، skill، behavior                     |
| `comments`                  | Comment                | doctor_id، user_id، body، status (approved\|pending\|rejected)                       |
| `likes`                     | Like                   | comment_id، user_id                                                                  |
| `blogs`                     | Blog                   | author_id، title، slug (unique)، body، status                                        |
| `sms_templates`             | SmsTemplate            | name، body، provider_code، status                                                    |
| `sms_logs`                  | SmsLog                 | mobile، message، provider، success                                                   |
| `profiles`                  | UserProfile            | blood_type، gender، insurance_id                                                     |
| `provinces`                 | Province               | name                                                                                 |
| `cities`                    | City                   | name، province_id (FK)                                                               |
| `specialties`               | Specialty              | name، parent_id (nullable برای زیر-تخصص)                                             |
| `doctor_services`           | DoctorService          | name                                                                                 |
| `insurances`                | Insurance              | name، type (basic\|supplementary)                                                    |
| `tags`                      | Tag                    | name                                                                                 |

### جداول رابطه‌ای (ManyToMany)

| جدول                 | رابطه                  |
| -------------------- | ---------------------- |
| `clinic_doctors`     | Clinic ↔ Doctor        |
| `clinic_specialties` | Clinic ↔ Specialty     |
| `clinic_services`    | Clinic ↔ DoctorService |
| `clinic_insurances`  | Clinic ↔ Insurance     |
| `doctor_specialties` | Doctor ↔ Specialty     |
| `doctor_expertise`   | Doctor ↔ Category      |
| `doctor_provinces`   | Doctor ↔ Province      |

### وابستگی‌های کلیدی

```
User ──OneToOne──► Doctor ──ManyToMany──► Specialty
     ──OneToOne──► Representation
     ──OneToMany──► Appointment
     ──OneToMany──► Clinic (owner)

Doctor ──OneToMany──► DoctorAddress
       ──OneToMany──► Appointment
       ──ManyToMany──► Clinic

Clinic ──ManyToOne──► User (owner)
       ──ManyToMany──► Doctor
       ──OneToMany──► ClinicDoctorInvitation
```

### نکته timestamp

همه فیلدهای تاریخ (createdAt، updatedAt، slotStart، ...) به صورت **integer Unix timestamp** ذخیره می‌شوند — نه DateTime object.

---

## ۹. API Endpoints

### احراز هویت

| متد  | آدرس                       | توضیح              | دسترسی     |
| ---- | -------------------------- | ------------------ | ---------- |
| POST | `/api/v1/user/send-code`   | ارسال OTP          | عمومی      |
| POST | `/api/v1/user/verify-code` | تأیید OTP          | عمومی      |
| POST | `/api/v1/user/register`    | ثبت‌نام            | عمومی      |
| POST | `/api/v1/user/login`       | لاگین با رمز عبور  | عمومی      |
| POST | `/oauth/token`             | دریافت JWT         | عمومی      |
| POST | `/oauth/token/refresh`     | تجدید توکن         | عمومی      |
| GET  | `/oauth/userinfo`          | اطلاعات کاربر جاری | احراز هویت |
| POST | `/oauth/logout`            | خروج               | احراز هویت |

### پزشکان

| متد    | آدرس                                                         | توضیح           | دسترسی     |
| ------ | ------------------------------------------------------------ | --------------- | ---------- |
| GET    | `/api/v1/doctors`                                            | لیست پزشکان     | عمومی      |
| GET    | `/api/v1/doctor/{uuid}`                                      | جزئیات پزشک     | عمومی      |
| POST   | `/api/v1/doctor`                                             | ایجاد پروفایل   | احراز هویت |
| PATCH  | `/api/v1/doctor/{uuid}`                                      | ویرایش          | احراز هویت |
| DELETE | `/api/v1/doctor/{uuid}`                                      | حذف             | احراز هویت |
| POST   | `/file/upload/clinic_pro/doctor/field_image`                 | آپلود تصویر     | احراز هویت |
| GET    | `/api/v1/clinic-pro/doctor-addresses/{doctorId}`             | آدرس‌های مطب    | عمومی      |
| POST   | `/api/v1/clinic-pro/doctor-address`                          | افزودن آدرس     | احراز هویت |
| PATCH  | `/api/v1/clinic-pro/doctor-address/{id}`                     | ویرایش آدرس     | احراز هویت |
| DELETE | `/api/v1/clinic-pro/doctor-address/{id}`                     | حذف آدرس        | احراز هویت |
| POST   | `/api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}` | ایجاد از کلینیک | احراز هویت |

### کلینیک‌ها

| متد   | آدرس                                                | توضیح          | دسترسی     |
| ----- | --------------------------------------------------- | -------------- | ---------- |
| GET   | `/api/v1/clinics`                                   | لیست کلینیک‌ها | عمومی      |
| GET   | `/api/v1/clinic/{uuid}`                             | جزئیات کلینیک  | عمومی      |
| POST  | `/api/v1/clinic`                                    | ایجاد کلینیک   | احراز هویت |
| PATCH | `/api/v1/clinic/{uuid}`                             | ویرایش         | احراز هویت |
| GET   | `/api/v1/clinic/doctor-list/{clinicUuid}`           | پزشکان کلینیک  | عمومی      |
| POST  | `/file/upload/clinic_pro/clinic/field_clinic_logo`  | آپلود لوگو     | احراز هویت |
| POST  | `/file/upload/clinic_pro/clinic/field_image_clinic` | آپلود گالری    | احراز هویت |

### دعوتنامه پزشک به کلینیک

| متد    | آدرس                                               | توضیح             | دسترسی     |
| ------ | -------------------------------------------------- | ----------------- | ---------- |
| POST   | `/api/v1/admin/clinic/{uuid}/invite-doctor`        | ارسال دعوتنامه    | ROLE_ADMIN |
| GET    | `/api/v1/admin/clinic/{uuid}/invitations`          | لیست دعوتنامه‌ها  | ROLE_ADMIN |
| POST   | `/api/v1/admin/clinic/invitation/{invUuid}/resend` | ارسال مجدد پیامک  | ROLE_ADMIN |
| PATCH  | `/api/v1/admin/clinic/invitation/{invUuid}/status` | تغییر وضعیت       | ROLE_ADMIN |
| DELETE | `/api/v1/admin/clinic/invitation/{invUuid}`        | حذف               | ROLE_ADMIN |
| GET    | `/api/v1/clinic-invitation/{token}`                | مشاهده دعوتنامه   | عمومی      |
| POST   | `/api/v1/clinic-invitation/{token}/accept`         | پذیرش توسط دکتر   | عمومی      |
| POST   | `/api/v1/clinic-invitation/{token}/reject`         | رد کردن توسط دکتر | عمومی      |

> توکن ۹۶ کاراکتر hex (`bin2hex(random_bytes(48))`) — مدت اعتبار ۷۲ ساعت. پیامک async از طریق Symfony Messenger ارسال می‌شود.

### نوبت‌دهی

| متد   | آدرس                                       | توضیح          | دسترسی     |
| ----- | ------------------------------------------ | -------------- | ---------- |
| GET   | `/api/v1/appointment-slots`                | اسلات‌های خالی | عمومی      |
| POST  | `/api/v1/appointment`                      | رزرو نوبت      | احراز هویت |
| GET   | `/api/v1/appointment/{uuid}`               | جزئیات نوبت    | احراز هویت |
| PATCH | `/api/v1/appointment/{uuid}/status`        | تغییر وضعیت    | احراز هویت |
| GET   | `/api/v1/appointments/doctor/{doctorUuid}` | نوبت‌های دکتر  | احراز هویت |
| GET   | `/api/v1/appointments/user`                | نوبت‌های کاربر | احراز هویت |

### تنظیمات نوبت‌دهی

| متد                   | آدرس                                           | توضیح        |
| --------------------- | ---------------------------------------------- | ------------ |
| POST/PATCH/GET        | `/api/v1/appointment-settings/weekly-schedule` | برنامه هفتگی |
| POST/PATCH/DELETE/GET | `/api/v1/appointment-settings/date-override`   | روز استثناء  |
| POST/PATCH/DELETE/GET | `/api/v1/appointment-settings/holidays`        | تعطیلات      |

### پرداخت

| متد      | آدرس                                 | توضیح                  | دسترسی     |
| -------- | ------------------------------------ | ---------------------- | ---------- |
| POST     | `/api/v1/payment/appointment`        | شروع پرداخت نوبت       | احراز هویت |
| POST/GET | `/api/v1/payment/callback/{gateway}` | callback (mellat\|sep) | عمومی      |
| GET      | `/api/v1/payment/{uuid}`             | وضعیت پرداخت           | احراز هویت |
| POST     | `/api/v1/subscription-payment`       | پرداخت اشتراک          | احراز هویت |

### کیف پول و تسویه‌حساب

| متد  | آدرس                                | توضیح               | دسترسی     |
| ---- | ----------------------------------- | ------------------- | ---------- |
| GET  | `/api/v1/wallet/balance`            | موجودی کیف پول      | احراز هویت |
| GET  | `/api/v1/wallet/transactions`       | تاریخچه تراکنش‌ها   | احراز هویت |
| POST | `/api/v1/settlement`                | درخواست تسویه       | احراز هویت |
| GET  | `/api/v1/settlement`                | لیست تسویه‌های خودم | احراز هویت |
| POST | `/api/v1/settlement/{uuid}/approve` | تأیید               | ROLE_ADMIN |
| POST | `/api/v1/settlement/{uuid}/reject`  | رد                  | ROLE_ADMIN |

### امتیاز و نظرات

| متد    | آدرس                            | توضیح          | دسترسی     |
| ------ | ------------------------------- | -------------- | ---------- |
| POST   | `/api/v1/rate`                  | ثبت امتیاز     | احراز هویت |
| GET    | `/api/v1/rate/{doctorUuid}`     | میانگین امتیاز | عمومی      |
| POST   | `/api/v1/comment`               | ثبت نظر        | احراز هویت |
| GET    | `/api/v1/comments/{doctorUuid}` | نظرات تأییدشده | عمومی      |
| DELETE | `/api/v1/comment/{uuid}`        | حذف نظر        | احراز هویت |
| POST   | `/api/v1/like/{commentUuid}`    | لایک           | احراز هویت |

### منشی

| متد    | آدرس                               | توضیح              | دسترسی      |
| ------ | ---------------------------------- | ------------------ | ----------- |
| POST   | `/api/v1/secretary`                | ایجاد منشی         | ROLE_DOCTOR |
| GET    | `/api/v1/secretary/{uuid}`         | جزئیات             | احراز هویت  |
| PATCH  | `/api/v1/secretary/{uuid}`         | ویرایش permissions | ROLE_DOCTOR |
| DELETE | `/api/v1/secretary/{uuid}`         | حذف                | ROLE_DOCTOR |
| GET    | `/api/v1/secretaries/{doctorUuid}` | لیست منشی‌های دکتر | احراز هویت  |

### نمایندگان

| متد                   | آدرس                                              | توضیح          | دسترسی     |
| --------------------- | ------------------------------------------------- | -------------- | ---------- |
| POST/GET/PATCH/DELETE | `/api/v1/representation/{uuid?}`                  | مدیریت نماینده | احراز هویت |
| GET                   | `/api/v1/representation/{uuid}/dashboard/monthly` | آمار ماهانه    | احراز هویت |
| GET                   | `/api/v1/representation/{uuid}/dashboard/yearly`  | آمار سالانه    | احراز هویت |

### پیامک

| متد                   | آدرس                                        | توضیح            | دسترسی     |
| --------------------- | ------------------------------------------- | ---------------- | ---------- |
| POST                  | `/api/v1/sms/send`                          | ارسال مستقیم     | احراز هویت |
| POST                  | `/api/v1/sms/send-template`                 | ارسال با قالب    | احراز هویت |
| POST/PATCH/GET/DELETE | `/api/v1/sms/template/{uuid?}`              | مدیریت قالب      | احراز هویت |
| POST                  | `/api/v1/sms/template/{uuid}/submit`        | ارسال برای تأیید | احراز هویت |
| POST                  | `/api/v1/admin/sms/template/{uuid}/approve` | تأیید قالب       | ROLE_ADMIN |

### تخصص، خدمات، بیمه، تگ، موقعیت

```
GET  /api/v1/specialties              عمومی
GET  /api/v1/doctor-services          عمومی
GET  /api/v1/insurances               عمومی
GET  /api/v1/tags                     عمومی
GET  /api/v1/provinces                عمومی
GET  /api/v1/cities?province_id=      عمومی
GET  /api/v1/categorys/{bundle}       عمومی (legacy — bundle: state|city|specially_doctor|doctor_services|insurance_type|supplementary_insurance|tag)

POST/PATCH/DELETE /api/v1/admin/specialty/{id?}        ROLE_ADMIN
POST/PATCH/DELETE /api/v1/admin/doctor-service/{id?}   ROLE_ADMIN
POST/PATCH/DELETE /api/v1/admin/insurance/{id?}        ROLE_ADMIN
POST/PATCH/DELETE /api/v1/admin/tag/{id?}              ROLE_ADMIN
POST/PATCH/DELETE /api/v1/admin/province/{id?}         ROLE_ADMIN
POST/PATCH/DELETE /api/v1/admin/city/{id?}             ROLE_ADMIN
```

### بلاگ

| متد    | آدرس                                       | توضیح                  | دسترسی     |
| ------ | ------------------------------------------ | ---------------------- | ---------- |
| GET    | `/api/v1/blogs`                            | لیست بلاگ‌های منتشرشده | عمومی      |
| GET    | `/api/v1/blog/{slug}`                      | جزئیات بلاگ            | عمومی      |
| POST   | `/api/v1/blog`                             | نوشتن بلاگ             | احراز هویت |
| PATCH  | `/api/v1/blog/{uuid}`                      | ویرایش                 | احراز هویت |
| DELETE | `/api/v1/blog/{uuid}`                      | حذف                    | احراز هویت |
| POST   | `/file/upload/clinic_pro/blog/field_image` | آپلود تصویر            | احراز هویت |

### پنل ادمین API

> همه endpoint های زیر نیاز به `ROLE_ADMIN` دارند.

| آدرس                                       | توضیح                                      |
| ------------------------------------------ | ------------------------------------------ |
| `GET /api/v1/admin/dashboard/stats`        | ۱۲ KPI (کاربران، پزشکان، نوبت، درآمد، ...) |
| `GET /api/v1/admin/dashboard/charts`       | نمودار ۳۰ روزه نوبت و درآمد                |
| `GET /api/v1/admin/dashboard/recent`       | آخرین نوبت‌ها، پرداخت‌ها، کاربران          |
| `GET /api/v1/admin/users`                  | لیست کاربران (paginated)                   |
| `GET /api/v1/admin/users/{uuid}`           | جزئیات کاربر                               |
| `PUT /api/v1/admin/users/{uuid}/role`      | تغییر نقش                                  |
| `POST /api/v1/admin/users/{uuid}/status`   | فعال/غیرفعال                               |
| `DELETE /api/v1/admin/users/{uuid}`        | حذف کاربر                                  |
| `GET /api/v1/admin/doctors`                | لیست پزشکان                                |
| `GET /api/v1/admin/clinics`                | لیست کلینیک‌ها                             |
| `PATCH /api/v1/admin/clinic/{uuid}/status` | تغییر وضعیت کلینیک                         |
| `DELETE /api/v1/admin/clinic/{uuid}`       | حذف کلینیک                                 |
| `GET /api/v1/admin/appointments`           | لیست نوبت‌ها                               |
| `GET /api/v1/admin/payments`               | لیست پرداخت‌ها                             |
| `GET /api/v1/admin/settlements`            | لیست تسویه‌ها                              |
| `GET /api/v1/admin/representations`        | لیست نمایندگان                             |
| `GET /api/v1/admin/secretaries`            | لیست منشی‌ها                               |
| `GET /api/v1/admin/rates`                  | لیست امتیازها                              |
| `GET /api/v1/admin/comments`               | لیست نظرات                                 |
| `GET /api/v1/admin/sms/logs`               | لاگ پیامک‌ها                               |
| `GET /api/v1/admin/sms/templates`          | قالب‌های پیامک                             |

### فرمت پاسخ‌های API

```json
// Single resource
{ "success": true, "data": { ... } }

// Paginated list
{ "success": true, "data": [...], "meta": { "totalRecords": N, "totalPages": N, "currentPage": N } }

// Error
{ "success": false, "data": null, "errors": [{ "code": "ERR_...", "message": "..." }] }
```

**نکته double-nested:** برخی endpoint ها پاسخ را داخل `data` اضافه می‌کنند:  
`$this->success(['data' => $entity->toArray()])` → فرانت باید با `data?.data?.data` استخراج کند.

---

## ۱۰. پنل ادمین React

### Route های پنل

| مسیر                           | صفحه                     | توضیح                                |
| ------------------------------ | ------------------------ | ------------------------------------ |
| `/admin/login`                 | LoginPage                | ورود با موبایل + رمز                 |
| `/admin/dashboard`             | DashboardPage            | آمار KPI، نمودار، فعالیت‌های اخیر    |
| `/admin/users`                 | UsersPage                | لیست + جستجو + فیلتر نقش             |
| `/admin/users/:uuid`           | UserDetailPage           | پروفایل + ویرایش نقش                 |
| `/admin/doctors`               | DoctorsPage              | لیست پزشکان                          |
| `/admin/doctors/new`           | DoctorFormPage           | ایجاد پزشک جدید                      |
| `/admin/doctors/:uuid`         | DoctorDetailPage         | پروفایل کامل + تخصص + نوبت‌ها        |
| `/admin/clinics`               | ClinicsPage              | لیست کلینیک‌ها                       |
| `/admin/clinics/:uuid`         | ClinicDetailPage         | پروفایل + نقشه + گالری + دعوتنامه‌ها |
| `/admin/appointments`          | AppointmentsPage         | لیست نوبت‌ها + فیلتر                 |
| `/admin/appointments/:uuid`    | AppointmentDetailPage    | جزئیات نوبت                          |
| `/admin/payments`              | PaymentsPage             | لیست پرداخت‌ها                       |
| `/admin/settlements`           | SettlementsPage          | تسویه‌حساب                           |
| `/admin/representations`       | RepresentationsPage      | نمایندگان                            |
| `/admin/representations/:uuid` | RepresentationDetailPage | جزئیات نماینده                       |
| `/admin/comments`              | CommentsPage             | مدیریت نظرات                         |
| `/admin/ratings`               | RatingsPage              | امتیازها                             |
| `/admin/sms`                   | SmsPage                  | مدیریت پیامک                         |
| `/admin/categories`            | CategoriesPage           | تخصص/خدمات/بیمه/تگ                   |
| `/admin/blogs`                 | BlogsPage                | لیست بلاگ‌ها                         |
| `/admin/blogs/new`             | BlogFormPage             | نوشتن بلاگ جدید                      |
| `/admin/secretaries`           | SecretariesPage          | مدیریت منشی‌ها                       |

### الگوی دریافت داده

```typescript
// لیست paginated
const { data } = useQuery({
    queryKey: ["resource", page, filters],
    queryFn: () =>
        api.get<PaginatedResponse<T>>(`/api/v1/admin/...?page=${page}`),
});
const items = data?.data ?? []; // آرایه آیتم‌ها
const total = data?.meta?.totalRecords ?? 0;

// Single resource (double-nested — برخی endpoint ها)
const raw = data?.data;
const item = (raw as any)?.data ?? raw;

// Category API (triple-nested legacy)
const cats = data?.data?.data ?? [];
```

### ویژگی‌های فنی فرانت‌اند

- **Auth Store** (Zustand persist): توکن در `localStorage['clinicpro-auth']` → `state.token`
- **Modal ها**: از `createPortal(modal, document.body)` استفاده می‌کنند تا از z-index Leaflet بگریزند
- **CSS**: کلاس‌های template اختصاصی — `.card`، `.btn.primary/.ghost/.soft/.sm`، `.badge.green/.gray/.blue`، `.overlay`، `.seg`، `.skeleton`، `.empty`
- **Avatar**: OKLCH gradient با `HUES_LIST = [256, 205, 162, 295, 272]`

---

## ۱۱. Migrations

```bash
# ایجاد migration جدید بعد از تغییر entity
ddev exec php bin/console doctrine:migrations:diff --no-interaction

# اجرا
ddev exec php bin/console doctrine:migrations:migrate --no-interaction

# وضعیت
ddev exec php bin/console doctrine:migrations:status
```

### لیست migration های موجود

| فایل                      | توضیح                                          |
| ------------------------- | ---------------------------------------------- |
| Version20260609130407     | جداول پایه: users, doctors, appointments       |
| Version20260609131304     | پرداخت، کیف پول                                |
| Version20260609131553     | امتیاز، نظرات                                  |
| Version20260609132009     | نمایندگان                                      |
| Version20260609132708     | منشی                                           |
| Version20260609133121     | کلینیک                                         |
| Version20260609133334     | پیامک                                          |
| Version20260609133546     | بلاگ                                           |
| Version20260609134112     | تخصص، بیمه، تگ                                 |
| Version20260609134741     | استان، شهر                                     |
| Version20260609135126     | پروفایل، آدرس مطب                              |
| Version20260609135514     | برنامه هفتگی                                   |
| Version20260609135704     | روز استثناء، تعطیلات                           |
| Version20260609135923     | ManyToMany: clinic_doctors, clinic_specialties |
| Version20260609140223     | ایندکس‌های اضافی                               |
| Version20260609140423     | doctor_insurances                              |
| Version20260610062539     | فیلدهای اضافی clinic                           |
| Version20260610175105     | فیلد is_active برای clinic                     |
| **Version20260610183655** | **جدول clinic_doctor_invitations**             |

---

## ۱۲. سرویس‌های خارجی

### پیامک

**KaveNegar** (پیش‌فرض)

- API: `https://api.kavenegar.com/v1/{KEY}/sms/send.json`
- پشتیبانی از: send (متن آزاد) + sendTemplate (قالب)

**Rangineh**

- API: `https://rest.payamresan.com/api/v1/send`
- پشتیبانی از: send + sendTemplate

**ارسال async:**

```php
$this->smsService->dispatchAsync($mobile, $message);
// → Symfony Messenger → Redis → SendSmsHandler
```

### درگاه پرداخت

**بانک ملت (Mellat)**

- SOAP WebService: `https://bpm.shaparak.ir/pgwchannel/services/pgw?wsdl`
- تأیید: ResCode=0 + SettlePayment

**سپ (SEP)**

- REST API با ترمینال ID

### Redis

- صف پیام: `redis://redis:6379/messages`
- Cache: `redis://redis:6379`
- Refresh Token: کلید `refresh_<sha256>`

---

## ۱۳. ساختار دایرکتوری

```
clinic-pro-symfony/
├── assets/
│   └── admin/                    # React SPA (پنل ادمین)
│       ├── App.tsx               # React Router routes
│       ├── pages/                # یک فایل به ازای هر صفحه
│       ├── components/
│       │   ├── layout/           # AdminLayout, Sidebar, Topbar
│       │   └── ui/               # DataTable, Modal, ConfirmDialog, Pagination, ...
│       ├── lib/
│       │   ├── api.ts            # fetch wrapper (JWT از localStorage)
│       │   └── utils.ts          # formatRial, formatDate, formatDateTime, formatNumber
│       ├── stores/
│       │   ├── authStore.ts      # Zustand — token, isAuthenticated
│       │   └── uiStore.ts        # sidebar state
│       ├── types/index.ts        # TypeScript interfaces
│       └── styles.css            # کلاس‌های template
├── config/
│   ├── packages/
│   │   ├── security.yaml         # firewalls, access_control
│   │   └── lexik_jwt_authentication.yaml
│   └── services.yaml             # dependency injection
├── docs/
│   └── tasks/                    # مستندات فنی هر feature
├── migrations/                   # Doctrine migrations
├── src/
│   ├── Admin/Controller/         # AdminApiController — همه endpoint های ادمین
│   ├── Appointment/              # نوبت‌دهی، زمان‌بندی، SlotCalculatorService
│   ├── Auth/                     # احراز هویت، JWT، OTP، User entity
│   ├── Blog/                     # بلاگ
│   ├── Category/                 # کتگوری legacy (bundle system)
│   ├── Clinic/                   # کلینیک
│   ├── ClinicInvitation/         # دعوتنامه پزشک به کلینیک
│   ├── Doctor/                   # پزشک، آدرس مطب
│   ├── DoctorService/            # خدمات پزشکی
│   ├── Insurance/                # بیمه
│   ├── Location/                 # استان، شهر
│   ├── Payment/                  # پرداخت، درگاه‌ها (Mellat, Sep)
│   ├── Rating/                   # امتیاز، نظرات، لایک
│   ├── Representation/           # نمایندگان
│   ├── Secretary/                # منشی
│   ├── Settlement/               # تسویه‌حساب، کیف پول
│   ├── Sms/                      # پیامک، قالب، لاگ
│   ├── Specialty/                # تخصص پزشکی
│   ├── Tag/                      # تگ بلاگ
│   ├── UserProfile/              # پروفایل پزشکی کاربر
│   └── Shared/
│       ├── Controller/BaseController.php   # success() / paginated() / error()
│       ├── Exception/AppException.php      # domain exception با httpStatus
│       ├── Constant/ErrorCodes.php         # کدهای خطا به صورت const string
│       └── EventSubscriber/ExceptionSubscriber.php
├── public/
│   └── build/                    # webpack output (gitignore شده)
├── var/
│   └── uploads/                  # فایل‌های آپلود‌شده
├── .ddev/config.yaml             # PHP 8.3، MariaDB 11.8، nginx-fpm
├── .env                          # متغیرهای پیش‌فرض (commit نشود .env.local)
└── webpack.config.js             # Webpack Encore
```

---

> **یادآور توسعه:** تمام دستورات را با پیشوند `ddev exec` اجرا کنید.  
> CSS فرانت‌اند از کلاس‌های template اختصاصی استفاده می‌کند (نه Tailwind).  
> بعد از هر تغییر backend: `cache:clear`  
> بعد از هر تغییر entity: `migrations:diff` سپس `migrations:migrate`

---

## 🛡️ ALTCHA — کپچای خودمیزبان (ضدِ اسپم/بات)

کپچای Proof-of-Work مبتنی بر [ALTCHA](https://altcha.org) — بدون تصویر، بدون تایپ، بدون سرویس خارجی (مناسب کاربران داخل ایران). فقط روی endpointهای **عمومی و بدون احراز هویت** اعمال می‌شود.

### نصب (انجام‌شده)
- بک‌اند: `composer require altcha-org/altcha` (پکیج رسمی PHP، از V1 API استفاده می‌شود).
- فرانت‌اند: `yarn add altcha` (web-component محلی، بدون CDN؛ Encore آن را bundle می‌کند).

### تنظیمات (`.env` / برای prod در `.env.local`)
| متغیر | پیش‌فرض | توضیح |
|---|---|---|
| `ALTCHA_ENABLED` | `false` | در prod روی `true`؛ در dev/test غیرفعال بماند |
| `ALTCHA_HMAC_KEY` | `change-me-in-env-local` | کلید امضای سرور — **حتماً در prod عوض شود** و مخفی بماند |
| `ALTCHA_MAX_NUMBER` | `100000` | سقف اعداد PoW = **سختی**؛ بالاتر = سنگین‌تر برای مرورگر |
| `ALTCHA_EXPIRE_SECONDS` | `300` | عمر هر challenge (ثانیه) |

### جریان
1. کلاینت `GET /api/v1/altcha/challenge` را می‌گیرد (challenge امضاشده).
2. `<altcha-widget>` در پس‌زمینه PoW را حل می‌کند.
3. مقدار حل‌شده (base64) با کلید `altcha` در بدنه‌ی درخواستِ endpoint عمومی ارسال می‌شود.
4. سرور با `CaptchaGuard::assertValid($request)` اعتبارسنجی می‌کند (امضا + انقضا + یک‌بارمصرف بودن).

### endpointهای محافظت‌شده
`send-code`، `register`، `otp-login`، `reset-password`، `pre-registration`.
> `rate`/`comment` پشت JWT هستند و کپچا نمی‌گیرند (بی‌فایده است).

### افزودن کپچا به endpoint عمومی جدید
`CaptchaGuard` را inject کن و اولین خط handler:
```php
$this->captcha->assertValid($request); // پرتاب ERR_CAPTCHA_001 (422) در صورت شکست
```
سپس مسیر را در فرانت به بدنه‌ی درخواست `altcha` وصل کن.

### تغییر سختی
`ALTCHA_MAX_NUMBER` را بالا/پایین ببر (مثلاً `1000000` برای سخت‌تر).

### غیرفعال‌سازی
`ALTCHA_ENABLED=false` → guard کاملاً no-op می‌شود (dev/test همیشه این‌طور است).

### Troubleshooting
- **همیشه ERR_CAPTCHA_001:** کلید `ALTCHA_HMAC_KEY` بین challenge و verify باید یکسان باشد؛ اگر بعد از صدور challenge کلید عوض شود، امضا نامعتبر می‌شود.
- **challenge منقضی:** ساعت سرور را چک کن؛ `ALTCHA_EXPIRE_SECONDS` خیلی کوتاه نباشد.
- **replay:** هر challenge یک‌بار مصرف است؛ برای هر submit یک challenge تازه بگیر.
- **خطای Redis:** pool اختصاصی `altcha.pool` روی `REDIS_URL` است؛ در دسترس بودن Redis لازم است.
