# Doctor Import (IRIMC) API > **Endpoint:** `POST /api/v1/admin/doctors/import` > **Permission:** `ROLE_ADMIN` **یا** `ROLE_IMPORTER` (نقش حداقلی کاربر سیستمی کرالر) > **Controller:** `App\Doctor\Controller\DoctorImportController::import` — منطق دامنه در `App\Doctor\Service\DoctorImportService` وارد کردن یک پزشک از سازمان نظام پزشکی (`membersearch.irimc.org`) **بدون شماره موبایل**. برخلاف `POST /api/v1/admin/doctors` (که موبایل معتبر ایرانی می‌خواهد)، این اندپوینت برای هر پزشک یک **کاربر جانشینِ غیرفعال** با شناسهٔ مصنوعی و نقشِ اختصاصی `ROLE_UNCLAIMED_DOCTOR` می‌سازد و پروفایل را در وضعیت `unclaimed` ذخیره می‌کند تا بعداً به پزشک واقعی منتقل شود. این نقش، هم کاربر جانشین را قابل‌شناسایی می‌کند و هم مبنای حذفِ امنِ او پس از انتقال است. مصرف‌کنندهٔ اصلی: خزندهٔ پایتون (`clinicpro-crawler/pipeline.py`) که با کاربر «مالک سیستمی» (`0000000000`) لاگین می‌کند و هر ~۶۰ ثانیه یک پزشک را می‌فرستد. --- ## مدل مالکیت (ستون‌های جدید `doctors`) | ستون | مقدار هنگام ایمپورت | توضیح | |---|---|---| | `owner_status` | `unclaimed` | `claimed` \| `unclaimed` \| `pending_transfer` | | `source` | `irimc` | `manual` (پیش‌فرض رکوردهای قدیمی) \| `irimc` | | `source_ref` | `profile_url` | شناسهٔ رکورد مبدأ برای ممیزی | | `managed_by` | id کاربر ادمینِ فراخوان | کاربری که پروفایل را مدیریت می‌کند | | `claimed_at` | `null` | زمان انتقال مالکیت (هنگام claim پر می‌شود) | رکوردهای موجود در migration مقدار `manual` + `claimed` می‌گیرند تا رفتارشان تغییر نکند. --- ## idempotency کلید یکتای `(source, medical_system_code)` — از این نسخه **در سطح دیتابیس** هم unique است (`uniq_doctors_source_code`، migration `Version20260711150000`)؛ درخواست هم‌زمانِ همان پزشک با retry داخلی به مسیر update می‌رود و هرگز رکورد تکراری نمی‌سازد. - اگر پزشکی با همان `source`+`medical_system_code` وجود نداشته باشد → **ساخته** می‌شود (`201`). - اگر وجود داشته باشد و `owner_status != claimed` → **به‌روزرسانی** می‌شود (`200`). - اگر وجود داشته باشد و `owner_status == claimed` → **رد** می‌شود (`200`, `skipped: "claimed"`) تا دادهٔ مالک واقعی بازنویسی نشود. --- ## Request ```json { "name": "دکتر فرخنده حسینی", "medical_system_code": "145657", "source": "irimc", "source_ref": "https://membersearch.irimc.org/member/profile?id=…", "gender": "woman", "degree": "general", "info": "دکترای حرفه‌ای پزشکی", "specialties": [1], "states": [23], "cities": [123] } ``` | فیلد | الزامی | توضیح | |---|:---:|---| | `name` | ✅ | نام کامل پزشک | | `medical_system_code` | ✅ | کد نظام پزشکی (کلید idempotency) | | `source` | — | پیش‌فرض `irimc` | | `source_ref` | — | `profile_url` یا شناسهٔ مبدأ | | `gender` | — | `man` \| `woman` | | `degree` | — | `general` \| `expert` \| `specialist` \| `subspecialistplus` | | `info` | — | متن تخصص/توضیح | | `specialties` / `states` / `cities` | — | آرایهٔ **شناسه‌های مرجع** (id) | > `gender`/`degree` باید با ثابت‌های `Doctor::GENDERS` / `Doctor::DEGREES` سازگار باشند. > `specialties`/`states`/`cities` اگر داده شوند مجموعهٔ فعلی را **جایگزین** می‌کنند. --- ## Response ### `201 Created` (ساخته شد) ```json { "success": true, "data": { "uuid": "…", "created": true } } ``` ### `200 OK` (به‌روزرسانی شد) ```json { "success": true, "data": { "uuid": "…", "created": false } } ``` ### `200 OK` (رد به‌دلیل تصاحب‌شده) ```json { "success": true, "data": { "uuid": "…", "created": false, "skipped": "claimed" } } ``` ### `422` (اعتبارسنجی) ```json { "success": false, "data": null, "errors": [ { "code": "…", "message": "کد نظام پزشکی الزامی است", "field": "medical_system_code" } ] } ``` --- ## عکس پروفایل پس از ایمپورت، عکس با دو مرحله اضافه می‌شود (همان مسیر پزشکِ عادی): 1. `POST /file/upload/clinic_pro/doctor/field_image` — بدنه: باینری خام؛ هدر `Content-Disposition: attachment; filename="145657.jpg"`. پاسخ: `{ fid, uuid, url, filename, filemime, filesize }`. 2. `PATCH /api/v1/doctor/{uuid}` با بدنهٔ `{ "image_data": <پاسخ مرحلهٔ ۱> }` تا به آرایهٔ `images` پروفایل افزوده شود. --- ## انتقال مالکیت به پزشک واقعی > **Endpoint:** `POST /api/v1/admin/doctors/{uuid}/transfer` — permission `ROLE_ADMIN` مالکیت یک پروفایلِ `unclaimed` را به پزشک واقعی منتقل می‌کند. ```json // Request { "mobile": "09120000000" } ``` مراحل اتمیک: 1. کاربر واقعی بر پایهٔ `mobile` پیدا یا ساخته می‌شود (باید موبایل معتبر ایران باشد). 2. اگر کاربر واقعی از قبل صاحب پزشک دیگری باشد → `409`. 3. `user_id` پروفایل به کاربر واقعی تغییر می‌کند، `ROLE_DOCTOR` به او داده می‌شود، `owner_status = claimed` و `claimed_at = now` و `managed_by = null` می‌شود. 4. **کاربر جانشین حذف می‌شود** — فقط اگر واقعاً `ROLE_UNCLAIMED_DOCTOR` داشته باشد و دیگر هیچ پزشکی به او وصل نباشد (حذف امن). ```json // Response 200 { "success": true, "data": { "uuid": "…", "owner_status": "claimed", "user_mobile": "09120000000", "claim": { "uuid": "…" } } } ``` > این اندپوینت در `App\Doctor\Controller\DoctorClaimController::transfer` است و همان > `DoctorClaimService::transferByAdmin` را صدا می‌زند؛ هر انتقال یک رکورد ممیزی در > `doctor_claim_requests` با `verification_method = "admin_manual"` می‌سازد. > جریان self-claim پزشک (با احراز هویت API.ir) در `docs/api/doctor-claim.md` مستند است. | کد | حالت | |---|---| | `404` | پزشک یافت نشد | | `409` | قبلاً `claimed` است، یا کاربر مقصد پزشک دیگری دارد | | `422` | موبایل نامعتبر | --- ## کاربر سیستمی و چرخهٔ عمر ```bash # ساخت/فعال‌سازی کاربر مالک سیستمی (کرالر با این لاگین می‌کند) php bin/console app:system-owner 0000000000 --password= --activate # غیرفعال‌سازی پس از پایان کار (کرالر هم با --deactivate-on-finish این را از طریق API انجام می‌دهد) php bin/console app:system-owner 0000000000 --deactivate ``` کاربر سیستمی **least privilege** است: فقط `ROLE_USER,ROLE_IMPORTER` می‌گیرد (اجرای مجدد دستور، `ROLE_ADMIN` قدیمی را هم حذف می‌کند). `ROLE_IMPORTER` فقط به همین اندپوینت ایمپورت دسترسی دارد و به هیچ اندپوینت `/api/v1/admin/*` دیگری راه ندارد (تست: `DoctorImportTest::testImporterRoleCanImportButNothingElse`). ### لاگین سرویسی (captcha) مسیر `/api/v1/user/login` کپچای ALTCHA دارد. برای لاگین headless کرالر، هدر سرّی تعریف شده است: ``` X-Service-Token: <مقدار env CRAWLER_SERVICE_TOKEN> ``` - فقط **کپچا** دور زده می‌شود؛ rate limit و اعتبارسنجی رمز دست‌نخورده می‌مانند. - اگر env خالی/تعریف‌نشده باشد هیچ bypass وجود ندارد (secure by default). - چرخش credential: تغییر `CRAWLER_SERVICE_TOKEN` + تغییر رمز با `app:system-owner … --password=…`؛ ابطال فوری: `--deactivate`. ### backfill نقش جانشین‌های قدیمی جانشین‌های ساخته‌شده قبل از افزودن نقش marker: ```bash php bin/console app:doctors:backfill-surrogate-role --dry-run # فقط گزارش php bin/console app:doctors:backfill-surrogate-role # اعمال ```