feat(doctor): complete IRIMC import feature — claim flow, least-privilege importer, unique import key
- Extract import logic from AdminApiController into DoctorImportService (thin DoctorImportController keeps the same route/contract) - Surrogate users get marker role ROLE_UNCLAIMED_DOCTOR (+ backfill command app:doctors:backfill-surrogate-role) enabling safe deletion after claim - DB-level UNIQUE (source, medical_system_code) + concurrent-import retry - Doctor profile claim flow (climed.md): shahkar + PersonInfo identity checks via existing ApiIrService, Persian name normalization (PersianText), pessimistic-lock race protection, DoctorClaimRequest audit table (national code hashed, mobile masked), doctor_claim rate limiter, public claim-info endpoint, welcome SMS - Admin support tools: manual transfer endpoint + paginated doctor-claims audit list + owner_status filter/fields in admin doctors list - Least privilege: system owner now gets ROLE_IMPORTER (ROLE_ADMIN stripped), import endpoint accepts ADMIN|IMPORTER, isStaff includes IMPORTER - Headless crawler login: X-Service-Token header bypasses captcha only (rate limit + password checks intact; empty env = no bypass) - docs: doctor-claim.md (new), doctor-import.md, admin.md, doctor.md - tests: DoctorImportTest (6), DoctorClaimTest (11), PersianTextTest (5) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+37
-13
@@ -1,8 +1,8 @@
|
||||
# Doctor Import (IRIMC) API
|
||||
|
||||
> **Endpoint:** `POST /api/v1/admin/doctors/import`
|
||||
> **Permission:** `ROLE_ADMIN`
|
||||
> **Controller:** `App\Admin\Controller\AdminApiController::importDoctor`
|
||||
> **Permission:** `ROLE_ADMIN` **یا** `ROLE_IMPORTER` (نقش حداقلی کاربر سیستمی کرالر)
|
||||
> **Controller:** `App\Doctor\Controller\DoctorImportController::import` — منطق دامنه در `App\Doctor\Service\DoctorImportService`
|
||||
|
||||
وارد کردن یک پزشک از سازمان نظام پزشکی (`membersearch.irimc.org`) **بدون شماره موبایل**.
|
||||
برخلاف `POST /api/v1/admin/doctors` (که موبایل معتبر ایرانی میخواهد)، این اندپوینت برای
|
||||
@@ -31,7 +31,9 @@
|
||||
|
||||
## idempotency
|
||||
|
||||
کلید یکتای منطقی: `(source, medical_system_code)`.
|
||||
کلید یکتای `(source, medical_system_code)` — از این نسخه **در سطح دیتابیس** هم unique است
|
||||
(`uniq_doctors_source_code`، migration `Version20260711150000`)؛ درخواست همزمانِ همان پزشک
|
||||
با retry داخلی به مسیر update میرود و هرگز رکورد تکراری نمیسازد.
|
||||
|
||||
- اگر پزشکی با همان `source`+`medical_system_code` وجود نداشته باشد → **ساخته** میشود (`201`).
|
||||
- اگر وجود داشته باشد و `owner_status != claimed` → **بهروزرسانی** میشود (`200`).
|
||||
@@ -132,10 +134,16 @@
|
||||
// Response 200
|
||||
{ "success": true, "data": {
|
||||
"uuid": "…", "owner_status": "claimed",
|
||||
"transferred_to": "09120000000", "placeholder_deleted": true
|
||||
"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` | پزشک یافت نشد |
|
||||
@@ -154,13 +162,29 @@ php bin/console app:system-owner 0000000000 --password=<secret> --activate
|
||||
php bin/console app:system-owner 0000000000 --deactivate
|
||||
```
|
||||
|
||||
کاربر باید `ROLE_ADMIN` و `status=1` داشته باشد تا لاگینِ رمزی (`POST /api/v1/user/login`)
|
||||
و فراخوانی این اندپوینت ممکن باشد.
|
||||
کاربر سیستمی **least privilege** است: فقط `ROLE_USER,ROLE_IMPORTER` میگیرد (اجرای مجدد
|
||||
دستور، `ROLE_ADMIN` قدیمی را هم حذف میکند). `ROLE_IMPORTER` فقط به همین اندپوینت ایمپورت
|
||||
دسترسی دارد و به هیچ اندپوینت `/api/v1/admin/*` دیگری راه ندارد (تست: `DoctorImportTest::testImporterRoleCanImportButNothingElse`).
|
||||
|
||||
> ⚠️ **captcha:** مسیر `/api/v1/user/login` از `CaptchaGuard` رد میشود و این guard وقتی
|
||||
> `ALTCHA_ENABLED=true` باشد (مقدار فعلی `.env`) یک payloadِ altcha میخواهد. برای اجرای
|
||||
> بدونِمرورگرِ کرالر یکی از اینها لازم است:
|
||||
> ۱) روی همان سرور `ALTCHA_ENABLED=false` در `.env.local` (سادهترین برای dev)، یا
|
||||
> ۲) افزودن یک استثنا در `PasswordAuthenticator` که برای کاربر مالک سیستمی captcha را رد کند،
|
||||
> یا ۳) لاگین سرویس با یک هدر سرّیِ مورد اعتماد. تا وقتی این حل نشود، لاگین کرالر با ۴۲۲
|
||||
> (`ERR_CAPTCHA_001`) رد میشود.
|
||||
### لاگین سرویسی (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 # اعمال
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user