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>
This commit is contained in:
hamed
2026-07-11 14:57:41 +03:30
co-authored by Claude Opus 4.8
parent 3dcd4f3b26
commit 2f0131171d
20 changed files with 1204 additions and 570 deletions
+6 -1
View File
@@ -41,6 +41,7 @@ unclaimed ──claim/transfer شروع──▶ pending_transfer ──موفق
**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
@@ -48,7 +49,9 @@ unclaimed ──claim/transfer شروع──▶ pending_transfer ──موفق
"national_code": "0010007700",
"birth_date": "1371/1/1",
"first_name": "فرخنده",
"last_name": "حسینی"
"last_name": "حسینی",
"mobile": "09121234567",
"altcha": "<payload کپچا>"
}
```
@@ -57,6 +60,8 @@ unclaimed ──claim/transfer شروع──▶ pending_transfer ──موفق
| `national_code` | ✅ | ۱۰ رقم (ارقام فارسی پذیرفته و نرمال می‌شوند) |
| `birth_date` | ✅ | شمسی `Y/m/d` |
| `first_name` / `last_name` | ✅ | با هویت ثبت احوال و نام پروفایل تطبیق داده می‌شود (نرمال‌سازی ي/ی، ك/ک، نیم‌فاصله — `PersianText`) |
| `mobile` | ❌ | اگر داده شود، باید با موبایل حساب کاربری یکی باشد وگرنه `422 ERR_CONFLICT_001`؛ اگر خالی باشد از موبایل کاربر لاگین‌شده استفاده می‌شود |
| `altcha` | prod | payload کپچای ALTCHA |
### مراحل سرور (اتمیک/ضد race)
+3 -3
View File
@@ -269,7 +269,7 @@ Updated doctor object (same structure as GET single).
Delete a doctor profile.
**Permission:** `ROLE_ADMIN`
**Permission:** `IS_AUTHENTICATED_FULLY`**admin** (any profile) **or the owner** of a `claimed` profile (`doctor.user === current user`). Other users get `403`.
> **Side effect:** the doctor's insurance configuration (`tenant_insurances`, `entity_insurance_pricing`, and their `tenant_service_coverages`) is purged in the same request — these reference the doctor via a polymorphic `entity_id` with no DB FK, so the cleanup is enforced in the application.
@@ -292,8 +292,8 @@ Delete a doctor profile.
| Code | HTTP | Description |
|------|------|-------------|
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
| `ERR_FORBIDDEN_001` | 403 | Not admin and not the owner of this claimed profile |
| `ERR_VALIDATION_002` | 404 | Doctor not found |
| `ERR_CONFLICT_001` | 409 | Doctor has existing appointments and cannot be deleted |
---