# Doctor Profile Claim API (تصاحب پروفایل پزشک ایمپورت‌شده) > **Controller:** `App\Doctor\Controller\DoctorClaimController` — منطق در `App\Doctor\Service\DoctorClaimService` > **مصرف‌کننده:** سایت عمومی Nobat724 (همهٔ دامنه‌ها) + پنل ادمین پزشکِ ایمپورت‌شده از نظام پزشکی (`owner_status = unclaimed`) توسط پزشک واقعی تصاحب می‌شود. احراز هویت سمت سرور با **API.ir** انجام می‌شود (شاهکار: تطبیق موبایل↔کدملی؛ PersonInfo: تطبیق کدملی+تاریخ تولد و نام). هیچ درخواستی از فرانت به API.ir نمی‌رود و توکن API.ir هرگز به کلاینت نمی‌رسد. claim پس از تطبیق موفق **خودکار** نهایی می‌شود (بدون approve ادمین — تصمیم مستند در `.claude/prompt/irimc-import-complete.md` §۲.۲). ## چرخهٔ وضعیت ``` unclaimed ──claim/transfer شروع──▶ pending_transfer ──موفق──▶ claimed ▲ │شکست تطبیق/خطای استعلام └───────────────────────────────────┘ (برگشت، قابل تلاش مجدد) ``` هر تلاش یک رکورد ممیزی در `doctor_claim_requests` می‌سازد — کد ملی فقط **hash sha256** و موبایل فقط **mask شده** ذخیره می‌شود؛ هیچ دادهٔ هویتی خام در DB یا لاگ نمی‌ماند. --- ## GET `/api/v1/doctor/{uuid}/claim-info` **Permission:** عمومی (بدون JWT) — فقط برای رندر دکمهٔ «آیا شما این پزشک هستید؟» ### Response `200` ```json { "success": true, "data": { "claimable": true, "owner_status": "unclaimed" } } ``` | کد | حالت | |---|---| | `404` | پزشک یافت نشد | --- ## POST `/api/v1/doctor/{uuid}/claim` **Permission:** `IS_AUTHENTICATED_FULLY` — کاربر با OTP لاگین شده (موبایلش تأییدشده است) **Rate limit:** limiter `doctor_claim` — ۵ تلاش در ساعت به‌ازای هر (کاربر، پزشک) **Captcha:** ALTCHA — بدنه باید payload کپچا بفرستد (`CaptchaGuard::assertValid`)؛ در dev با `ALTCHA_ENABLED=false` بی‌اثر است، در prod اجباری. خطا → `ERR_CAPTCHA_001` (۴۲۲). ### Request ```json { "national_code": "0010007700", "birth_date": "1371/1/1", "first_name": "فرخنده", "last_name": "حسینی", "mobile": "09121234567", "altcha": "" } ``` | فیلد | الزامی | قاعده | |---|:---:|---| | `national_code` | ✅ | ۱۰ رقم (ارقام فارسی پذیرفته و نرمال می‌شوند) | | `birth_date` | ✅ | شمسی `Y/m/d` | | `first_name` / `last_name` | ✅ | با هویت ثبت احوال و نام پروفایل تطبیق داده می‌شود (نرمال‌سازی ي/ی، ك/ک، نیم‌فاصله — `PersianText`) | | `mobile` | ❌ | اگر داده شود، باید با موبایل حساب کاربری یکی باشد وگرنه `422 ERR_CONFLICT_001`؛ اگر خالی باشد از موبایل کاربر لاگین‌شده استفاده می‌شود | | `altcha` | prod | payload کپچای ALTCHA | ### مراحل سرور (اتمیک/ضد race) 1. قفل `PESSIMISTIC_WRITE` روی ردیف پزشک → اگر `unclaimed` نبود `409`؛ اگر کاربر از قبل پزشکی دارد `409`؛ اگر کد ملی متعلق به کاربر دیگری است `409` — سپس `pending_transfer` + رکورد ممیزی (تراکنش کوتاه، بدون فراخوان خارجی داخل قفل). 2. شاهکار (`ApiIrService::shahkarMatch`) — تطبیق موبایل کاربر با کد ملی. اگر API.ir پیکربندی نشده باشد، این گام skip و مبنا موبایلِ OTP-تأییدشده است. 3. `PersonInfo` — تطبیق کدملی+تاریخ تولد؛ `alive=false` → رد. 4. تطبیق نام: ورودی کاربر ↔ هویت تأییدشده ↔ نام پروفایل (بدون پیشوند «دکتر»). 5. نهایی‌سازی اتمیک: `user_id` → کاربر واقعی، `ROLE_DOCTOR`، `national_code_verified=true`، `owner_status=claimed`، حذف امنِ کاربر جانشین (فقط با `ROLE_UNCLAIMED_DOCTOR` و بدون پزشک دیگر). 6. پیامک خوش‌آمد (تمپلیت `welcome`، async). > شکست در گام‌های ۲-۴ پروفایل را به `unclaimed` برمی‌گرداند تا پزشک واقعی بتواند دوباره تلاش کند. > نوبت‌دهی (`active_doctor_appointment`) خاموش می‌ماند تا مالک جدید برنامهٔ کاری تعریف کند. ### Response `200` ```json { "success": true, "data": { "status": "claimed", "claim": { "uuid": "…" }, "doctor": { "uuid": "…", "name": "فرخنده حسینی" } } } ``` ### Errors | کد | HTTP | حالت | |---|---|---| | `ERR_AUTH_001` | 401 | بدون لاگین | | `ERR_NOT_FOUND_001` | 404 | پزشک یافت نشد | | `ERR_CONFLICT_001` | 409 | پروفایل قابل تصاحب نیست / کاربر پزشک دیگری دارد / درخواست هم‌زمان دیگری در جریان است | | `ERR_PROFILE_001` | 409 | کد ملی قبلاً برای کاربر دیگری ثبت شده است | | `ERR_VALIDATION_001/002` | 422 | کد ملی/تاریخ/نام نامعتبر | | `ERR_IDENTITY_001` | 422 | عدم تطبیق هویت (شاهکار/ثبت احوال/نام) — پیام عمومی، ضد enumeration | | `ERR_RATE_LIMIT_001` | 429 | عبور از سقف تلاش | | `ERR_EXTERNAL_001` | 502 | خطا/تایم‌اوت API.ir | | `ERR_EXTERNAL_002` | 503 | API.ir پیکربندی نشده (env) | --- ## POST `/api/v1/admin/doctors/{uuid}/transfer` انتقال دستی (ابزار پشتیبانی ادمین) — مستند کامل در `docs/api/doctor-import.md`. **Permission:** `ROLE_ADMIN`. بدنه `{ "mobile": "09…" }`. همان نهایی‌سازی claim را اجرا می‌کند و رکورد ممیزی با `verification_method="admin_manual"` می‌سازد. --- ## GET `/api/v1/admin/doctor-claims` **Permission:** `ROLE_ADMIN` — لیست ممیزی درخواست‌های claim برای پشتیبانی عملیاتی. | Query | پیش‌فرض | توضیح | |---|---|---| | `status` | همه | `pending` \| `completed` \| `failed` | | `page` / `limit` | 1 / 20 (سقف 50) | صفحه‌بندی استاندارد | ### Response `200` (paginated) ```json { "success": true, "data": [ { "uuid": "…", "status": "completed", "doctor": { "uuid": "…", "name": "فرخنده حسینی" }, "mobile_masked": "0912***4567", "verification_method": "apiir_personinfo+shahkar", "failure_reason": null, "created_at": 1783750000, "completed_at": 1783750040 } ], "meta": { "totalRecords": 1, "totalPages": 1, "currentPage": 1 } } ``` `failure_reason` فارسی و انسانی‌خوان است تا ادمین بدون خواندن لاگ سرور علت شکست را ببیند. --- ## env های مرتبط | متغیر | نقش | |---|---| | `APIIR_*` (baseUrl/token موجود `ApiIrService`) | استعلام شاهکار و PersonInfo | | `CRAWLER_SERVICE_TOKEN` | فقط برای لاگین سرویسی کرالر (`doctor-import.md`) — ربطی به claim ندارد | ## تست‌ها `tests/Doctor/DoctorClaimTest.php` (۱۱ سناریو، API.ir همیشه mock — هیچ تستی به سرویس واقعی درخواست نمی‌زند) و `tests/Shared/PersianTextTest.php` (نرمال‌سازی نام).