# سناریو: ایمپورت پزشکان سازمان نظام پزشکی و مدیریت مالکیت پروفایل

> نسخه: ۱.۰ — تاریخ: ۱۴۰۵/۰۴/۱۹ (۲۰۲۶-۰۷-۱۰)
> دامنه: `clinicpro` (بک‌اند + پنل ادمین) · `nobat724_front` (سایت عمومی) · `clinic-pro-tauri` (اپ دسکتاپ)
> وضعیت: پیش‌نویس طراحی برای پیاده‌سازی

---

## ۱. خلاصه اجرایی

یک دیتاست ۱۶۰ نفره از پزشکان از سامانه استعلام اعضای سازمان نظام پزشکی (`membersearch.irimc.org`) استخراج شده است. هدف، وارد کردن این پزشکان به Clinic Pro است تا در سایت عمومی Nobat724 نمایش داده شوند، **پیش از آنکه پزشک واقعی در سیستم ثبت‌نام کرده باشد**.

مشکل محوری: در مدل داده‌ی فعلی، هر پزشک (`Doctor`) به‌صورت اجباری و **یک‌به‌یک و یکتا** به یک کاربر (`User`) متصل است، و هر کاربر نیز الزاماً یک **شماره موبایل یکتا و غیرتهی** دارد. اما رکوردهای سازمان نظام پزشکی فاقد شماره موبایل هستند (`mobileNumber: null`). بنابراین نه می‌توان کاربر ساخت (چون موبایل لازم است) و نه می‌توان یک پزشک را بدون کاربر ذخیره کرد.

این سند یک مدل «مالکیت پروفایل» (Profile Ownership) طراحی می‌کند که در آن پزشکان ایمپورت‌شده در حالت **«بدون‌مالک» (unclaimed)** ذخیره و مدیریت می‌شوند، و بعداً از طریق یک فرایند احراز هویت‌شده در Nobat724 به پزشک واقعی **منتقل (claim/transfer)** می‌شوند.

---

## ۲. مسئله و محدودیت‌های سیستم فعلی

پیش از طراحی راه‌حل، محدودیت‌های واقعی کد فعلی مستند می‌شوند (منبع: `src/Doctor/Entity/Doctor.php`, `src/Auth/Entity/User.php`, `src/Doctor/Controller/DoctorController.php`).

| محدودیت | جزئیات کد فعلی | پیامد برای ایمپورت |
|---|---|---|
| کاربر برای پزشک اجباری است | `Doctor::$user` → `OneToOne`، `JoinColumn(nullable: false, onDelete: RESTRICT)` | نمی‌توان پزشک بدون کاربر ذخیره کرد. |
| رابطه پزشک↔کاربر یکتاست | `UniqueConstraint idx_doctors_user (user_id)` | **نمی‌توان چند پزشک را به یک کاربر مشترک وصل کرد** — ایده‌ی «همه به یک کاربر سیستمی» با این قید نقض می‌شود. |
| موبایل کاربر اجباری و یکتاست | `User::$mobileNumber` → `NOT NULL`، `UniqueConstraint uniq_mobile` | بدون موبایل نمی‌توان `User` ساخت؛ داده‌ی نظام پزشکی موبایل ندارد. |
| ساخت پزشک به کاربر لاگین‌شده گره خورده | `DoctorController::create()` از `#[CurrentUser] User $user` استفاده می‌کند و اگر همان کاربر پزشک داشته باشد خطای ۴۰۹ می‌دهد | مسیر فعلی ساخت پزشک برای ایمپورت انبوه مناسب نیست. |
| `medical_system_code` یکتا نیست | `Doctor::$medicalSystemCode` → `nullable`, بدون `unique` | برای جلوگیری از ایمپورت تکراری و برای تطبیق هنگام claim، باید کلید طبیعی یکتا شود. |

### نتیجه‌گیری کلیدی طراحی

خواسته‌ی اولیه («همه‌ی پزشکان ایمپورت‌شده به یک کاربر سیستمی اختصاص یابند») به‌دلیل قید یکتای `user_id` روی جدول `doctors` **مستقیماً قابل اجرا نیست**. بنابراین یکی از دو مسیر زیر لازم است، و این سند **گزینه A** را توصیه می‌کند:

- **گزینه A (توصیه‌شده): جداسازی «مالکیت» از «کاربر».** ستون `Doctor.user` اختیاری (`nullable`) می‌شود. یک کاربر سیستمی به‌نام «مالک سیستمی» (System Owner) صرفاً به‌عنوان **مدیرِ منطقیِ** پزشکان بدون‌مالک عمل می‌کند (نه از طریق ستون `user_id`، بلکه از طریق فیلد جدید `managed_by`). این کار قید یکتا را نقض نمی‌کند و مدل تمیزتری می‌سازد.
- **گزینه B (جایگزین کم‌تغییر): کاربر جانشین (Placeholder User) به‌ازای هر پزشک.** برای هر پزشک یک `User` غیرفعال با شناسه‌ی مصنوعی (مثلاً موبایل رزروشده‌ی `IRIMC-<code>`) ساخته می‌شود. اسکیمای `doctors` تقریباً دست‌نخورده می‌ماند اما جدول `users` با ۱۶۰ کاربر جعلی شلوغ می‌شود و هنگام claim باید ادغام (merge) انجام شود.

مقایسه و تصمیم نهایی در بخش ۱۳ آمده است.

---

## ۳. مدل مفهومی مالکیت (Ownership Model)

هر پروفایل پزشک یکی از این وضعیت‌های مالکیت را دارد:

- **`unclaimed` (بدون‌مالک):** ایمپورت‌شده از نظام پزشکی، هنوز به پزشک واقعی وصل نشده. توسط «مالک سیستمی» مدیریت می‌شود. در Nobat724 نمایش داده می‌شود اما قابل ویرایش توسط عموم نیست و نوبت‌دهی آنلاین آن پیش‌فرض **غیرفعال** است.
- **`pending_transfer` (در انتظار انتقال):** پزشک واقعی درخواست تصاحب داده و در حال احراز هویت / انتظار تأیید ادمین است.
- **`claimed` (تصاحب‌شده):** مالکیت به پزشک واقعی منتقل شده؛ پروفایل به کاربر واقعی او متصل است و او کنترل کامل دارد.

منبع پروفایل نیز ثبت می‌شود:

- **`source`**: `irimc` (نظام پزشکی) یا `manual` (ساخت دستی/ثبت‌نام عادی — رفتار فعلی).
- **`source_ref`**: شناسه‌ی یکتای رکورد مبدأ (`profile_url` id یا `medicalSystemCode`) برای idempotency و ممیزی.

---

## ۴. بخش اول — ایمپورت پزشکان نظام پزشکی

### ۴.۱ کاربر «مالک سیستمی»

یک کاربر ویژه یک‌بار ساخته می‌شود (از طریق دستور کنسول، هم‌سبک `CreateAdminCommand`):

- موبایل رزروشده و ثابت، مثلاً `0000000000` (خارج از فضای شماره‌های واقعی ایران، ۱۱ رقمی نامعتبر).
- نقش‌ها: `['ROLE_USER', 'ROLE_ADMIN']` یا نقش اختصاصی `ROLE_SYSTEM_OWNER`.
- `status = 0` (غیرفعال برای لاگین) تا امکان ورود با آن وجود نداشته باشد.
- `real_name = 'مالک سیستمی نوبت۷۲۴'`.

این کاربر **صاحب `user_id` پزشکان نیست** (چون یکتاست)؛ بلکه شناسه‌اش در ستون جدید `Doctor.managed_by` قرار می‌گیرد تا مشخص باشد این پزشکان توسط پلتفرم مدیریت می‌شوند و بعداً قابل واگذاری‌اند.

### ۴.۲ نگاشت فیلدها از `doctors.json`

هر رکورد ورودی به این شکل به موجودیت `Doctor` نگاشت می‌شود:

| فیلد ورودی (JSON) | مقصد در `Doctor` | توضیح |
|---|---|---|
| `name` | `name` | مثلاً «دکتر فرخنده حسینی» |
| `gender` (`woman`/`man`) | `gender` | با `Doctor::GENDERS` سازگار است |
| `medicalSystemCode` | `medical_system_code` + `source_ref` | کلید طبیعی یکتا برای dedup |
| `mobileNumber` (`null`) | `mobile_number` = `null` | اجازه دارد null بماند |
| `degree` (`general`) | `degree` | با `Doctor::DEGREES` سازگار است |
| `info` | `info` | «دکترای حرفه‌ای پزشکی» |
| `specialty_id` / `specialty_uuid` | رابطه `specialties` | تطبیق با جدول `specialties` (fallback با uuid) |
| `state_id` / `state_uuid` | رابطه `provinces` | استان محل فعالیت |
| `city_id` / `city_uuid` | رابطه `cities` | شهر محل فعالیت |
| `images` (`[]`) | `images` | خالی → `null` |
| `socialMedia` | `social_media` | نگاشت به کلیدهای مجاز |
| `profile_url` / `source_url` | متادیتای ایمپورت | برای ممیزی و لینک بازبینی |

فیلدهای ثابت هنگام ایمپورت: `owner_status = 'unclaimed'`، `source = 'irimc'`، `managed_by = <systemOwnerId>`، `active_doctor_appointment = false` (تا وقتی مالک واقعی برنامه‌ی کاری تعریف کند نوبت‌دهی روشن نشود).

### ۴.۳ قواعد Idempotency و اعتبارسنجی

- کلید یکتای ایمپورت: `(source = 'irimc', medical_system_code)`. اجرای مجدد ایمپورت رکورد موجود را **به‌روزرسانی** می‌کند نه تکراری‌سازی.
- رکوردهای بدون `medicalSystemCode` رد و در گزارش ایمپورت لاگ می‌شوند.
- تطبیق تخصص/استان/شهر ابتدا با `*_id` و در صورت نبود، با `*_uuid` انجام می‌شود؛ عدم تطبیق باعث رد کل رکورد نمی‌شود بلکه فقط آن رابطه خالی می‌ماند و در گزارش ثبت می‌شود.
- خروجی دستور ایمپورت: تعداد ساخته‌شده / به‌روزشده / ردشده + مسیر فایل گزارش.

### ۴.۴ روش اجرا

دستور کنسول اختصاصی (هم‌سبک `SeedDemoDataCommand` و `SeedCategoriesCommand`):

```bash
ddev exec php bin/console app:doctors:import-irimc var/import/doctors.json --dry-run
ddev exec php bin/console app:doctors:import-irimc var/import/doctors.json
```

`--dry-run` فقط گزارش می‌دهد و چیزی ذخیره نمی‌کند. ایمپورت درون یک تراکنش دیتابیس و به‌صورت دسته‌ای (batch/flush هر ۵۰ رکورد) انجام می‌شود.

---

## ۵. طراحی فنی دیتابیس

تغییرات روی موجودیت `Doctor` (به‌همراه یک migration در `migrations/`):

```
doctors:
  user_id           INT NULL            -- تغییر از NOT NULL به NULL (گزینه A)
  managed_by        INT NULL            -- FK به users.id؛ کاربر «مالک سیستمی»
  owner_status      VARCHAR(20) NOT NULL DEFAULT 'claimed'
                                        -- unclaimed | pending_transfer | claimed
  source            VARCHAR(20) NOT NULL DEFAULT 'manual'   -- irimc | manual
  source_ref        VARCHAR(100) NULL   -- شناسه رکورد مبدأ
  claimed_at        INT NULL
  medical_system_code VARCHAR(25) NULL  -- (موجود) + ایندکس یکتای جزئی
```

قیود و ایندکس‌ها:

- حذف/تعدیل `UniqueConstraint idx_doctors_user`: یکتایی فقط باید برای پزشکانِ **دارای کاربر** اعمال شود. چون MariaDB از partial unique index پشتیبانی مستقیم ندارد، یکتایی `user_id` در سطح اپلیکیشن (هنگام claim) تضمین می‌شود و ایندکس دیتابیس به `INDEX` ساده تبدیل می‌شود.
- ایندکس یکتای طبیعی: `UNIQUE (source, medical_system_code)` برای idempotency ایمپورت.
- ایندکس `owner_status` برای فیلتر سریع «پزشکان بدون‌مالک».

سازگاری با داده‌ی موجود: تمام پزشکان فعلی هنگام migration مقدار `owner_status = 'claimed'` و `source = 'manual'` می‌گیرند تا رفتارشان تغییر نکند.

> نکته سازگاری: طبق `CLAUDE.md`، `medical_system_code` تا الان `nullable` و بدون یکتایی بوده؛ پیش از افزودن ایندکس یکتا باید داده‌ی موجود از نظر تکراری بودن پاک‌سازی شود.

---

## ۶. طراحی API (بک‌اند clinicpro)

پاسخ‌ها از پوشش `BaseController` پیروی می‌کنند: `{ success, data }` / `{ success, errors }` / صفحه‌بندی `{ data, meta }`. مطابق قانون پروژه، هر تغییر کنترلر باید در `docs/api/*` هم مستند شود.

### ۶.۱ ایمپورت (داخلی / ادمین)

معمولاً از طریق دستور کنسول انجام می‌شود؛ در صورت نیاز به تریگر از پنل ادمین:

```
POST /api/v1/admin/doctors/import-irimc      [ROLE_ADMIN]
  body: { source_url?, dry_run?: bool, records: [...] }
  → 200 { success, data: { created, updated, skipped, report_url } }
```

### ۶.۲ فهرست پزشکان بدون‌مالک

اندپوینت موجود `GET /api/v1/doctors` با فیلتر جدید `owner_status` توسعه می‌یابد تا هم برای پنل ادمین و هم برای صفحه‌ی «تصاحب پروفایل» در Nobat724 قابل‌استفاده باشد:

```
GET /api/v1/doctors?owner_status=unclaimed&search=&city_id=&specialty_id=
  → 200 { success, data: [...], meta }
```

### ۶.۳ درخواست تصاحب (Claim) — عمومی و احراز هویت‌شده

```
POST /api/v1/doctor/{uuid}/claim            [IS_AUTHENTICATED_FULLY]
  body: { national_code, medical_system_code, activity_time? }
  قواعد:
   - پروفایل باید owner_status = 'unclaimed' باشد، وگرنه 409.
   - medical_system_code ورودی باید با رکورد پزشک مطابقت کند، وگرنه 422.
   - کاربر لاگین‌شده (که موبایلش قبلاً با OTP تأیید شده) نباید از قبل پزشکِ دیگری داشته باشد.
   - رکورد به pending_transfer می‌رود و یک ClaimRequest ثبت می‌شود.
  → 202 { success, data: { claim_id, status: 'pending_transfer' } }
```

### ۶.۴ تأیید/رد توسط ادمین و نهایی‌سازی انتقال

```
GET   /api/v1/admin/doctor-claims?status=pending        [ROLE_ADMIN]
POST  /api/v1/admin/doctor-claims/{claimId}/approve      [ROLE_ADMIN]
POST  /api/v1/admin/doctor-claims/{claimId}/reject       [ROLE_ADMIN] { reason }
```

هنگام approve، عملیات انتقال مالکیت (بخش ۷) به‌صورت اتمیک اجرا می‌شود.

> امکان «انتقال خودکار» (بدون ادمین) نیز قابل تعریف است: اگر `national_code` کاربر تأییدشده باشد و `medical_system_code` و نام کاملاً منطبق باشند، سیستم می‌تواند مستقیماً claim را تأیید کند. تصمیم پیش‌فرض این سند: **تأیید ادمین اجباری برای فاز اول** (بخش ۱۳).

---

## ۷. بخش دوم — مکانیزم انتقال مالکیت

جریان کامل تصاحب پروفایل توسط پزشک واقعی:

1. **کشف:** پزشک در Nobat724 نام خود را می‌بیند (پروفایل `unclaimed`) و روی «این پروفایل من است» کلیک می‌کند.
2. **احراز هویت پایه:** اگر لاگین نیست، با موبایل + OTP ثبت‌نام/ورود می‌کند. در این مرحله یک `User` واقعی با موبایل واقعی ساخته می‌شود (مسیر عادی auth موجود).
3. **تطبیق هویت:** فرم تصاحب، `national_code` و `medical_system_code` را می‌گیرد و با رکورد پزشک تطبیق می‌دهد (`POST .../claim`). پروفایل به `pending_transfer` می‌رود.
4. **بازبینی:** ادمین در پنل، درخواست را با `profile_url` سازمان نظام پزشکی بازبینی و approve/reject می‌کند.
5. **نهایی‌سازی انتقال (اتمیک):**
   - `Doctor.user` = کاربر واقعی پزشک (پر شدن ستونی که تا الان null بود).
   - `Doctor.managed_by` = `null`؛ `owner_status = 'claimed'`؛ `claimed_at = time()`.
   - افزودن `ROLE_DOCTOR` به کاربر واقعی (همان منطق موجود در `DoctorController::create`).
   - از این پس ویرایش پروفایل توسط خود پزشک از طریق `PATCH /api/v1/doctor/{uuid}` مجاز است (چک مالکیت فعلی `getUser()->getId() === user` اکنون درست کار می‌کند).
6. **اطلاع‌رسانی:** پیامک/نوتیف تأیید به پزشک (هم‌سبک `Sms` موجود).

قواعد یکتایی هنگام انتقال: چون یک کاربر واقعی نباید صاحب دو پزشک شود، پیش از اتصال باید بررسی شود که `user_id` مقصد در جدول `doctors` تکراری نشود (تضمین در سطح اپلیکیشن، جایگزین قید یکتای حذف‌شده).

---

## ۸. اتصال با Nobat724 (`nobat724_front`)

مصرف‌کننده‌ی API از طریق `services/response.js` است (که هم‌اکنون `getDoctors`, `postDoctor`, ... را دارد). تغییرات لازم:

- **نمایش پزشکان بدون‌مالک:** فهرست فعلی پزشکان (`api/v1/doctors`) به‌طور خودکار پزشکان `unclaimed` را هم شامل می‌شود؛ در کارت پزشک، به‌جای دکمه‌ی «رزرو نوبت»، دکمه‌ی «این پروفایل من است / تکمیل پروفایل» نمایش داده می‌شود چون `active=false` است.
- **صفحه/فرم تصاحب:** فراخوانی `POST api/v1/doctor/{uuid}/claim` پس از ورود با OTP. نگهداری `access_token`/`refresh_token` در کوکی (مطابق الگوی فعلی Nobat724).
- **زبان/تقویم:** تمام رشته‌های جدید فارسی و تاریخ‌ها جلالی (شمسی) بمانند.
- رعایت قرارداد: تغییر قالب پاسخ در بک‌اند باید در `nobat724_front/services/response.js` هم منعکس شود، چون در زمان build خطا نمی‌دهد.

---

## ۹. اثر بر `clinic-pro-tauri`

اپ دسکتاپ نیز کلاینت همان API است (`src/service/response.js`) و از CASL برای نقش‌ها استفاده می‌کند (`clinic`, `doctor`, `clinic_doctor`, `secretary`).

- اگر لیست پزشکان در اپ نمایش داده می‌شود، باید فیلد `owner_status` و رفتار `active=false` را مدیریت کند (پزشک بدون‌مالک قابل رزرو آنلاین نیست).
- مرز sync آفلاین/آنلاین این اپ هنوز کامل نگاشت نشده؛ پیش از فرض هم‌ترازی، رفتار `owner_status` در دیتابیس محلی SQLite باید بررسی شود (طبق هشدار `AGENTS.md`).
- برای فاز اول، تغییر در Tauri **اختیاری** است؛ فقط در صورتی که این اپ پزشکان `unclaimed` را نشان دهد لازم می‌شود.

---

## ۱۰. جریان کاربری (خلاصه‌ی گام‌به‌گام)

```
[نظام پزشکی JSON] → دستور ایمپورت → پزشکِ unclaimed (managed_by = System Owner)
         │
         ▼
   نمایش در Nobat724 (active=false، بدون نوبت آنلاین)
         │  پزشک واقعی: «این پروفایل من است»
         ▼
   ورود با موبایل + OTP  →  ساخت User واقعی
         │
         ▼
   فرم تصاحب (کد ملی + کد نظام پزشکی) → POST /claim → pending_transfer
         │
         ▼
   بازبینی ادمین (approve) → انتقال اتمیک:
        user_id=واقعی، owner_status=claimed، +ROLE_DOCTOR
         │
         ▼
   پزشک پروفایل و برنامه‌ی کاری را کامل می‌کند → active=true → نوبت‌دهی آنلاین فعال
```

---

## ۱۱. حالات مرزی و قواعد کسب‌وکار

- **درخواست تصاحب هم‌زمان دو نفر برای یک پروفایل:** فقط اولین `pending_transfer` پذیرفته می‌شود؛ بقیه با ۴۰۹ رد می‌شوند تا تعیین تکلیف قبلی روشن شود.
- **کاربری که قبلاً پزشک دارد:** نمی‌تواند پروفایل دوم را تصاحب کند (قید یکتای منطقی `user_id`).
- **عدم تطابق کد نظام پزشکی:** رد با ۴۲۲ و بدون تغییر وضعیت.
- **رد توسط ادمین:** پروفایل به `unclaimed` بازمی‌گردد و برای تصاحب مجدد آزاد می‌شود.
- **حذف پزشک بدون‌مالک:** مجاز برای ادمین (مسیر فعلی `DELETE`); اما پزشکِ `claimed` طبق رفتار فعلی محافظت می‌شود.
- **ایمپورت مجدد یک پزشکِ از قبل claimed:** فیلدهای هویتی به‌روز نمی‌شوند (مالک واقعی اولویت دارد)؛ فقط در گزارش «skipped/claimed» ثبت می‌شود.
- **نوبت‌دهی:** تا زمانی که پروفایل `unclaimed` است، `active_doctor_appointment=false` و برنامه‌ی کاری وجود ندارد؛ لذا در `toListArray` مقدار `active=false` می‌شود و رزرو ممکن نیست.

---

## ۱۲. مراحل پیاده‌سازی (به‌ترتیب و به‌تفکیک ریپو)

مطابق `CLAUDE.md`: ابتدا بک‌اند `clinicpro`، سپس مستندسازی API، سپس کلاینت‌ها.

**الف) `clinicpro` (بک‌اند):**
1. افزودن فیلدهای `managed_by`, `owner_status`, `source`, `source_ref`, `claimed_at` و nullable کردن `user_id` در `Doctor` + migration در `migrations/`.
2. پاک‌سازی داده و افزودن ایندکس یکتای `(source, medical_system_code)`.
3. دستور کنسول `app:doctors:import-irimc` (با `--dry-run`، گزارش، تراکنش).
4. دستور/سیدر ساخت کاربر «مالک سیستمی».
5. موجودیت/جدول `DoctorClaim` + اندپوینت‌های claim و approve/reject.
6. توسعه‌ی فیلتر `owner_status` در `GET /api/v1/doctors` و به‌روزرسانی چک‌های مالکیت.
7. به‌روزرسانی `docs/api/*` (طبق قانون استاندارد پروژه) و افزودن این سند به مستندات.

**ب) `nobat724_front`:**
8. هم‌ترازی `services/response.js` با قالب‌های جدید.
9. دکمه‌ی «این پروفایل من است» روی کارت پزشکِ `unclaimed` + صفحه‌ی فرم تصاحب (فارسی، جلالی، RTL).

**ج) `clinic-pro-tauri` (در صورت نیاز):**
10. مدیریت `owner_status`/`active=false` در لیست پزشکان و بررسی مرز sync محلی.

**د) بازبینی نهایی:**
11. تست ایمپورت روی نمونه‌ی ۱۶۰ رکورد، تست جریان claim سرتاسری، و بازسازی کاربران تست (`ddev exec php create_test_users.php`).

---

## ۱۳. تصمیمات باز و ریسک‌ها

- **گزینه A در برابر B:** این سند گزینه A (nullable کردن `user_id` + `managed_by`) را توصیه می‌کند چون جدول `users` را با کاربران جعلی آلوده نمی‌کند و مدل مالکیت را صریح می‌سازد. هزینه‌اش: از دست رفتن قید یکتای دیتابیسی روی `user_id` و انتقال آن به سطح اپلیکیشن.
- **تأیید ادمین در برابر انتقال خودکار:** پیش‌فرض فاز اول تأیید دستی ادمین است (امن‌تر برای هویت پزشک). خودکارسازی بعداً با اتکا به تأیید کد ملی افزوده می‌شود.
- **کیفیت داده‌ی نظام پزشکی:** برخی رکوردها ممکن است فاقد `specialty_id`/`city_id` معتبر باشند؛ گزارش ایمپورت باید این‌ها را شفاف کند.
- **حریم خصوصی:** نمایش عمومی نام و کد نظام پزشکی پیش از رضایت پزشک، ملاحظه‌ی حقوقی دارد و باید با سیاست پلتفرم بررسی شود.
- **یکتایی موبایل مالک سیستمی:** مقدار رزروشده باید تضمیناً هرگز با موبایل واقعی کاربر تداخل نکند.

---

## ۱۴. مرجع نمونه‌ی داده

نمونه‌ی یک رکورد ورودی از `doctors.json` (۱۶۰ رکورد، همگی `mobileNumber: null`):

```json
{
  "name": "دکتر فرخنده حسینی",
  "gender": "woman",
  "medicalSystemCode": "145657",
  "mobileNumber": null,
  "degree": "general",
  "info": "دکترای حرفه‌ای پزشکی",
  "specialty_id": 1,
  "specialty_name": "پزشک عمومی",
  "state_id": 23,
  "state_name": "کهگیلویه و بویراحمد",
  "city_id": 123,
  "city_name": "یاسوج",
  "profile_url": "https://membersearch.irimc.org/member/profile?id=02058131-...",
  "source_url": "https://membersearch.irimc.org"
}
```
