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
@@ -0,0 +1,163 @@
# رفع زوم نقشه در افزودن آدرس + کپچا و موبایل در claim + حذف پروفایل توسط مالک
## پروژه
`clinicpro` (پنل ادمین React + backend)
> پرامپت همتا (سایت عمومی): `nobat724_front/.claude/prompt/doctor-map-claim-modal.md` — نقشهٔ صفحه پزشک + مودال claim + دکمهٔ حذف. این پرامپت قرارداد API را که سایت مصرف می‌کند تغییر می‌دهد.
## زمینه
سه موضوع مرتبط با پروفایل پزشک:
1. در فرم افزودن آدرس (پنل ادمین)، با انتخاب شهر نقشه باید روی آن شهر زوم کند؛ ولی بار اول کار نمی‌کند و کاربر مجبور است شهر را **دو بار** انتخاب کند.
2. جریان تصاحب پروفایل (claim) از قبل هست (`DoctorClaimController`) ولی طبق سناریوی جدید باید **کپچای ALTCHA** داشته باشد و **شماره موبایل** به‌صراحت در فرم گرفته و تطبیق داده شود.
3. پس از claim، **مالک** پروفایل باید بتواند پروفایل خود را حذف کند (الان حذف فقط `ROLE_ADMIN` است).
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `assets/admin/pages/DoctorDetailPage.tsx` | فرم آدرس + `MapPicker`/`MapController` (react-leaflet) + geocode شهر |
| `src/Doctor/Controller/DoctorClaimController.php` | endpoint `claim` — افزودن کپچا + فیلد mobile |
| `src/Doctor/Service/DoctorClaimService.php` | منطق claim |
| `src/Shared/Captcha/CaptchaGuard.php` | `assertValid($request)` — الگوی موجود کپچا (در `AuthController`) |
| `src/Doctor/Controller/DoctorController.php` | متد `delete` (خط ۳۷۱، الان `#[IsGranted('ROLE_ADMIN')]`) |
| `docs/api/doctor.md` + `docs/api/doctor-claim.md` | مستندسازی |
## وظیفه ۱ — رفع زوم نقشه هنگام انتخاب شهر (نیازِ دو‌بار انتخاب)
### وضعیت فعلی
```tsx
// DoctorDetailPage.tsx:468 — recenter فقط با flyTo روی تغییر flyTarget
function MapController({ flyTarget }: { flyTarget: [number, number] | null }) {
const map = useMap();
useEffect(() => {
if (flyTarget) map.flyTo(flyTarget, 12, { duration: 1.2 });
}, [flyTarget, map]);
return null;
}
// :935 — انتخاب شهر → geocode خارجی → setMapFlyTarget
onChange={(val, label) => {
setValue('city_id', val);
if (label) {
geocodeCityInIran(label).then(coords => { if (coords) setMapFlyTarget(coords); });
}
}}
```
### ریشه‌ها
1. **نقشهٔ تازه‌مانت‌شده اندازه‌اش ۰ است:** وقتی فرم/نقشه تازه باز می‌شود، Leaflet ابعاد کانتینر را نگرفته و `flyTo` روی نقشهٔ بدون‌اندازه بی‌اثر است؛ انتخاب دومِ شهر (که نقشه دیگر layout شده) کار می‌کند. باید `map.invalidateSize()` قبل از `flyTo` صدا زده شود.
2. **geocode خارجی (nominatim) async و rate-limited است:** اولین فراخوان ممکن است خالی/۴۰۳ برگردد (کاربر بلافاصله بعد از باز شدن انتخاب می‌کند) و `setMapFlyTarget` اجرا نشود.
### راه‌حل
**الف) `invalidateSize` + recenter مقاوم:**
```tsx
function MapController({ flyTarget }: { flyTarget: [number, number] | null }) {
const map = useMap();
useEffect(() => {
map.invalidateSize(); // ابعاد را پس از mount/تغییر layout به‌روز کن
if (flyTarget) map.flyTo(flyTarget, 12, { duration: 1.2 });
}, [flyTarget, map]);
return null;
}
```
اگر نقشه داخل بخشی است که با باز/بسته شدن mount/unmount می‌شود، یک `invalidateSize` هنگام mount هم لازم است (effect بالا با `map` در deps این را پوشش می‌دهد).
**ب) geocode مقاوم — ترجیحاً از مختصات خودِ شهر به‌جای سرویس خارجی:**
- اول بررسی کن آیا آبجکت شهر در `cities` (یا endpoint `/api/v1/cities`) مختصات دارد؛ اگر دارد، مستقیم از همان `flyTarget` را بساز و از nominatim صرف‌نظر کن (سریع، بدون rate-limit، بدون async ناموفق).
- اگر مختصات در دیتا نیست، `geocodeCityInIran` را نگه‌دار اما با retry ساده (یک تلاش مجدد بعد از ~۱ ثانیه در صورت پاسخ خالی) و بدون بلاک‌کردن UI.
> **edge:** اگر کاربر پیش از resolve شدن geocode شهر دیگری انتخاب کند، فقط آخرین انتخاب باید اعمال شود (نگه‌داشتن یک request id/ابطال نتیجهٔ قدیمی).
## وظیفه ۲ — کپچا و فیلد موبایل در claim
### وضعیت فعلی
`DoctorClaimController::claim` پشت `IS_AUTHENTICATED_FULLY` است و کپچا ندارد؛ موبایل را از کاربر لاگین‌شده می‌گیرد (`$user->getMobileNumber()`)، فیلد جدا در بدنه ندارد.
### راه‌حل
**الف) کپچای ALTCHA** — الگوی موجود `CaptchaGuard::assertValid($request)` (همان که در `AuthController::sendCode` استفاده می‌شود):
```php
// ابتدای DoctorClaimController::claim، پیش از rate limiter/منطق
$this->captcha->assertValid($request); // تزریق CaptchaGuard در constructor
```
- `assertValid` هنگام `ALTCHA_ENABLED=false` بی‌اثر است (dev)، و در prod payload کپچا می‌خواهد؛ خطای آن به `ERR_CAPTCHA_001` (۴۲۲) تبدیل می‌شود (ExceptionSubscriber).
**ب) فیلد موبایل صریح** — بدنه فیلد `mobile` بگیرد و با موبایل کاربر لاگین‌شده تطبیق داده شود (طبق سناریو: «شماره موبایل ثبت‌شده در حساب کاربری باید به عنوان مالک بررسی شود»):
```php
$mobile = \App\Shared\Util\PersianText::normalize((string) ($data['mobile'] ?? ''));
$mobile = preg_replace('/\D/', '', $mobile);
if (!preg_match('/^09\d{9}$/', $mobile)) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'شماره موبایل نامعتبر است', 422, 'mobile');
}
if ($mobile !== $user->getMobileNumber()) {
return $this->error(ErrorCodes::ERR_CONFLICT_001, 'شماره موبایل باید با حساب کاربری شما یکی باشد', 422, 'mobile');
}
```
- `DoctorClaimService::claim` بدون تغییر می‌ماند (همان موبایل کاربر برای شاهکار استفاده می‌شود).
- سند `docs/api/doctor-claim.md`: افزودن فیلد `mobile` به بدنه + ذکر کپچای ALTCHA و کد `ERR_CAPTCHA_001`.
## وظیفه ۳ — حذف پروفایل توسط مالک
### وضعیت فعلی
```php
// DoctorController.php:369
#[Route('/api/v1/doctor/{uuid}', methods: ['DELETE'])]
#[IsGranted('ROLE_ADMIN')]
public function delete(string $uuid): JsonResponse { ... }
```
فقط ادمین حذف می‌کند؛ مالک پزشک نمی‌تواند پروفایل خود را حذف کند.
### راه‌حل
مالک (`claimed` و `doctor.getUser()->getId() === user`) هم اجازهٔ حذف بگیرد:
- `#[IsGranted('ROLE_ADMIN')]` را از متد بردار و به `#[IsGranted('IS_AUTHENTICATED_FULLY')]` تغییر بده؛ داخل متد `#[CurrentUser] User $user` را بگیر و کنترل دسترسی صریح:
```php
public function delete(string $uuid, #[CurrentUser] User $user): JsonResponse
{
$doctor = $this->doctorRepo->findByUuid($uuid);
if ($doctor === null) {
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
}
$isAdmin = $user->hasRole('ROLE_ADMIN');
$isOwner = $doctor->getOwnerStatus() === 'claimed' && $doctor->getUser()->getId() === $user->getId();
if (!$isAdmin && !$isOwner) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'اجازهٔ حذف این پروفایل را ندارید', 403);
}
// ... گاردِ FK موجود (نوبت ثبت‌شده → 409) و remove فعلی بدون تغییر ...
}
```
- گارد FK موجود (پزشکِ دارای نوبت → ۴۰۹ `ERR_CONFLICT_001`) حفظ شود.
- امنیت: IDOR — کاربرِ لاگین‌شده فقط پروفایلِ **claimed متعلق به خودش** یا (اگر ادمین) هر پروفایلی را حذف کند؛ پروفایلِ `unclaimed` توسط کاربر عادی حذف نشود.
## نکات مهم
- بعد از تغییر `delete`/`claim`، `docs/api/doctor.md` و `docs/api/doctor-claim.md` را به‌روز کن (قانون پروژه).
- نقشهٔ react-leaflet فقط admin frontend است؛ تغییر backend ندارد.
- کپچا: مسیر public سایت (`nobat724`) هم باید payload ALTCHA بفرستد (پرامپت همتا)؛ در dev با `ALTCHA_ENABLED=false` بی‌اثر است.
- تست:
```bash
ddev exec php -l src/Doctor/Controller/DoctorClaimController.php
ddev exec php bin/console cache:clear
ddev exec npx tsc --noEmit --project tsconfig.json 2>&1 | head
ddev exec yarn dev
# claim: با mobile نامطابق → 422؛ delete توسط مالک claimed → 200؛ توسط کاربر دیگر → 403
ddev exec php bin/phpunit tests/Doctor
```
- تست‌های موجود `DoctorClaimTest`/`DoctorImportTest` را با فیلد `mobile` و مسیر delete مالک به‌روز/تکمیل کن؛ سبز بمانند.
+19 -9
View File
@@ -449,15 +449,19 @@ function StarRating({ rate }: { rate: number }) {
const IRAN_CENTER: [number, number] = [32.4279, 53.6880];
async function geocodeCityInIran(cityName: string): Promise<[number, number] | null> {
try {
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(cityName + ',ایران')}&format=json&countrycodes=ir&limit=1`;
const res = await fetch(url, { headers: { 'Accept-Language': 'fa' } });
const data = await res.json();
if (data?.[0]) return [parseFloat(data[0].lat), parseFloat(data[0].lon)];
return null;
} catch {
return null;
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(cityName + ',ایران')}&format=json&countrycodes=ir&limit=1`;
// nominatim گاهی روی اولین فراخوان خالی/۴۲۹ برمی‌گرداند؛ یک retry تا انتخاب اول هم کار کند.
for (let attempt = 0; attempt < 2; attempt++) {
try {
const res = await fetch(url, { headers: { 'Accept-Language': 'fa' } });
const data = await res.json();
if (data?.[0]) return [parseFloat(data[0].lat), parseFloat(data[0].lon)];
} catch {
/* تلاش بعدی */
}
if (attempt === 0) await new Promise(r => setTimeout(r, 900));
}
return null;
}
function MapClickHandler({ onPick }: { onPick: (lat: number, lng: number) => void }) {
@@ -467,8 +471,14 @@ function MapClickHandler({ onPick }: { onPick: (lat: number, lng: number) => voi
function MapController({ flyTarget }: { flyTarget: [number, number] | null }) {
const map = useMap();
// نقشهٔ تازه‌مانت‌شده ابعادش را نگرفته؛ flyTo بی‌اثر می‌ماند تا invalidateSize صدا زده شود.
useEffect(() => {
if (flyTarget) map.flyTo(flyTarget, 12, { duration: 1.2 });
map.invalidateSize();
}, [map]);
useEffect(() => {
if (!flyTarget) return;
map.invalidateSize();
map.flyTo(flyTarget, 12, { duration: 1.2 });
}, [flyTarget, map]);
return null;
}
+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 |
---
+1 -12
View File
@@ -713,7 +713,6 @@
"711": "Community 711",
"712": "Community 712",
"713": "Community 713",
"714": "Community 714",
"715": "Community 715",
"716": "Community 716",
"717": "Community 717",
@@ -727,8 +726,6 @@
"725": "Community 725",
"726": "Community 726",
"727": "Community 727",
"728": "Community 728",
"729": "Community 729",
"730": "Community 730",
"731": "Community 731",
"732": "Community 732",
@@ -736,15 +733,11 @@
"734": "Community 734",
"735": "Community 735",
"736": "Community 736",
"737": "Community 737",
"738": "Community 738",
"739": "Community 739",
"740": "Community 740",
"741": "Community 741",
"742": "Community 742",
"743": "Community 743",
"744": "Community 744",
"745": "Community 745",
"746": "Community 746",
"747": "Community 747",
"748": "Community 748",
@@ -757,19 +750,15 @@
"755": "Community 755",
"756": "Community 756",
"757": "Community 757",
"758": "Community 758",
"759": "Community 759",
"760": "Community 760",
"761": "Community 761",
"762": "Community 762",
"763": "Community 763",
"764": "Community 764",
"765": "Community 765",
"766": "Community 766",
"767": "Community 767",
"768": "Community 768",
"769": "Community 769",
"770": "Community 770",
"771": "Community 771",
"772": "Community 772"
"771": "Community 771"
}
+78 -137
View File
@@ -1,16 +1,16 @@
# Graph Report - clinicpro (2026-07-11)
## Corpus Check
- 766 files · ~564,895 words
- 767 files · ~566,250 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 9595 nodes · 13312 edges · 773 communities (614 shown, 159 thin omitted)
- 9614 nodes · 13338 edges · 762 communities (602 shown, 160 thin omitted)
- Extraction: 98% EXTRACTED · 2% INFERRED · 0% AMBIGUOUS · INFERRED: 294 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `0b49b03a`
- Built from commit: `3dcd4f3b`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -715,7 +715,6 @@
- [[_COMMUNITY_Community 711|Community 711]]
- [[_COMMUNITY_Community 712|Community 712]]
- [[_COMMUNITY_Community 713|Community 713]]
- [[_COMMUNITY_Community 714|Community 714]]
- [[_COMMUNITY_Community 715|Community 715]]
- [[_COMMUNITY_Community 716|Community 716]]
- [[_COMMUNITY_Community 717|Community 717]]
@@ -729,8 +728,6 @@
- [[_COMMUNITY_Community 725|Community 725]]
- [[_COMMUNITY_Community 726|Community 726]]
- [[_COMMUNITY_Community 727|Community 727]]
- [[_COMMUNITY_Community 728|Community 728]]
- [[_COMMUNITY_Community 729|Community 729]]
- [[_COMMUNITY_Community 730|Community 730]]
- [[_COMMUNITY_Community 731|Community 731]]
- [[_COMMUNITY_Community 732|Community 732]]
@@ -738,15 +735,11 @@
- [[_COMMUNITY_Community 734|Community 734]]
- [[_COMMUNITY_Community 735|Community 735]]
- [[_COMMUNITY_Community 736|Community 736]]
- [[_COMMUNITY_Community 737|Community 737]]
- [[_COMMUNITY_Community 738|Community 738]]
- [[_COMMUNITY_Community 739|Community 739]]
- [[_COMMUNITY_Community 740|Community 740]]
- [[_COMMUNITY_Community 741|Community 741]]
- [[_COMMUNITY_Community 742|Community 742]]
- [[_COMMUNITY_Community 743|Community 743]]
- [[_COMMUNITY_Community 744|Community 744]]
- [[_COMMUNITY_Community 745|Community 745]]
- [[_COMMUNITY_Community 746|Community 746]]
- [[_COMMUNITY_Community 747|Community 747]]
- [[_COMMUNITY_Community 748|Community 748]]
@@ -757,21 +750,17 @@
- [[_COMMUNITY_Community 753|Community 753]]
- [[_COMMUNITY_Community 754|Community 754]]
- [[_COMMUNITY_Community 757|Community 757]]
- [[_COMMUNITY_Community 758|Community 758]]
- [[_COMMUNITY_Community 759|Community 759]]
- [[_COMMUNITY_Community 760|Community 760]]
- [[_COMMUNITY_Community 761|Community 761]]
- [[_COMMUNITY_Community 762|Community 762]]
- [[_COMMUNITY_Community 763|Community 763]]
- [[_COMMUNITY_Community 764|Community 764]]
- [[_COMMUNITY_Community 765|Community 765]]
- [[_COMMUNITY_Community 766|Community 766]]
- [[_COMMUNITY_Community 767|Community 767]]
- [[_COMMUNITY_Community 768|Community 768]]
- [[_COMMUNITY_Community 769|Community 769]]
- [[_COMMUNITY_Community 770|Community 770]]
- [[_COMMUNITY_Community 771|Community 771]]
- [[_COMMUNITY_Community 772|Community 772]]
## God Nodes (most connected - your core abstractions)
1. `BaseController` - 86 edges
@@ -800,7 +789,7 @@
## Import Cycles
- None detected.
## Communities (773 total, 159 thin omitted)
## Communities (762 total, 160 thin omitted)
### Community 0 - "Community 0"
Cohesion: 0.05
@@ -827,12 +816,12 @@ Cohesion: 0.07
Nodes (3): UserProfile, self, User
### Community 6 - "Community 6"
Cohesion: 0.07
Nodes (19): SettlementController, FinancialBreakdown, FinancialBreakdownRepository, SettlementRepository, WalletTransactionRepository, CommissionService, Settlement, JsonResponse (+11 more)
Cohesion: 0.09
Nodes (15): SettlementController, SettlementRepository, WalletTransactionRepository, CommissionService, Settlement, JsonResponse, Request, User (+7 more)
### Community 7 - "Community 7"
Cohesion: 0.06
Nodes (8): DoctorService, Clinic, DoctorServiceRepository, Collection, Doctor, self, User, ManagerRegistry
Cohesion: 0.05
Nodes (11): DoctorServiceController, DoctorService, Clinic, DoctorServiceRepository, Collection, Doctor, self, User (+3 more)
### Community 8 - "Community 8"
Cohesion: 0.08
@@ -867,8 +856,8 @@ Cohesion: 0.25
Nodes (7): PaymentController, Appointment, JsonResponse, Payment, Request, Response, User
### Community 16 - "Community 16"
Cohesion: 0.06
Nodes (9): PatientSession, SmsWallet, AppLog, AppointmentExpiryService, Appointment, Collection, PatientRecord, self (+1 more)
Cohesion: 0.07
Nodes (7): PatientSession, SmsWallet, Appointment, Collection, PatientRecord, self, SessionService
### Community 17 - "Community 17"
Cohesion: 0.05
@@ -891,8 +880,8 @@ Cohesion: 0.03
Nodes (48): formatNumber(), ClinicAddress, ClinicDetailPage(), ClinicDoctorItem, ClinicInvitation, EditForm, editSchema, HUES_LIST (+40 more)
### Community 22 - "Community 22"
Cohesion: 0.11
Nodes (14): RatingController, Like, Rate, LikeRepository, RateRepository, JsonResponse, Request, User (+6 more)
Cohesion: 0.29
Nodes (4): RatingController, JsonResponse, Request, User
### Community 23 - "Community 23"
Cohesion: 0.05
@@ -1011,8 +1000,8 @@ Cohesion: 0.07
Nodes (26): الزامات UI, باگ‌فیکس صفحه نوبت‌ها, باگ ۱ — کرش تقویم, باگ ۲ — روز هفته در DateNavigator, باگ ۳ — پیام «slot نیست», باگ ۴ — نوبت جدید: نام اجباری + find-or-create patient, باگ ۵ — patient_mobile نشان می‌دهد موبایل پزشک, باگ ۶ — نوبت‌های رزرو شده در نمایش زمانبندی (+18 more)
### Community 53 - "Community 53"
Cohesion: 0.08
Nodes (18): AppLogRepository, ClaimItemRepository, DoctorClaimRequestRepository, DoctorInsuranceRepository, PreRegistrationRepository, SessionServiceRepository, SmsSettingsRepository, ServiceEntityRepository (+10 more)
Cohesion: 0.09
Nodes (15): AppLogRepository, ClaimItemRepository, DoctorClaimRequestRepository, DoctorInsuranceRepository, InvoiceRepository, PreRegistrationRepository, ServiceEntityRepository, ManagerRegistry (+7 more)
### Community 54 - "Community 54"
Cohesion: 0.10
@@ -1055,8 +1044,8 @@ Cohesion: 0.05
Nodes (50): FreeVisitPrice(), Pricing, cn(), formatDate(), formatDateTime(), formatRial(), iranMobileOptionalSchema, iranMobileSchema (+42 more)
### Community 64 - "Community 64"
Cohesion: 0.29
Nodes (4): SmsWalletController, JsonResponse, Request, User
Cohesion: 0.19
Nodes (7): SmsWalletController, SmsSettingsRepository, SmsSettings, JsonResponse, Request, User, ManagerRegistry
### Community 65 - "Community 65"
Cohesion: 0.08
@@ -1127,8 +1116,8 @@ Cohesion: 0.07
Nodes (30): devDependencies, @babel/core, @babel/preset-env, @babel/preset-react, @babel/preset-typescript, core-js, @csstools/postcss-oklab-function, @hotwired/stimulus (+22 more)
### Community 86 - "Community 86"
Cohesion: 0.04
Nodes (18): AppointmentExpiryServiceTest, LowTierFixesTest, SendCodeMobileRateLimitTest, ClaimsListPaginationTest, CaptchaFlowTest, ServiceItemDeleteCleanupTest, ServiceItemStaffOwnershipTest, TenantInsuranceCleanupTest (+10 more)
Cohesion: 0.06
Nodes (16): AppointmentExpiryServiceTest, LowTierFixesTest, SendCodeMobileRateLimitTest, ClaimsListPaginationTest, CaptchaFlowTest, ServiceItemStaffOwnershipTest, EntityManagerInterface, KernelBrowser (+8 more)
### Community 87 - "Community 87"
Cohesion: 0.10
@@ -1215,8 +1204,8 @@ Cohesion: 0.33
Nodes (6): AppointmentController, Appointment, Doctor, JsonResponse, Request, User
### Community 108 - "Community 108"
Cohesion: 0.13
Nodes (10): BaseController, CategoryController, CategoryImportController, SiteContextController, JsonResponse, JsonResponse, Request, JsonResponse (+2 more)
Cohesion: 0.11
Nodes (13): CaptchaController, BaseController, CategoryController, DoctorImportController, SiteContextController, JsonResponse, JsonResponse, Request (+5 more)
### Community 109 - "Community 109"
Cohesion: 0.29
@@ -1315,8 +1304,8 @@ Cohesion: 0.12
Nodes (16): Endpoint ها, GET /api/v1/representation/filter/{id}, GET /api/v1/representation/filter/{representationId}, GET /api/v1/representation/my-appointments/{id}, GET /api/v1/representation/{uuid}, GET /api/v1/representation/yearly-income/{id}, GET /api/v1/representation/yearly-income/{representationId}, POST /api/v1/representations/{id}/bank-accounts (+8 more)
### Community 135 - "Community 135"
Cohesion: 0.09
Nodes (22): Appointment Settings API, Available Locations, Day Index Convention, DELETE `/api/v1/booking-setting/{uuid}`, Errors, Errors, Errors, Errors (+14 more)
Cohesion: 0.12
Nodes (16): Day Index Convention, DELETE `/api/v1/booking-setting/{uuid}`, Errors, Errors, Errors, Errors, GET `/api/v1/appointment-settings/weekly-schedule/{uuid}`, PATCH `/api/v1/appointment-settings/weekly-schedule/{uuid}` (+8 more)
### Community 136 - "Community 136"
Cohesion: 0.12
@@ -1447,8 +1436,8 @@ Cohesion: 0.10
Nodes (20): api.ir (استعلام هویت — Shahkar / IbanMatch), اتصال به دیتابیس‌های مستقل (الزامی), اسرار (الزامی — قبل از اولین دیپلوی), امنیت و منابع, بررسی سلامت, دامنه‌ها و CORS, دیپلوی‌های بعدی, راهنمای دیپلوی ClinicPro (Coolify + Docker Compose) (+12 more)
### Community 169 - "Community 169"
Cohesion: 0.13
Nodes (7): EntityInsurancePricing, EntityInsurancePricingRepository, TenantServiceCoverageRepository, TenantInsuranceCleanupService, ManagerRegistry, ManagerRegistry, TenantServiceCoverage
Cohesion: 0.15
Nodes (5): EntityInsurancePricing, TenantInsuranceCleanupTest, EntityInsurancePricingRepository, TenantInsuranceCleanupService, ManagerRegistry
### Community 170 - "Community 170"
Cohesion: 0.13
@@ -1582,9 +1571,13 @@ Nodes (12): Clinic Services API, DELETE /api/v1/service-item/{uuid}, DELETE /api
Cohesion: 0.21
Nodes (6): AuthenticationException, ExceptionSubscriber, SecurityHeadersSubscriber, EventSubscriberInterface, ExceptionEvent, ResponseEvent
### Community 205 - "Community 205"
Cohesion: 0.21
Nodes (4): SeedDemoDataCommand, InputInterface, OutputInterface, SymfonyStyle
### Community 206 - "Community 206"
Cohesion: 0.06
Nodes (17): GatewayFactory, MellatGateway, MockGateway, SepGateway, MellatGateway, SepGateway, SoapClient, PaymentGatewayInterface (+9 more)
Cohesion: 0.08
Nodes (14): MellatGateway, MockGateway, SepGateway, SoapClient, PaymentGatewayInterface, PaymentInitResult, PaymentRefundResult, PaymentVerifyResult (+6 more)
### Community 207 - "Community 207"
Cohesion: 0.15
@@ -1835,8 +1828,8 @@ Cohesion: 0.17
Nodes (5): Authentication, ClinicPro — API Documentation Index, Error Code Reference, Modules, Standard Response Envelope
### Community 275 - "Community 275"
Cohesion: 0.19
Nodes (5): MellatGatewayTest, ErrorCodesTest, KavehNegarProviderTest, TestCase, KavehNegarProvider
Cohesion: 0.17
Nodes (6): MellatGateway, MellatGatewayTest, ErrorCodesTest, KavehNegarProviderTest, TestCase, KavehNegarProvider
### Community 276 - "Community 276"
Cohesion: 0.20
@@ -1947,8 +1940,8 @@ Cohesion: 0.12
Nodes (16): آماده‌سازی پروژه ClinicPro برای دیپلوی روی Coolify با Docker Compose, زمینه, فایل‌های مرتبط, نکات مهم (محدودیت‌ها و edge caseها), هدف, وظایف, ۱. ساخت `Dockerfile` چندمرحله‌ای, ۱۰. ساخت راهنمای `docs/deploy/coolify.md` (+8 more)
### Community 304 - "Community 304"
Cohesion: 0.22
Nodes (9): 30. 🟢 `GET` get 🆕, 33. 🔵 `POST` image_clinic, 36. 🟢 `GET` get my rate, 6. کلینیک (Clinic), هدرهای اضافی, هدرهای اضافی, پاسخ‌ها, پاسخ‌ها (+1 more)
Cohesion: 0.05
Nodes (42): 29. 🟢 `GET` clinic list 🆕, 30. 🟢 `GET` get 🆕, 33. 🔵 `POST` image_clinic, 34. 🔵 `POST` image logo, 36. 🟢 `GET` get my rate, 37. 🔵 `POST` post, 39. 🟡 `PATCH` Comment confirmation, 40. 🔵 `POST` post (+34 more)
### Community 306 - "Community 306"
Cohesion: 0.07
@@ -2047,8 +2040,8 @@ Cohesion: 0.22
Nodes (8): Query های جدید, بیماران منحصربه‌فرد در بازه, درآمد بر اساس روز (از patient_sessions), فروش اشتراک بر اساس پنل (admin), نوبت‌ها بر اساس روز (admin chart), نکات مهم, هیچ migration لازم نیست, پایگاه داده — تسک ۱۶: داشبورد هوشمند
### Community 333 - "Community 333"
Cohesion: 0.25
Nodes (7): DELETE `/api/v1/clinic-pro/doctor-address/{id}`, Doctor API, Errors, GET `/api/v1/doctors`, Query Parameters, Response `200`, Response `200`
Cohesion: 0.05
Nodes (39): DELETE `/api/v1/clinic-pro/doctor-address/{id}`, DELETE `/api/v1/doctor/{uuid}`, Doctor API, Errors, Errors, Errors, Errors, Errors (+31 more)
### Community 334 - "Community 334"
Cohesion: 0.25
@@ -2294,6 +2287,10 @@ Nodes (6): ClaimAmountBoundsTest, ClaimsListNPlusOneTest, ClaimItem, Claim, Doct
Cohesion: 0.22
Nodes (7): initiate(), refund(), reverse(), verify(), PaymentInitResult, PaymentRefundResult, PaymentVerifyResult
### Community 399 - "Community 399"
Cohesion: 0.23
Nodes (5): AbstractMigration, Schema, Version20260609130407, Schema, Version20260610175105
### Community 401 - "Community 401"
Cohesion: 0.12
Nodes (15): `lib/utils.ts`, `SettingsPage.tsx` (ورودی‌ها ریال ذخیره می‌شوند), زمینه, فایل‌های مرتبط, مشکل / هدف, نمونهٔ نمایش (Subscription), نکات مهم, واحد پول = تومان در پنل ادمین (نمایش ÷۱۰ / ورودی ×۱۰) — ذخیره و درگاه ریال می‌ماند (+7 more)
@@ -2303,8 +2300,8 @@ Cohesion: 0.11
Nodes (17): بازطراحی معماری پرداخت — سرویس‌محور، امن، توسعه‌پذیر (Backend), تست دستی (ddev، در حالت `payment_test_mode=1`), خروجی نهایی (طبق spec — در گزارش اجرا ارائه شود), زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی (+9 more)
### Community 407 - "Community 407"
Cohesion: 0.26
Nodes (7): MessageBusInterface, MockObject, SmsServiceLookupOnlyTest, SmsLogRepository, SmsMessageTemplateRepository, SmsTextResolver, KavehNegarProvider
Cohesion: 0.27
Nodes (8): MessageBusInterface, MockObject, SmsServiceLookupOnlyTest, SmsLogRepository, SmsMessageTemplateRepository, SmsService, SmsTextResolver, KavehNegarProvider
### Community 414 - "Community 414"
Cohesion: 0.10
@@ -2314,9 +2311,9 @@ Nodes (20): edge cases, خلاصهٔ خطاها و اولویت, راه‌حل,
Cohesion: 0.17
Nodes (11): دیپلوی ClinicPro روی Coolify (Docker Compose), رفع اشکال, مراحل دیپلوی, معماری دیپلوی, نکات عملیاتی, چند دامنه فرانت‌اند (مهم), ۱. ساخت Resource در Coolify, ۲. اختصاص دامنه (+3 more)
### Community 421 - "Community 421"
Cohesion: 0.23
Nodes (5): AbstractMigration, Schema, Version20260609132009, Schema, Version20260611075829
### Community 422 - "Community 422"
Cohesion: 0.12
Nodes (15): راه‌حل, راه‌حل, راه‌حل, رفع زوم نقشه در افزودن آدرس + کپچا و موبایل در claim + حذف پروفایل توسط مالک, ریشه‌ها, زمینه, فایل‌های مرتبط, نکات مهم (+7 more)
### Community 424 - "Community 424"
Cohesion: 0.33
@@ -2335,8 +2332,8 @@ Cohesion: 0.35
Nodes (5): PatientController, JsonResponse, PatientSession, Request, User
### Community 435 - "Community 435"
Cohesion: 0.16
Nodes (10): Command, CancelExpiredAppointmentsCommand, PruneLogsCommand, PurgeDoctorsCommand, InputInterface, OutputInterface, InputInterface, OutputInterface (+2 more)
Cohesion: 0.18
Nodes (10): Command, CancelExpiredAppointmentsCommand, PruneLogsCommand, SeedSmsMessageTemplatesCommand, InputInterface, OutputInterface, InputInterface, OutputInterface (+2 more)
### Community 439 - "Community 439"
Cohesion: 0.17
@@ -2447,8 +2444,8 @@ Cohesion: 0.43
Nodes (5): BeforeInstallPromptEvent, usePwaInstall(), PwaInstallBanner(), detectIOS(), PwaLoginCard()
### Community 483 - "Community 483"
Cohesion: 0.43
Nodes (3): InvoiceRepository, Invoice, ManagerRegistry
Cohesion: 0.33
Nodes (5): Like, LikeRepository, Comment, ManagerRegistry, User
### Community 487 - "Community 487"
Cohesion: 0.40
@@ -2459,13 +2456,17 @@ Cohesion: 0.40
Nodes (5): Admin Endpoints, GET /api/v1/admin/sms/settings/review, GET /api/v1/admin/sms/wallet-report, POST /api/v1/admin/sms/settings/{id}/approve, POST /api/v1/admin/sms/settings/{id}/reject
### Community 490 - "Community 490"
Cohesion: 0.36
Nodes (3): DoctorServiceController, JsonResponse, Request
Cohesion: 0.38
Nodes (5): Rate, RateRepository, Doctor, ManagerRegistry, User
### Community 491 - "Community 491"
Cohesion: 0.12
Nodes (15): `Modal.tsx` (بدون Portal), `PersianCalendar.tsx` (buttonها بدون `type`) — نمونه‌ها, باگ ۱ — علت, باگ ۲ — علت, رفع دو باگ Modal و تقویم شمسی در پنل ادمین, زمینه, فایل‌های مرتبط, مشکل / هدف (+7 more)
### Community 492 - "Community 492"
Cohesion: 0.18
Nodes (4): KavehNegarProvider, SmsService, SendSmsMessage, SmsProviderInterface
### Community 493 - "Community 493"
Cohesion: 0.20
Nodes (9): Backend Audit Backlog — ClinicPro, ☐ CRITICAL, ✅ DONE (committed on backend-audit), ☐ EPICS (design debt — cross-repo, defer; do NOT quick-fix), ☐ HIGH, ☐ LOW, ☐ MEDIUM, Progress (this audit session) (+1 more)
@@ -2478,10 +2479,6 @@ Nodes (3): InvoiceItemRepository, InvoiceItem, ManagerRegistry
Cohesion: 0.12
Nodes (15): CORS — `config/packages/nelmio_cors.yaml`, MellatGateway — تشخیص فعال‌بودن, payment/config — `PaymentController::config()` (خط ۵۴۶), resolveGateway + callback (کد واقعی), رفع باگ‌های پروداکشن: CORS + payment/config 500 + درگاه‌های فعال و callback ملت, زمینه, فایل‌های مرتبط, فرانت — `SubscriptionPage.tsx` (+7 more)
### Community 496 - "Community 496"
Cohesion: 0.40
Nodes (5): Errors, PATCH `/api/v1/clinic-pro/doctor-address/{id}`, Path Parameters, Request Body, Response `200`
### Community 497 - "Community 497"
Cohesion: 0.53
Nodes (3): SlotUniquenessTest, Appointment, Doctor
@@ -2775,8 +2772,8 @@ Cohesion: 0.50
Nodes (3): Entity: Payment, ساختار فایل‌ها, معماری — تسک ۱۵: ماژول پرداخت
### Community 595 - "Community 595"
Cohesion: 0.36
Nodes (4): SmsLogRepository, SmsLog, SmsService, ManagerRegistry
Cohesion: 0.43
Nodes (3): SmsLogRepository, SmsLog, ManagerRegistry
### Community 596 - "Community 596"
Cohesion: 0.29
@@ -2814,6 +2811,10 @@ Nodes (5): RepresentationController, JsonResponse, Representation, Request, User
Cohesion: 0.67
Nodes (3): بک‌اند, فرانت‌اند, وضعیت فعلی کد (مهم — قبل از تغییر بخوان)
### Community 618 - "Community 618"
Cohesion: 0.16
Nodes (5): ServiceItemDeleteCleanupTest, ServiceCoverageNPlusOneTest, TenantServiceCoverageRepository, ManagerRegistry, TenantServiceCoverage
### Community 625 - "Community 625"
Cohesion: 0.40
Nodes (5): 31. 🟡 `PATCH` patch, Request Body, مثال Request, هدرهای اضافی, پاسخ‌ها
@@ -2823,8 +2824,8 @@ Cohesion: 0.23
Nodes (7): Money, BillingCalculator, InvoiceService, CoverageRule, ShareBreakdown, Invoice, PatientSession
### Community 631 - "Community 631"
Cohesion: 0.32
Nodes (3): SmsService, SendSmsMessage, SmsProviderInterface
Cohesion: 0.39
Nodes (4): FinancialBreakdown, FinancialBreakdownRepository, ManagerRegistry, Payment
### Community 632 - "Community 632"
Cohesion: 0.40
@@ -2919,8 +2920,8 @@ Cohesion: 0.40
Nodes (5): addMinutes(), calcSlotCount(), hasOverlap(), parseMinutes(), SessionEditor()
### Community 677 - "Community 677"
Cohesion: 0.40
Nodes (5): 37. 🔵 `POST` post, Request Body, مثال Request, هدرهای اضافی, پاسخ‌ها
Cohesion: 0.29
Nodes (6): Appointment Settings API, Available Locations, Errors, `GET /api/v1/appointment-settings/available-locations/{doctorUuid}`, Response `200`, Slot Calculation Logic (Reference)
### Community 678 - "Community 678"
Cohesion: 0.53
@@ -2939,25 +2940,21 @@ Cohesion: 0.40
Nodes (5): GET `/api/v1/admin/comments`, GET `/api/v1/admin/rates`, Query Parameters, Query Parameters, Rating & Comment Management
### Community 687 - "Community 687"
Cohesion: 0.50
Nodes (4): DELETE `/api/v1/doctor/{uuid}`, Errors, Path Parameters, Response `200`
Cohesion: 0.38
Nodes (3): PurgeDoctorsCommand, InputInterface, OutputInterface
### Community 689 - "Community 689"
Cohesion: 0.40
Nodes (5): 41. 🟡 `PATCH` patch, Request Body, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 690 - "Community 690"
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/doctor`, Request Body (`application/json`), Response `201`
Cohesion: 0.43
Nodes (3): CategoryImportController, JsonResponse, Request
### Community 692 - "Community 692"
Cohesion: 0.50
Nodes (4): GET `/api/v1/admin/representations`, Query Parameters, Representation Management, Response `200`
### Community 693 - "Community 693"
Cohesion: 0.50
Nodes (4): Errors, GET `/api/v1/doctor/{uuid}`, Path Parameters, Response `200`
### Community 694 - "Community 694"
Cohesion: 0.50
Nodes (4): Error Codes, POST `/api/v1/user/reset-password`, Request Body, Response `200`
@@ -2975,8 +2972,8 @@ Cohesion: 0.17
Nodes (11): رفع خطاهای ارسال پیامک کاوه‌نگار در سرور prod (431 + Idle timeout), زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, پروژه (+3 more)
### Community 700 - "Community 700"
Cohesion: 0.50
Nodes (4): Errors, POST `/file/upload/clinic_pro/doctor/field_image`, Request, Response `200`
Cohesion: 0.53
Nodes (3): SessionServiceRepository, ManagerRegistry, SessionService
### Community 701 - "Community 701"
Cohesion: 0.17
@@ -2986,13 +2983,9 @@ Nodes (11): رفع خطای `Class "SoapClient" not found` در پرداخت م
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/user/send-code`, Request Body, Response `200`
### Community 705 - "Community 705"
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/clinic-pro/doctor-address`, Request Body, Response `201`
### Community 706 - "Community 706"
Cohesion: 0.50
Nodes (4): Errors, Path Parameters, POST `/api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}`, Response `201`
Cohesion: 0.40
Nodes (5): Errors, GET `/api/v1/admin/clinic/{uuid}/invitations`, Path Parameters, Query Parameters, Response `200`
### Community 707 - "Community 707"
Cohesion: 0.50
@@ -3006,18 +2999,6 @@ Nodes (4): Errors, POST `/api/v1/user/login`, Request Body, Response `200`
Cohesion: 0.50
Nodes (4): Errors, POST `/oauth/token`, Request Body, Response `200`
### Community 712 - "Community 712"
Cohesion: 0.67
Nodes (3): GET `/api/v1/clinic-pro/doctor-addresses/{doctorId}`, Path Parameters, Response `200`
### Community 713 - "Community 713"
Cohesion: 0.50
Nodes (4): 39. 🟡 `PATCH` Comment confirmation, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 714 - "Community 714"
Cohesion: 0.50
Nodes (4): 45. 🔵 `POST` post, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 716 - "Community 716"
Cohesion: 0.50
Nodes (4): Errors, GET `/oauth/userinfo`, Headers, Response `200`
@@ -3062,14 +3043,6 @@ Nodes (4): Errors, GET `/api/v1/representation/doctors`, Query Parameters, Respo
Cohesion: 0.50
Nodes (4): 38. 🟢 `GET` Unapproved comments, هدرهای اضافی, پارامترهای Query, پاسخ‌ها
### Community 728 - "Community 728"
Cohesion: 0.50
Nodes (4): 40. 🔵 `POST` post, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 729 - "Community 729"
Cohesion: 0.50
Nodes (4): 44. 🟢 `GET` list comment, هدرهای اضافی, پارامترهای Query, پاسخ‌ها
### Community 730 - "Community 730"
Cohesion: 0.50
Nodes (4): 46. 🟡 `PATCH` patch, مثال Request, هدرهای اضافی, پاسخ‌ها
@@ -3098,10 +3071,6 @@ Nodes (3): POST `/api/v1/admin/insurance/{id}/upload-logo`, Request, Response `2
Cohesion: 0.67
Nodes (3): GET `/api/v1/representation/{uuid}/dashboard/yearly`, Query Parameters, Response `200`
### Community 737 - "Community 737"
Cohesion: 0.67
Nodes (3): 34. 🔵 `POST` image logo, هدرهای اضافی, پاسخ‌ها
### Community 738 - "Community 738"
Cohesion: 0.50
Nodes (4): Errors, GET `/api/v1/representation/{uuid}`, Path Parameters, Response `200`
@@ -3110,14 +3079,6 @@ Nodes (4): Errors, GET `/api/v1/representation/{uuid}`, Path Parameters, Respons
Cohesion: 0.38
Nodes (4): DoctorClaimController, JsonResponse, Request, User
### Community 740 - "Community 740"
Cohesion: 0.67
Nodes (3): 42. 🔴 `DELETE` delete, هدرهای اضافی, پاسخ‌ها
### Community 741 - "Community 741"
Cohesion: 0.67
Nodes (3): 43. 🟢 `GET` get, هدرهای اضافی, پاسخ‌ها
### Community 744 - "Community 744"
Cohesion: 0.36
Nodes (4): DoctorImportResult, DoctorImportService, Collection, User
@@ -3130,10 +3091,6 @@ Nodes (3): BackfillSurrogateRoleCommand, InputInterface, OutputInterface
Cohesion: 0.38
Nodes (3): CreateAdminCommand, InputInterface, OutputInterface
### Community 751 - "Community 751"
Cohesion: 0.25
Nodes (5): HealthController, EntityManagerInterface, InputInterface, OutputInterface, JsonResponse
### Community 753 - "Community 753"
Cohesion: 0.38
Nodes (3): FixIrimcNamesCommand, InputInterface, OutputInterface
@@ -3142,10 +3099,6 @@ Nodes (3): FixIrimcNamesCommand, InputInterface, OutputInterface
Cohesion: 0.38
Nodes (3): SystemOwnerCommand, InputInterface, OutputInterface
### Community 758 - "Community 758"
Cohesion: 0.43
Nodes (4): DoctorImportController, JsonResponse, Request, User
### Community 759 - "Community 759"
Cohesion: 0.38
Nodes (3): RepositoryClassMappingTest, KernelTestCase, DbLoggerTest
@@ -3154,10 +3107,6 @@ Nodes (3): RepositoryClassMappingTest, KernelTestCase, DbLoggerTest
Cohesion: 0.47
Nodes (3): SeedCategoriesCommand, InputInterface, OutputInterface
### Community 762 - "Community 762"
Cohesion: 0.47
Nodes (3): SeedSmsMessageTemplatesCommand, InputInterface, OutputInterface
### Community 763 - "Community 763"
Cohesion: 0.33
Nodes (6): GET /api/v1/sms/balance — موجودی حساب پیامک, POST /api/v1/sms/queue — افزودن به صف, ۴. سیستم حساب و صف پیامک, ۴.۱ حساب پیامک (SMS Account), ۴.۲ صف پیامک (SMS Queue), ۴.۳ API پیامک
@@ -3166,10 +3115,6 @@ Nodes (6): GET /api/v1/sms/balance — موجودی حساب پیامک, POST /a
Cohesion: 0.53
Nodes (3): PaymentLog, PaymentLogRepository, ManagerRegistry
### Community 765 - "Community 765"
Cohesion: 0.40
Nodes (5): Errors, Path Parameters, POST `/api/v1/doctor/invitation/{invUuid}/respond`, Request Body, Response `200`
### Community 766 - "Community 766"
Cohesion: 0.40
Nodes (5): ۲.۸ تنظیمات نوبت (Appointment Settings), ۲.۸.۱ برنامه هفتگی (Weekly Schedule), ۲.۸.۲ تعطیلات (Holidays), ۲.۸.۳ لغو تعطیل (Date Override), ۲.۸.۴ الگوریتم محاسبه اسلات‌های خالی
@@ -3190,26 +3135,22 @@ Nodes (4): ۲.۱۳ نظرات، لایک و امتیازدهی, ۲.۱۳.۱ نظ
Cohesion: 0.67
Nodes (3): POST `/api/v1/admin/sms/template/{uuid}/approve`, Request Body (`application/json`), Response `200`
### Community 772 - "Community 772"
Cohesion: 0.67
Nodes (3): 29. 🟢 `GET` clinic list 🆕, پارامترهای Query, پاسخ‌ها
## Knowledge Gaps
- **4146 isolated node(s):** `ALLOWED_ROLES`, `Pricing`, `ImageCropModalProps`, `TenantInsurance`, `CoverageRow` (+4141 more)
- **4157 isolated node(s):** `ALLOWED_ROLES`, `Pricing`, `ImageCropModalProps`, `TenantInsurance`, `CoverageRow` (+4152 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **159 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **160 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
## Suggested Questions
_Questions this graph is uniquely positioned to answer:_
- **Why does `Altcha` connect `Community 367` to `Community 0`, `Community 485`?**
_High betweenness centrality (0.065) - this node is a cross-community bridge._
- **Why does `BaseController` connect `Community 108` to `Community 6`, `Community 138`, `Community 139`, `Community 14`, `Community 654`, `Community 655`, `Community 15`, `Community 20`, `Community 22`, `Community 26`, `Community 164`, `Community 295`, `Community 424`, `Community 300`, `Community 433`, `Community 308`, `Community 58`, `Community 59`, `Community 318`, `Community 64`, `Community 577`, `Community 75`, `Community 77`, `Community 607`, `Community 480`, `Community 739`, `Community 230`, `Community 104`, `Community 745`, `Community 490`, `Community 107`, `Community 109`, `Community 499`, `Community 252`, `Community 758`, `Community 121`, `Community 122`, `Community 380`?**
- **Why does `BaseController` connect `Community 108` to `Community 6`, `Community 7`, `Community 138`, `Community 139`, `Community 14`, `Community 654`, `Community 655`, `Community 15`, `Community 20`, `Community 22`, `Community 26`, `Community 164`, `Community 295`, `Community 424`, `Community 300`, `Community 433`, `Community 690`, `Community 308`, `Community 58`, `Community 59`, `Community 318`, `Community 64`, `Community 577`, `Community 75`, `Community 77`, `Community 607`, `Community 480`, `Community 739`, `Community 230`, `Community 104`, `Community 107`, `Community 109`, `Community 499`, `Community 252`, `Community 121`, `Community 122`, `Community 380`?**
_High betweenness centrality (0.038) - this node is a cross-community bridge._
- **Why does `Version20260705070546` connect `Community 646` to `Community 421`?**
- **Why does `Version20260705070546` connect `Community 646` to `Community 399`?**
_High betweenness centrality (0.024) - this node is a cross-community bridge._
- **What connects `ALLOWED_ROLES`, `Pricing`, `ImageCropModalProps` to the rest of the system?**
_4146 weakly-connected nodes found - possible documentation gaps or missing edges._
_4157 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `Community 0` be split into smaller, more focused modules?**
_Cohesion score 0.05075187969924812 - nodes in this community are weakly interconnected._
- **Should `Community 1` be split into smaller, more focused modules?**
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+847 -389
View File
File diff suppressed because it is too large Load Diff
+21 -16
View File
@@ -230,8 +230,8 @@
"semantic_hash": ""
},
"assets/admin/pages/DoctorDetailPage.tsx": {
"mtime": 1783189217.065477,
"ast_hash": "d10e0ec287b6d42011d384e3e8d8583b",
"mtime": 1783768892.4257572,
"ast_hash": "9e14c9cc70e4707bddaab0a5267e54c0",
"semantic_hash": ""
},
"assets/admin/pages/DoctorFormPage.tsx": {
@@ -1180,8 +1180,8 @@
"semantic_hash": ""
},
"src/Doctor/Controller/DoctorController.php": {
"mtime": 1783747336.6328127,
"ast_hash": "7828b41ebfdf61e887bf38d360c8a8e1",
"mtime": 1783769018.613043,
"ast_hash": "57c2d4ba148aa5084683dbfd08f8e2b0",
"semantic_hash": ""
},
"src/Doctor/Entity/Doctor.php": {
@@ -2395,8 +2395,8 @@
"semantic_hash": ""
},
"docs/api/doctor.md": {
"mtime": 1783757329.0750217,
"ast_hash": "cb33dfc5636d36fdb6c98402cb789340",
"mtime": 1783769157.105152,
"ast_hash": "572576e30db5b049cb62ba8d54bdf8a5",
"semantic_hash": ""
},
"docs/api/insurance.md": {
@@ -3900,8 +3900,8 @@
"semantic_hash": ""
},
"src/Doctor/Controller/DoctorClaimController.php": {
"mtime": 1783757329.0908747,
"ast_hash": "ed52d6bb87506e44be9f86adffee5645",
"mtime": 1783768981.0086484,
"ast_hash": "997576824b9039e977fd58efb417cf70",
"semantic_hash": ""
},
"src/Doctor/Controller/DoctorImportController.php": {
@@ -3930,8 +3930,8 @@
"semantic_hash": ""
},
"src/Doctor/Service/DoctorImportService.php": {
"mtime": 1783765876.696864,
"ast_hash": "56bacf3ad261680191151c347f8cd411",
"mtime": 1783766986.8738186,
"ast_hash": "a675ebe7e1acac7da14cc8de3364757e",
"semantic_hash": ""
},
"src/Shared/Util/PersianText.php": {
@@ -3940,13 +3940,13 @@
"semantic_hash": ""
},
"tests/Doctor/DoctorClaimTest.php": {
"mtime": 1783757329.09223,
"ast_hash": "5973aa00e5a19406cd79f93be5455b76",
"mtime": 1783769087.937134,
"ast_hash": "50ea89c2d246faf98dc843152d51a0c5",
"semantic_hash": ""
},
"tests/Doctor/DoctorImportTest.php": {
"mtime": 1783765976.698628,
"ast_hash": "1d99f84ff5b37dacd93000fa8562f994",
"mtime": 1783766996.494613,
"ast_hash": "997b8d698c8eb4bbf69652bdcead514d",
"semantic_hash": ""
},
"tests/Shared/PersianTextTest.php": {
@@ -3960,8 +3960,8 @@
"semantic_hash": ""
},
"docs/api/doctor-claim.md": {
"mtime": 1783757329.090325,
"ast_hash": "1cc55e0c447ec1d4176adc0700b8da54",
"mtime": 1783769130.2951994,
"ast_hash": "2f4e095e4e0cac673062cc48b0577d5d",
"semantic_hash": ""
},
"docs/scenarios/climed.md": {
@@ -3993,5 +3993,10 @@
"mtime": 1783765695.1482882,
"ast_hash": "b03ab1640dc46aa51114fbc3eba51870",
"semantic_hash": ""
},
".claude/prompt/doctor-map-claim-captcha-delete.md": {
"mtime": 1783768547.5967684,
"ast_hash": "d4531629fe5bf2befc994f1a3f2a34ff",
"semantic_hash": ""
}
}
@@ -24,6 +24,7 @@ class DoctorClaimController extends BaseController
private readonly DoctorClaimService $claimService,
private readonly RateLimiterFactory $doctorClaimLimiter,
private readonly \App\Doctor\Repository\DoctorClaimRequestRepository $claimRepo,
private readonly \App\Shared\Captcha\CaptchaGuard $captcha,
) {}
#[OA\Get(
@@ -82,6 +83,9 @@ class DoctorClaimController extends BaseController
throw new TooManyRequestsHttpException($limit->getRetryAfter()->getTimestamp() - time());
}
// کپچای ALTCHA (در dev با ALTCHA_ENABLED=false بی‌اثر) — خطا → ERR_CAPTCHA_001 (422)
$this->captcha->assertValid($request);
$doctor = $this->doctorRepo->findByUuid($uuid);
if ($doctor === null) {
return $this->error(ErrorCodes::ERR_NOT_FOUND_001, 'پزشک یافت نشد', 404);
@@ -92,7 +96,11 @@ class DoctorClaimController extends BaseController
$birthDate = trim(\App\Shared\Util\PersianText::normalize((string) ($data['birth_date'] ?? '')));
$firstName = trim((string) ($data['first_name'] ?? ''));
$lastName = trim((string) ($data['last_name'] ?? ''));
$mobile = preg_replace('/\D/', '', \App\Shared\Util\PersianText::normalize((string) ($data['mobile'] ?? '')));
if ($mobile !== '' && $mobile !== $user->getMobileNumber()) {
return $this->error(ErrorCodes::ERR_CONFLICT_001, 'شماره موبایل باید با حساب کاربری شما یکی باشد', 422, 'mobile');
}
if (!preg_match('/^\d{10}$/', $nationalCode)) {
return $this->error(ErrorCodes::ERR_VALIDATION_001, 'کد ملی نامعتبر است', 422, 'national_code');
}
+8 -2
View File
@@ -368,14 +368,20 @@ class DoctorController extends BaseController
]
)]
#[Route('/api/v1/doctor/{uuid}', methods: ['DELETE'])]
#[IsGranted('ROLE_ADMIN')]
public function delete(string $uuid): JsonResponse
#[IsGranted('IS_AUTHENTICATED_FULLY')]
public function delete(string $uuid, #[CurrentUser] User $user): JsonResponse
{
$doctor = $this->doctorRepo->findByUuid($uuid);
if ($doctor === null) {
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
}
// ادمین هر پروفایلی را؛ مالک فقط پروفایلِ claimedِ خودش را حذف می‌کند (ضد IDOR)
$isOwner = $doctor->getOwnerStatus() === 'claimed' && $doctor->getUser()->getId() === $user->getId();
if (!$user->hasRole('ROLE_ADMIN') && !$isOwner) {
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'اجازهٔ حذف این پروفایل را ندارید', 403);
}
if ($this->appointmentRepo->count(['doctor' => $doctor]) > 0) {
return $this->error(ErrorCodes::ERR_CONFLICT_001, 'این پزشک نوبت ثبت‌شده دارد و قابل حذف نیست', 409);
}
+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();