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
+41
View File
@@ -169,6 +169,47 @@ class DoctorClaimTest extends ApiTestCase
$this->assertSame(422, $this->responseCode());
}
public function testMobileMismatchIsRejected(): void
{
$uuid = $this->importUnclaimedDoctor();
$this->mockApiIr();
$claimer = $this->createUser(['ROLE_USER']);
$body = $this->claimBody();
$body['mobile'] = '09990000000'; // متفاوت با موبایل کاربر
$this->authJson('POST', "/api/v1/doctor/{$uuid}/claim", $claimer, $body);
$this->assertSame(422, $this->responseCode());
}
public function testOwnerCanDeleteOwnProfileButOthersCannot(): void
{
$uuid = $this->importUnclaimedDoctor();
$this->mockApiIr();
$claimer = $this->createUser(['ROLE_USER']);
$this->authJson('POST', "/api/v1/doctor/{$uuid}/claim", $claimer, $this->claimBody());
$this->assertSame(200, $this->responseCode());
// کاربر دیگر → 403
$other = $this->createUser(['ROLE_USER']);
$this->authJson('DELETE', "/api/v1/doctor/{$uuid}", $other);
$this->assertSame(403, $this->responseCode());
// مالک → 200
$this->authJson('DELETE', "/api/v1/doctor/{$uuid}", $claimer);
$this->assertSame(200, $this->responseCode());
$this->em->clear();
$this->assertNull($this->em->getRepository(Doctor::class)->findOneBy(['uuid' => $uuid]));
}
public function testUnclaimedProfileNotDeletableByRandomUser(): void
{
$uuid = $this->importUnclaimedDoctor();
$user = $this->createUser(['ROLE_USER']);
$this->authJson('DELETE', "/api/v1/doctor/{$uuid}", $user);
$this->assertSame(403, $this->responseCode());
}
public function testClaimInfoIsPublic(): void
{
$uuid = $this->importUnclaimedDoctor();