Files
clinicpro/docs/api/doctor-claim.md
T
hamedandClaude Opus 4.8 2f0131171d feat(doctor): claim captcha+mobile, owner profile delete, admin map zoom fix
- DoctorClaimController: ALTCHA CaptchaGuard on /claim (dev no-op via
  ALTCHA_ENABLED=false); optional `mobile` field must match the logged-in
  user's number (422 ERR_CONFLICT_001 on mismatch)
- DoctorController::delete: now IS_AUTHENTICATED_FULLY — admin (any) or the
  owner of a claimed profile (IDOR-guarded); FK appointment guard kept
- DoctorDetailPage address map: MapController calls map.invalidateSize()
  before flyTo (fixes needing to pick a city twice on a freshly-mounted map);
  geocode retries once (nominatim empty/429 on first hit)
- tests: mobile mismatch, owner-delete allowed + others 403, unclaimed not
  deletable by random user
- docs: doctor-claim.md (mobile+captcha), doctor.md (delete permission)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 14:57:41 +03:30

7.7 KiB

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

{ "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

{
  "national_code": "0010007700",
  "birth_date": "1371/1/1",
  "first_name": "فرخنده",
  "last_name": "حسینی",
  "mobile": "09121234567",
  "altcha": "<payload کپچا>"
}
فیلد الزامی قاعده
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

{
  "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)

{
  "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 (نرمال‌سازی نام).