feat(migrations): add ownership fields to doctors table for IRIMC import

- Introduced new columns: owner_status, source, source_ref, managed_by, and claimed_at to the doctors table.
- Created indexes for owner_status and source to optimize queries related to unclaimed doctors.

feat(auth): implement SystemOwnerCommand for managing system-owner user

- Added command to create, activate, and deactivate a system-owner user for IRIMC crawler.
- Ensured the user has ROLE_ADMIN to access import endpoints.
- Handled password setting and user status management within the command.
This commit is contained in:
hamed
2026-07-11 08:59:36 +03:30
parent f30bf5dfbd
commit 9f56f4aa08
28 changed files with 3694 additions and 989 deletions
@@ -0,0 +1,231 @@
# رفع خطاهای لاگ سرور (production) — ۱۴۰۵/۰۴/۲۰
## پروژه
`clinicpro` (backend فقط)
## زمینه
لاگ ارور production (فایل `logs-20260711-083844.csv`) بررسی شد. پنج دسته خطا شناسایی شد. بیشترشان error سطح ۵۰۰ هستند که باید یا رفع شوند یا از سطح error خارج شوند تا لاگ کثیف نشود. ریشهٔ هر کدام در کد پیدا شده و در ادامه با راه‌حل دقیق آمده است.
## خلاصهٔ خطاها و اولویت
| # | خطا | تعداد در لاگ | ریشه | نوع |
|---|-----|------|------|-----|
| ۱ | `SMS sendTemplate failed (kavenegar): HTTP 431 Request Header Fields Too Large` | ۲ | توکن دعوت ۹۶ کاراکتری + URL کامل به‌عنوان توکن کاوه‌نگار | باگ کد |
| ۲ | `Payment initiate failed (mellat): Class "SoapClient" not found` | ۱ | ایمیج prod قدیمی؛ افزونهٔ soap نصب نیست | deploy + گارد کد |
| ۳ | `ForeignKeyConstraintViolationException` هنگام حذف پزشک | ۱ | حذف پزشک بدون بررسی نوبت‌های وابسته → ۵۰۰ | باگ کد |
| ۴ | `MethodNotAllowedHttpException: POST/OPTIONS https://clinic-pro.ir/` | ~۳۰ | ربات/اسکنر روی `/`؛ در fallback عمومی error+۵۰۰ لاگ می‌شود | نویز لاگ |
| ۵ | `SMS Idle timeout` / `login_failed` | چند | گذرا/عادی | بدون اقدام |
خطای ۵ اقدام لازم ندارد (idle timeout قبلاً retry دارد؛ `login_failed` warning عادی است).
---
## فایل‌های مرتبط
| فایل | نقش |
|------|-----|
| `src/ClinicInvitation/Entity/ClinicDoctorInvitation.php` | تولید توکن دعوت (خط ۷۷ و ۹۷: `bin2hex(random_bytes(48))`) |
| `src/ClinicInvitation/Service/ClinicInvitationService.php` | ساخت لینک و ارسال پیامک دعوت (خط ۱۱۳، `sendSms`) |
| `src/Sms/Entity/SmsMessageTemplate.php` | `token_map` قالب‌ها؛ `CLINIC_INVITATION``'link' => 'token'` |
| `src/Sms/Provider/KavehNegarProvider.php` | ساخت GET به کاوه‌نگار؛ محدودیت slotها |
| `src/Payment/Gateway/MellatGateway.php` | خط ۷۰: `new \SoapClient(...)` |
| `src/Doctor/Controller/DoctorController.php` | خط ۳۶۹–۳۸۱: متد `delete` |
| `src/Shared/EventSubscriber/ExceptionSubscriber.php` | fallback عمومی که همه‌چیز را error+۵۰۰ می‌کند |
| `Dockerfile` | خط ۶۷: `docker-php-ext-install ... soap` (از قبل هست) |
---
## وظیفه ۱ — رفع 431 پیامک دعوت (و welcome)
### ریشه
توکن دعوت این‌گونه تولید می‌شود:
```php
// ClinicDoctorInvitation.php:77 و :97
$this->token = bin2hex(random_bytes(48)); // ۹۶ کاراکتر hex
```
سپس لینک کامل ساخته و به‌عنوان توکن کاوه‌نگار فرستاده می‌شود:
```php
// ClinicInvitationService.php:113
$link = rtrim($this->appUrl, '/') . '/clinic-invitation/' . $inv->getToken();
// link ≈ https://clinic-pro.ir/clinic-invitation/<۹۶ کاراکتر> ≈ ۱۳۶ کاراکتر
```
و در `token_map`، `link` روی slot `token` می‌نشیند:
```php
// SmsMessageTemplate.php — TAG_CLINIC_INVITATION
'token_map' => ['clinic' => 'token10', 'link' => 'token'],
```
دو مشکل:
1. کل URL (۱۳۶ کاراکتر) در query stringِ GET کاوه‌نگار → طول request line از حد edge کاوه‌نگار رد می‌شود → `431 Request Header Fields Too Large`.
2. slot `token`/`token2`/`token3` کاراکترهای خاص (`/`, `:`) و فاصله را رد می‌کند؛ URL پر از `/` است. باید slot `token10`/`token20` باشد (طبق کامنت خود provider).
### راه‌حل
**الف) توکن دعوت را کوتاه کن** (هم برای SMS، هم index دیتابیس سبک‌تر). ۹۶ کاراکتر بیش از حد است؛ `random_bytes(16)` → ۳۲ hex کافیِ امن است:
```php
// ClinicDoctorInvitation.php — خط ۷۷ و ۹۷ (هر دو محل)
$this->token = bin2hex(random_bytes(16)); // ۳۲ کاراکتر، همچنان کریپتوگرافیک امن
```
**ب) slot لینک را به `token10` منتقل کن** (فاصله/URL را می‌پذیرد):
```php
// SmsMessageTemplate.php — TAG_CLINIC_INVITATION
'token_map' => ['clinic' => 'token20', 'link' => 'token10'],
```
> نکته مهم: کاوه‌نگار قالب‌های verify/lookup را از پیش تأیید می‌کند. اگر متن قالب تأییدشدهٔ `clinicpro-clinic-invite` به `%token%`/`%token10%` خاصی بسته شده، تغییر slot باید با پنل کاوه‌نگار هماهنگ شود. اول بررسی کن قالب فعلی کدام tokenها را انتظار دارد؛ اگر تغییر slot در پنل ممکن نیست، حداقل وظیفهٔ (الف) — کوتاه‌کردن توکن — را انجام بده که به‌تنهایی طول را از آستانهٔ 431 پایین می‌آورد.
**ج) بررسی welcome** (خطای ۹۴۹۴ روی `clinicpro-welcome`، `token_map: ['name' => 'token', 'site' => 'token10']`): اگر `name` می‌تواند طولانی/دارای فاصله باشد، آن هم باید `token10` شود. متن قالب welcome در پنل کاوه‌نگار را چک کن و در صورت نیاز `name` را به slot طولانی‌تر ببر.
### edge cases
- توکن‌های دعوتِ قبلی (۹۶ کاراکتری) در دیتابیس باقی می‌مانند؛ تغییر فقط روی دعوت‌های جدید اثر دارد — نیازی به migration نیست (طول ستون `token` محدودیت‌شکن نمی‌شود؛ فقط index).
- مطمئن شو `random_bytes(16)` هنوز به‌اندازهٔ کافی یکتاست (هست — ۱۲۸ بیت).
---
## وظیفه ۲ — رفع `SoapClient not found` (درگاه ملت)
### ریشه
`Dockerfile:67` از قبل soap را نصب می‌کند:
```dockerfile
&& docker-php-ext-install pdo_mysql intl opcache soap \
```
پس کد درست است؛ **ایمیج در حال اجرا در production قدیمی است** (قبل از افزوده‌شدن soap ساخته شده) یا build ناموفق بوده. `php -m | grep soap` روی کانتینر prod خالی است.
### راه‌حل
**الف) redeploy با rebuild کامل** (بدون کش) — قدم اصلی. بعد از deploy تأیید:
```bash
# روی کانتینر prod
php -m | grep -i soap # باید soap چاپ شود
```
**ب) گارد نرم در کد** تا اگر باز هم soap نبود، پرداخت خطای فارسی تمیز بدهد نه `Error: Class "SoapClient" not found` با ۵۰۰:
```php
// MellatGateway.php — داخل soap() قبل از new \SoapClient(...)
private function soap(): \SoapClient
{
if (!class_exists(\SoapClient::class)) {
throw new \App\Shared\Exception\AppException(
'ERR_PAYMENT_GATEWAY_001',
'درگاه پرداخت موقتاً در دسترس نیست. لطفاً بعداً تلاش کنید',
503
);
}
if ($this->soap === null) {
$this->soap = new \SoapClient($this->wsdlUrl(), [ /* ... بدون تغییر ... */ ]);
}
return $this->soap;
}
```
> کد ارور `ERR_PAYMENT_GATEWAY_001` را با الگوی موجود `ErrorCodes`/`AppException` هماهنگ کن؛ اگر ثابت مشابهی برای درگاه هست از همان استفاده کن.
---
## وظیفه ۳ — رفع ۵۰۰ هنگام حذف پزشکِ دارای نوبت
### ریشه
```php
// DoctorController.php:369-381
#[Route('/api/v1/doctor/{uuid}', methods: ['DELETE'])]
public function delete(string $uuid): JsonResponse
{
$doctor = $this->doctorRepo->findByUuid($uuid);
if ($doctor === null) { return $this->error(...404); }
$this->insuranceCleanup->purgeForEntity(TenantInsurance::TYPE_DOCTOR, $doctor->getId());
$this->doctorRepo->remove($doctor); // ← اگر appointment وابسته باشد: FK 1451 → ۵۰۰
return $this->success(['message' => 'دکتر با موفقیت حذف شد']);
}
```
`appointments.doctor_id` FK دارد؛ حذف پزشکِ دارای نوبت → `ForeignKeyConstraintViolationException` → fallback عمومی ۵۰۰.
### راه‌حل
قبل از `remove` تعداد نوبت‌های پزشک را بررسی کن و خطای ۴۰۹ فارسی بده:
```php
// تزریق AppointmentRepository در constructor (اگر نیست)
$appointmentCount = $this->appointmentRepo->count(['doctor' => $doctor]);
if ($appointmentCount > 0) {
return $this->error(
ErrorCodes::ERR_VALIDATION_002, // یا کد conflict مناسب
'این پزشک نوبت ثبت‌شده دارد و قابل حذف نیست. ابتدا نوبت‌ها را مدیریت کنید',
409
);
}
```
**بررسی کن**: constructor فعلی `DoctorController` (خط ۳۳) چه repositoryهایی تزریق می‌کند؛ اگر `AppointmentRepository` نیست اضافه کن. نام دقیق فیلد رابطه در `Appointment` (`doctor`) را از entity تأیید کن.
> جایگزین: اگر منطق محصول اجازه می‌دهد، به‌جای بلاک‌کردن، حذف را به soft-delete تبدیل کن — ولی راه‌حل بالا (۴۰۹) کمترین ریسک است. تصمیم را با الگوی موجود بقیهٔ deleteها (مثل `deleteAddress`) هماهنگ کن.
---
## وظیفه ۴ — کاهش نویز لاگِ `MethodNotAllowedHttpException` روی `/`
### ریشه
حدود ۳۰ خطا از نوع `POST`/`OPTIONS` روی `https://clinic-pro.ir/` (ربات/اسکنر و preflight). این‌ها در `ExceptionSubscriber` به هیچ‌کدام از شاخه‌های خاص نمی‌خورند و به **fallback عمومی** می‌رسند که:
```php
// ExceptionSubscriber.php — انتهای onKernelException
$this->logger->error(sprintf('Unhandled exception: %s ...')); // ← error سطح ۵۰۰
$event->setResponse(new JsonResponse([...'ERR_INTERNAL_001'...], 500)); // ← اشتباه: باید ۴۰۵
```
یعنی خطای کلاینت ۴۰۵ به‌اشتباه به‌عنوان error داخلی ۵۰۰ لاگ و پاسخ داده می‌شود.
### راه‌حل
یک شاخهٔ اختصاصی برای `MethodNotAllowedHttpException` قبل از fallback اضافه کن — پاسخ ۴۰۵ و لاگ در سطح `notice` (نه error):
```php
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
// قبل از بلاک fallback عمومی
if ($exception instanceof MethodNotAllowedHttpException) {
$this->logger->notice('Method not allowed', [
'path' => $event->getRequest()->getPathInfo(),
'method' => $event->getRequest()->getMethod(),
]);
$event->setResponse(new JsonResponse(
['success' => false, 'data' => null, 'errors' => [['code' => 'ERR_METHOD_NOT_ALLOWED_001', 'message' => 'متد درخواستی مجاز نیست']]],
405
));
return;
}
```
### نکته دربارهٔ OPTIONS
بعضی خطاها `OPTIONS https://clinic-pro.ir/` هستند = preflight CORS. اگر کلاینتی واقعاً به ریشهٔ دامنه preflight می‌زند، احتمالاً base URL اشتباه در فرانت‌اند است — ولی چون فقط چند مورد است و probeهای ربات هم OPTIONS می‌فرستند، برای الان همین کاهش نویز کافی است. اگر config CORS جداگانه OPTIONS را قبل از router هندل می‌کند، بررسی کن که این تغییر با آن تداخل ندارد.
---
## نکات مهم کلی
- همهٔ تغییرها backend `clinicpro` هستند؛ کلاینت (`nobat724_front`) قرارداد API را برای این موارد مصرف نمی‌کند به‌جز کد خطای جدید ۴۰۵/۴۰۹/۵۰۳ — پیام‌ها فارسی‌اند و ساختار envelope حفظ می‌شود.
- بعد از تغییرِ رفتار endpoint حذف پزشک و درگاه پرداخت، فایل مربوطه در `clinicpro/docs/api/` را به‌روز کن (قانون استاندارد پروژه: docs در همان session).
- برای وظیفهٔ ۲ (soap) قدم اصلی **redeploy** است؛ گارد کد فقط شبکهٔ ایمنی است.
- تست: بعد از تغییرات، `ddev exec php bin/console lint:container` و در صورت وجود، تست‌های SMS/Doctor delete را اجرا کن.
- کدهای خطای جدید (`ERR_METHOD_NOT_ALLOWED_001`, `ERR_PAYMENT_GATEWAY_001`) را در `ErrorCodes` (اگر enum/const مرکزی دارد) ثبت کن تا با الگوی موجود یکدست بماند.
+3
View File
@@ -267,6 +267,8 @@ Delete a doctor profile.
> **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.
> **Guard:** a doctor with existing appointments cannot be deleted (the `appointments.doctor_id` FK would otherwise raise a `500`). The endpoint pre-checks and returns `409 ERR_CONFLICT_001` instead.
### Path Parameters
| Param | Type | Description |
|-------|------|-------------|
@@ -286,6 +288,7 @@ Delete a doctor profile.
| `ERR_AUTH_001` | 401 | Missing token |
| `ERR_AUTH_006` | 403 | Not admin |
| `ERR_NOT_FOUND_001` | 404 | Doctor not found |
| `ERR_CONFLICT_001` | 409 | Doctor has existing appointments and cannot be deleted |
---
+27 -1
View File
@@ -693,6 +693,7 @@
"691": "Community 691",
"692": "Community 692",
"693": "Community 693",
"694": "Community 694",
"695": "Community 695",
"696": "Community 696",
"697": "Community 697",
@@ -701,18 +702,43 @@
"700": "Community 700",
"701": "Community 701",
"702": "Community 702",
"703": "Community 703",
"704": "Community 704",
"705": "Community 705",
"706": "Community 706",
"707": "Community 707",
"708": "Community 708",
"709": "Community 709",
"710": "Community 710",
"711": "Community 711",
"712": "Community 712",
"713": "Community 713",
"714": "Community 714",
"715": "Community 715",
"716": "Community 716",
"717": "Community 717",
"718": "Community 718",
"719": "Community 719",
"720": "Community 720",
"721": "Community 721",
"722": "Community 722",
"738": "Community 738"
"723": "Community 723",
"724": "Community 724",
"725": "Community 725",
"726": "Community 726",
"727": "Community 727",
"728": "Community 728",
"729": "Community 729",
"730": "Community 730",
"731": "Community 731",
"732": "Community 732",
"733": "Community 733",
"734": "Community 734",
"735": "Community 735",
"736": "Community 736",
"737": "Community 737",
"738": "Community 738",
"739": "Community 739",
"740": "Community 740",
"741": "Community 741"
}
+287 -137
View File
@@ -1,16 +1,16 @@
# Graph Report - clinicpro (2026-07-10)
# Graph Report - clinicpro (2026-07-11)
## Corpus Check
- 736 files · ~542,018 words
- 740 files · ~550,094 words
- Verdict: corpus is large enough that graph structure adds value.
## Summary
- 9274 nodes · 12833 edges · 716 communities (567 shown, 149 thin omitted)
- Extraction: 98% EXTRACTED · 2% INFERRED · 0% AMBIGUOUS · INFERRED: 279 edges (avg confidence: 0.8)
- 9347 nodes · 12943 edges · 742 communities (598 shown, 144 thin omitted)
- Extraction: 98% EXTRACTED · 2% INFERRED · 0% AMBIGUOUS · INFERRED: 281 edges (avg confidence: 0.8)
- Token cost: 0 input · 0 output
## Graph Freshness
- Built from commit: `ec184dfc`
- Built from commit: `f30bf5df`
- Run `git rev-parse HEAD` and compare to check if the graph is stale.
- Run `graphify update .` after code changes (no API cost).
@@ -695,6 +695,7 @@
- [[_COMMUNITY_Community 691|Community 691]]
- [[_COMMUNITY_Community 692|Community 692]]
- [[_COMMUNITY_Community 693|Community 693]]
- [[_COMMUNITY_Community 694|Community 694]]
- [[_COMMUNITY_Community 695|Community 695]]
- [[_COMMUNITY_Community 696|Community 696]]
- [[_COMMUNITY_Community 697|Community 697]]
@@ -703,53 +704,78 @@
- [[_COMMUNITY_Community 700|Community 700]]
- [[_COMMUNITY_Community 701|Community 701]]
- [[_COMMUNITY_Community 702|Community 702]]
- [[_COMMUNITY_Community 703|Community 703]]
- [[_COMMUNITY_Community 704|Community 704]]
- [[_COMMUNITY_Community 705|Community 705]]
- [[_COMMUNITY_Community 706|Community 706]]
- [[_COMMUNITY_Community 707|Community 707]]
- [[_COMMUNITY_Community 708|Community 708]]
- [[_COMMUNITY_Community 709|Community 709]]
- [[_COMMUNITY_Community 710|Community 710]]
- [[_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]]
- [[_COMMUNITY_Community 718|Community 718]]
- [[_COMMUNITY_Community 719|Community 719]]
- [[_COMMUNITY_Community 720|Community 720]]
- [[_COMMUNITY_Community 721|Community 721]]
- [[_COMMUNITY_Community 722|Community 722]]
- [[_COMMUNITY_Community 723|Community 723]]
- [[_COMMUNITY_Community 724|Community 724]]
- [[_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]]
- [[_COMMUNITY_Community 733|Community 733]]
- [[_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]]
## God Nodes (most connected - your core abstractions)
1. `BaseController` - 80 edges
2. `ApiTestCase` - 76 edges
3. `api` - 55 edges
4. `UserProfile` - 52 edges
5. `Clinic` - 50 edges
6. `Doctor` - 48 edges
3. `Doctor` - 59 edges
4. `api` - 55 edges
5. `UserProfile` - 52 edges
6. `Clinic` - 50 edges
7. `useAuthStore` - 43 edges
8. `ApiResponse` - 41 edges
9. `AdminApiController` - 41 edges
8. `AdminApiController` - 43 edges
9. `ApiResponse` - 41 edges
10. `formatDate()` - 39 edges
## Surprising Connections (you probably didn't know these)
- `ServiceTariffModal()` --calls--> `formatNumber()` [EXTRACTED]
assets/admin/components/ServiceTariffModal.tsx → assets/admin/lib/utils.ts
- `SmsPage()` --calls--> `formatDateTime()` [EXTRACTED]
assets/admin/pages/SmsPage.tsx → assets/admin/lib/utils.ts
- `Pagination()` --calls--> `formatNumber()` [EXTRACTED]
assets/admin/components/ui/Pagination.tsx → assets/admin/lib/utils.ts
- `SettlementsPage()` --calls--> `formatRial()` [EXTRACTED]
assets/admin/pages/SettlementsPage.tsx → assets/admin/lib/utils.ts
- `NewAppointmentModal()` --calls--> `useAuthStore` [EXTRACTED]
assets/admin/pages/AppointmentsPage.tsx → assets/admin/stores/authStore.ts
- `LogoUploadField()` --calls--> `useAuthStore` [EXTRACTED]
assets/admin/pages/CategoriesPage.tsx → assets/admin/stores/authStore.ts
- `TabActions()` --calls--> `useAuthStore` [EXTRACTED]
assets/admin/pages/CategoriesPage.tsx → assets/admin/stores/authStore.ts
## Import Cycles
- None detected.
## Communities (716 total, 149 thin omitted)
## Communities (742 total, 144 thin omitted)
### Community 0 - "Community 0"
Cohesion: 0.05
Nodes (48): ALLOWED_ROLES, PrivateRoute(), PublicRoute(), RoleRoute(), queryClient, toastStyle, get, BeforeInstallPromptEvent (+40 more)
Nodes (41): ALLOWED_ROLES, PrivateRoute(), PublicRoute(), RoleRoute(), queryClient, toastStyle, DEGREE_OPTIONS, DoctorFormPage() (+33 more)
### Community 1 - "Community 1"
Cohesion: 0.03
@@ -764,7 +790,7 @@ Cohesion: 0.10
Nodes (20): `RepresentationActionController` — welcome به‌صورت inline (بدون تمپلت), `SmsMessageTemplate::DEFAULTS` (فاقد نام تمپلت و نگاشت token), `SmsService::sendNow` — انتخاب بین lookup و متن‌آزاد, الگوی فعلی همه‌ی call-siteها (به‌جز OTP) — متن‌آزاد، بدون `templateCode`, تبدیل همه‌ی پیامک‌های سیستمی به VerifyLookup کاوه‌نگار (تمپلت نام‌دار), تنها جای درست (OTP) — که باید الگوی بقیه شود, زمینه, فایل‌های مرتبط (+12 more)
### Community 4 - "Community 4"
Cohesion: 0.08
Cohesion: 0.07
Nodes (5): Doctor, Collection, self, User, WeeklySchedule
### Community 5 - "Community 5"
@@ -780,8 +806,8 @@ Cohesion: 0.05
Nodes (11): DoctorServiceController, DoctorService, Clinic, DoctorServiceRepository, Collection, Doctor, self, User (+3 more)
### Community 8 - "Community 8"
Cohesion: 0.07
Nodes (27): Contract, InsuranceOption, KIND_LABEL, AdminLayout(), Topbar(), DEGREE_OPTIONS, DoctorFormPage(), FormValues (+19 more)
Cohesion: 0.08
Nodes (27): useSubscription(), AdminLayout(), avatarBg(), buildSections(), HUES, Props, ROLE_LABELS, Section (+19 more)
### Community 9 - "Community 9"
Cohesion: 0.04
@@ -797,11 +823,11 @@ Nodes (4): UserActiveContextRepository, ManagerRegistry, User, UserActiveContext
### Community 12 - "Community 12"
Cohesion: 0.05
Nodes (44): Clinic Doctor Invitation API, DELETE `/api/v1/admin/clinic/invitation/{invUuid}`, Errors, Errors, Errors, Errors, Errors, Errors (+36 more)
Nodes (39): Clinic Doctor Invitation API, DELETE `/api/v1/admin/clinic/invitation/{invUuid}`, Errors, Errors, Errors, Errors, Errors, Errors (+31 more)
### Community 13 - "Community 13"
Cohesion: 0.08
Nodes (21): api, ApiError, downloadFile(), getToken(), refreshOnce(), request(), { refreshMock, logoutMock }, replaceMock (+13 more)
Cohesion: 0.05
Nodes (36): get, PaymentConfig, PaymentGatewayInfo, usePaymentConfig(), api, ApiError, ApiResponse, downloadFile() (+28 more)
### Community 14 - "Community 14"
Cohesion: 0.10
@@ -812,8 +838,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, LogPruneService, Appointment, Collection, PatientRecord, self (+1 more)
Cohesion: 0.05
Nodes (10): PatientSession, SmsWallet, AppLog, LogPruneService, AppointmentExpiryService, Appointment, Collection, PatientRecord (+2 more)
### Community 17 - "Community 17"
Cohesion: 0.05
@@ -825,19 +851,19 @@ Nodes (38): API, API, API, API, API, Route, Route, Route (+30 more)
### Community 19 - "Community 19"
Cohesion: 0.03
Nodes (59): CityForm, citySchema, ImportError, InsuranceForm, insuranceSchema, LogoUploadField(), ProvinceForm, provinceSchema (+51 more)
Nodes (56): CityForm, citySchema, ImportError, InsuranceForm, insuranceSchema, LogoUploadField(), ProvinceForm, provinceSchema (+48 more)
### Community 20 - "Community 20"
Cohesion: 0.13
Nodes (4): AdminApiController, JsonResponse, Request, StreamedResponse
Cohesion: 0.12
Nodes (6): AdminApiController, Collection, JsonResponse, Request, User, StreamedResponse
### Community 21 - "Community 21"
Cohesion: 0.05
Nodes (34): formatNumber(), ClinicDetailPage(), AdminCharts, AdminDashboard(), AdminRecent, AdminStats, APPT_CLS, APPT_COLOR (+26 more)
Cohesion: 0.03
Nodes (48): formatNumber(), ClinicAddress, ClinicDetailPage(), ClinicDoctorItem, ClinicInvitation, EditForm, editSchema, HUES_LIST (+40 more)
### Community 22 - "Community 22"
Cohesion: 0.16
Nodes (9): RatingController, Rate, RateRepository, JsonResponse, Request, User, Doctor, ManagerRegistry (+1 more)
Cohesion: 0.12
Nodes (10): RatingController, Like, CommentListNPlusOneTest, LikeRepository, JsonResponse, Request, User, Comment (+2 more)
### Community 23 - "Community 23"
Cohesion: 0.05
@@ -876,8 +902,8 @@ Cohesion: 0.06
Nodes (32): 10. Modal / Dialog, 11. Toast Notifications, 12. Empty States & Loading, 13. Page Header (هر صفحه), 14. تکنولوژی Stack, 15. Responsive Breakpoints, 16. Dark Mode (اختیاری — فاز دوم), 17. نمونه رنگ‌بندی صفحه داشبورد (+24 more)
### Community 33 - "Community 33"
Cohesion: 0.08
Nodes (21): CoverageRow, Draft, KIND, TenantInsurance, ServiceTariffModal(), TariffResponse, TariffRow, EMPTY_ITEMS (+13 more)
Cohesion: 0.07
Nodes (25): CoverageRow, Draft, KIND, TenantInsurance, ServiceTariffModal(), TariffResponse, TariffRow, EMPTY_ITEMS (+17 more)
### Community 34 - "Community 34"
Cohesion: 0.06
@@ -896,8 +922,8 @@ Cohesion: 0.06
Nodes (31): API endpoint موجود برای آدرس دکتر:, `DELETE /api/v1/clinic/{clinicUuid}/address/{addressUuid}` — حذف, `docs/api/appointment-settings.md`:, `docs/api/clinic.md`:, Endpoint موجود (workaround که حذف می‌شود):, Entity `DoctorAddress`:, Frontend موجود (`DoctorDetailPage.tsx`):, `GET /api/v1/clinic/{clinicUuid}/addresses` — لیست آدرس‌ها (+23 more)
### Community 38 - "Community 38"
Cohesion: 0.05
Nodes (44): Authentication API, DELETE `/api/v1/notification-mobile/{target}`, Error Codes, Error Codes, Errors, Errors, Errors, Errors (+36 more)
Cohesion: 0.25
Nodes (8): Authentication API, DELETE `/api/v1/notification-mobile/{target}`, Errors, Notification Mobile (OTP), POST `/oauth/logout`, Request Body, Response `200`, Response `200`
### Community 39 - "Community 39"
Cohesion: 0.12
@@ -940,8 +966,8 @@ Cohesion: 0.11
Nodes (4): Payment, Appointment, self, User
### Community 49 - "Community 49"
Cohesion: 0.07
Nodes (22): AppointmentsPage(), BookingSlot, CancelledBadge(), DateNavigator(), EMPTY_ARR, getPersianWeekDay(), navBtnSx, NewAppointmentModal() (+14 more)
Cohesion: 0.08
Nodes (19): AppointmentsPage(), BookingSlot, CancelledBadge(), DateNavigator(), EMPTY_ARR, getPersianWeekDay(), navBtnSx, NewAppointmentModal() (+11 more)
### Community 50 - "Community 50"
Cohesion: 0.07
@@ -957,7 +983,7 @@ Nodes (26): الزامات UI, باگ‌فیکس صفحه نوبت‌ها, با
### Community 53 - "Community 53"
Cohesion: 0.08
Nodes (16): AppLogRepository, PaymentLog, ClaimItemRepository, InvoiceItemRepository, InvoiceRepository, PaymentLogRepository, PreRegistrationRepository, ServiceEntityRepository (+8 more)
Nodes (16): AppLogRepository, ClaimItemRepository, DoctorInsuranceRepository, PreRegistrationRepository, ProvinceRepository, SessionServiceRepository, ServiceEntityRepository, ManagerRegistry (+8 more)
### Community 54 - "Community 54"
Cohesion: 0.10
@@ -976,8 +1002,8 @@ Cohesion: 0.08
Nodes (24): Blog API, DELETE `/api/v1/blog/{uuid}`, Errors, Errors, Errors, Errors, Errors, GET `/api/v1/blog/{slug}` (+16 more)
### Community 58 - "Community 58"
Cohesion: 0.15
Nodes (8): Blog, BlogController, BlogRepository, JsonResponse, Request, User, ManagerRegistry, QueryBuilder
Cohesion: 0.28
Nodes (4): Blog, BlogRepository, ManagerRegistry, QueryBuilder
### Community 59 - "Community 59"
Cohesion: 0.22
@@ -996,8 +1022,8 @@ Cohesion: 0.08
Nodes (25): Bulk import / export, DELETE `/api/v1/admin/specialty/{id}`, Errors, Errors, Errors, Errors, GET `/api/v1/admin/specialties`, GET `/api/v1/specialties` (+17 more)
### Community 63 - "Community 63"
Cohesion: 0.05
Nodes (45): FreeVisitPrice(), Pricing, cn(), formatDateTime(), formatRial(), iranMobileOptionalSchema, iranMobileSchema, isValidIranMobile() (+37 more)
Cohesion: 0.06
Nodes (41): FreeVisitPrice(), Pricing, cn(), formatRial(), iranMobileOptionalSchema, iranMobileSchema, isValidIranMobile(), maskMobile() (+33 more)
### Community 64 - "Community 64"
Cohesion: 0.19
@@ -1024,8 +1050,8 @@ Cohesion: 0.15
Nodes (12): رفع بهم‌ریختگی کامل پنل ادمین روی iPhone 8 (Safari/Chrome iOS), زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, پروژه (+4 more)
### Community 71 - "Community 71"
Cohesion: 0.13
Nodes (12): AdminUserDetail, AVATAR_COLORS, EditForm, editSchema, GENDER_LABELS, getPrimaryRole(), MARITAL_LABELS, MEDICAL_SECTIONS (+4 more)
Cohesion: 0.08
Nodes (24): سناریو: ایمپورت پزشکان سازمان نظام پزشکی و مدیریت مالکیت پروفایل, نتیجه‌گیری کلیدی طراحی, ۱. خلاصه اجرایی, ۱۰. جریان کاربری (خلاصه‌ی گام‌به‌گام), ۱۱. حالات مرزی و قواعد کسب‌وکار, ۱۲. مراحل پیاده‌سازی (به‌ترتیب و به‌تفکیک ریپو), ۱۳. تصمیمات باز و ریسک‌ها, ۱۴. مرجع نمونه‌ی داده (+16 more)
### Community 72 - "Community 72"
Cohesion: 0.09
@@ -1059,6 +1085,10 @@ Nodes (3): SubscriptionPlan, Collection, self
Cohesion: 0.09
Nodes (23): dependencies, @ckeditor/ckeditor5-build-classic, @ckeditor/ckeditor5-react, @fontsource/vazirmatn, @heroicons/react, @hookform/resolvers, jalaali-js, leaflet (+15 more)
### Community 82 - "Community 82"
Cohesion: 0.07
Nodes (8): CategoryImportTest, Connection, RepositoryClassMappingTest, KernelTestCase, DbLogger, CategoryImporter, DbLoggerTest, Stringable
### Community 83 - "Community 83"
Cohesion: 0.09
Nodes (21): Backend, CSS / UI, Frontend, روند اجرای هر قابلیت, قبل از شروع — تحلیل پرامپت و ساخت Todo, قوانین اجرا (اجباری — هیچ استثنایی ندارد), قوانین خاص این پروژه, مثال اجرا (+13 more)
@@ -1072,8 +1102,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.06
Nodes (15): AppointmentExpiryServiceTest, DateOverrideOwnershipTest, LowTierFixesTest, SendCodeMobileRateLimitTest, ClaimsListPaginationTest, CaptchaFlowTest, ServiceItemStaffOwnershipTest, KernelBrowser (+7 more)
Cohesion: 0.05
Nodes (16): AppointmentExpiryServiceTest, ScheduleOwnershipTest, LowTierFixesTest, SendCodeMobileRateLimitTest, ClaimsListPaginationTest, CaptchaFlowTest, ServiceItemStaffOwnershipTest, KernelBrowser (+8 more)
### Community 87 - "Community 87"
Cohesion: 0.10
@@ -1096,7 +1126,7 @@ Cohesion: 0.10
Nodes (20): DELETE `/api/v1/admin/users/{uuid}`, Errors, Errors, GET `/api/v1/admin/users`, GET `/api/v1/admin/users/stats`, GET `/api/v1/admin/users/{uuid}`, POST `/api/v1/admin/users/{uuid}/status`, PUT `/api/v1/admin/users/{uuid}` (+12 more)
### Community 92 - "Community 92"
Cohesion: 0.10
Cohesion: 0.09
Nodes (21): Bulk import / export, DELETE `/api/v1/admin/doctor-service/{id}`, Doctor Service API, Errors, Errors, Errors, Errors, GET `/api/v1/admin/doctor-services` (+13 more)
### Community 93 - "Community 93"
@@ -1144,8 +1174,8 @@ Cohesion: 0.11
Nodes (18): `AdminApiController::paymentDetail` — الان فقط GET, `MellatGateway.php` — الگوی موجود REST/SOAP (پس از کار sandbox), `PaymentGatewayInterface.php`, `PaymentManager.php` — الگوی log و transaction, برگشت/استرداد وجه ملت از پنل ادمین (bpReversalRequest / bpRefundRequest), زمینه, فایل‌های مرتبط, نکات مهم (+10 more)
### Community 104 - "Community 104"
Cohesion: 0.18
Nodes (6): TagController, TagRepository, JsonResponse, Request, ManagerRegistry, Tag
Cohesion: 0.35
Nodes (3): TagController, JsonResponse, Request
### Community 105 - "Community 105"
Cohesion: 0.11
@@ -1160,8 +1190,8 @@ Cohesion: 0.33
Nodes (6): AppointmentController, Appointment, Doctor, JsonResponse, Request, User
### Community 108 - "Community 108"
Cohesion: 0.11
Nodes (12): CaptchaController, BaseController, CategoryController, CategoryImportController, SiteContextController, JsonResponse, JsonResponse, Request (+4 more)
Cohesion: 0.13
Nodes (9): CaptchaController, BaseController, CategoryController, CategoryImportController, JsonResponse, JsonResponse, Request, JsonResponse (+1 more)
### Community 109 - "Community 109"
Cohesion: 0.29
@@ -1260,8 +1290,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.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)
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)
### Community 136 - "Community 136"
Cohesion: 0.12
@@ -1392,8 +1422,8 @@ Cohesion: 0.10
Nodes (20): api.ir (استعلام هویت — Shahkar / IbanMatch), اتصال به دیتابیس‌های مستقل (الزامی), اسرار (الزامی — قبل از اولین دیپلوی), امنیت و منابع, بررسی سلامت, دامنه‌ها و CORS, دیپلوی‌های بعدی, راهنمای دیپلوی ClinicPro (Coolify + Docker Compose) (+12 more)
### Community 169 - "Community 169"
Cohesion: 0.15
Nodes (5): EntityInsurancePricing, TenantInsuranceCleanupTest, EntityInsurancePricingRepository, TenantInsuranceCleanupService, ManagerRegistry
Cohesion: 0.22
Nodes (4): EntityInsurancePricing, TenantInsuranceCleanupTest, EntityInsurancePricingRepository, ManagerRegistry
### Community 170 - "Community 170"
Cohesion: 0.13
@@ -1524,16 +1554,16 @@ Cohesion: 0.15
Nodes (12): Clinic Services API, DELETE /api/v1/service-item/{uuid}, DELETE /api/v1/service-section/{uuid}, GET /api/v1/service-items/{sectionUuid}, GET /api/v1/service-items/{uuid}/tariffs, GET /api/v1/service-sections, PATCH /api/v1/service-item/{uuid}, PATCH /api/v1/service-section/{uuid} (+4 more)
### Community 204 - "Community 204"
Cohesion: 0.23
Nodes (5): ExceptionSubscriber, SecurityHeadersSubscriber, EventSubscriberInterface, ExceptionEvent, ResponseEvent
Cohesion: 0.21
Nodes (6): AuthenticationException, ExceptionSubscriber, SecurityHeadersSubscriber, EventSubscriberInterface, ExceptionEvent, ResponseEvent
### Community 205 - "Community 205"
Cohesion: 0.07
Nodes (20): Command, CancelExpiredAppointmentsCommand, CreateAdminCommand, PruneLogsCommand, SeedCategoriesCommand, SeedDemoDataCommand, SeedSmsMessageTemplatesCommand, InputInterface (+12 more)
Cohesion: 0.06
Nodes (23): Command, CancelExpiredAppointmentsCommand, CreateAdminCommand, PruneLogsCommand, SeedCategoriesCommand, SeedDemoDataCommand, SeedSmsMessageTemplatesCommand, SystemOwnerCommand (+15 more)
### Community 206 - "Community 206"
Cohesion: 0.21
Nodes (5): MellatGateway, SoapClient, PaymentInitResult, PaymentRefundResult, PaymentVerifyResult
Cohesion: 0.08
Nodes (14): MellatGateway, MockGateway, SepGateway, SoapClient, PaymentGatewayInterface, PaymentInitResult, PaymentRefundResult, PaymentVerifyResult (+6 more)
### Community 207 - "Community 207"
Cohesion: 0.15
@@ -1592,8 +1622,8 @@ Cohesion: 0.23
Nodes (5): PaymentRepository, Appointment, ManagerRegistry, Payment, User
### Community 225 - "Community 225"
Cohesion: 0.09
Nodes (18): formatDate(), toDate(), ALL_STATUSES, AppointmentDetailPage(), timeOf(), Claim, ClaimItem, DebtRow (+10 more)
Cohesion: 0.05
Nodes (36): formatDate(), formatDateTime(), toDate(), ALL_STATUSES, AppointmentDetailPage(), timeOf(), Claim, ClaimItem (+28 more)
### Community 226 - "Community 226"
Cohesion: 0.15
@@ -1604,12 +1634,12 @@ Cohesion: 0.17
Nodes (11): تشخیص عمیق down شدن سرور بعد از ~۱۰ سیکل + وریفای و تکمیل فیکس‌های پایداری, زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, ۱. وریفای فیکس‌های repo (idempotent) (+3 more)
### Community 228 - "Community 228"
Cohesion: 0.04
Nodes (51): Bulk import / export, DELETE `/api/v1/admin/insurance/{id}`, DELETE `/api/v1/billing/tenant-insurances/{uuid}`, DELETE `/api/v1/insurance/{id}`, EntityInsurancePricing — قیمت‌گذاری ویزیت بر اساس بیمه, Errors, Errors, Errors (+43 more)
Cohesion: 0.20
Nodes (10): Bulk import / export, DELETE `/api/v1/admin/insurance/{id}`, DELETE `/api/v1/insurance/{id}`, EntityInsurancePricing — قیمت‌گذاری ویزیت بر اساس بیمه, GET `/api/v1/insurance/{id}`, Insurance API, Response `200`, Response `200` (+2 more)
### Community 229 - "Community 229"
Cohesion: 0.14
Nodes (13): Errors, GET `/api/v1/representation/{uuid}/dashboard/monthly`, GET `/api/v1/representation/{uuid}/dashboard/yearly`, GET `/api/v1/site-context`, Path Parameters, Query Parameters, Query Parameters, Query Parameters (+5 more)
Nodes (13): DELETE `/api/v1/representation/{uuid}`, Errors, Errors, GET `/api/v1/representation/{uuid}/dashboard/monthly`, GET `/api/v1/site-context`, Path Parameters, Query Parameters, Query Parameters (+5 more)
### Community 230 - "Community 230"
Cohesion: 0.35
@@ -1668,8 +1698,8 @@ Cohesion: 0.28
Nodes (4): SubscriptionService, ClinicSubscription, Payment, SubscriptionPlan
### Community 245 - "Community 245"
Cohesion: 0.06
Nodes (29): PaymentConfig, PaymentGatewayInfo, usePaymentConfig(), emptyFeatures(), FEATURE_KEYS, FEATURE_LABELS, PeriodForm, periodSchema (+21 more)
Cohesion: 0.15
Nodes (11): emptyFeatures(), FEATURE_KEYS, FEATURE_LABELS, PeriodForm, periodSchema, PLAN_DISPLAY, PlanForm, planSchema (+3 more)
### Community 246 - "Community 246"
Cohesion: 0.17
@@ -1780,7 +1810,7 @@ Cohesion: 0.18
Nodes (10): Endpoint های موجود که تغییر می‌کنند, GET /api/v1/admin/dashboard/charts?from=UNIX&to=UNIX, GET /api/v1/dashboard/clinic, GET /api/v1/dashboard/doctor, تسک ۱۶: داشبورد هوشمند — چارت + فیلتر زمانی, توضیح, زمان تخمینی, فیلتر بازه زمانی (+2 more)
### Community 274 - "Community 274"
Cohesion: 0.16
Cohesion: 0.17
Nodes (5): Authentication, ClinicPro — API Documentation Index, Error Code Reference, Modules, Standard Response Envelope
### Community 275 - "Community 275"
@@ -1856,8 +1886,8 @@ Cohesion: 0.20
Nodes (9): AdminApiController — dashboardCharts با بازه زمانی, DashboardController — اضافه کردن from/to, date range selector component:, تغییر Backend, تغییر Frontend — DashboardPage.tsx, فایل‌هایی که تغییر می‌کنند, معماری — تسک ۱۶: داشبورد هوشمند, نصب dependency: (+1 more)
### Community 294 - "Community 294"
Cohesion: 0.25
Nodes (8): AbstractAuthenticator, AuthenticationException, Passport, RateLimiterFactory, PasswordAuthenticator, Request, Response, TokenInterface
Cohesion: 0.30
Nodes (6): AbstractAuthenticator, Passport, PasswordAuthenticator, Request, Response, TokenInterface
### Community 295 - "Community 295"
Cohesion: 0.20
@@ -1868,8 +1898,8 @@ Cohesion: 0.22
Nodes (9): Clinic Management, DELETE `/api/v1/admin/clinic/{uuid}`, Errors, GET `/api/v1/admin/clinics`, PATCH `/api/v1/admin/clinic/{uuid}/status`, Query Parameters, Response `200`, Response `200` (+1 more)
### Community 297 - "Community 297"
Cohesion: 0.15
Nodes (13): DELETE `/api/v1/representation/iban/{id}`, Errors, Errors, Errors, GET `/api/v1/representation/doctors`, GET `/api/v1/representation/doctors/stats`, GET `/api/v1/representation/me`, Query Parameters (+5 more)
Cohesion: 0.22
Nodes (9): DELETE `/api/v1/representation/iban/{id}`, Errors, Errors, GET `/api/v1/representation/doctors/stats`, GET `/api/v1/representation/me`, Response `200`, Response `200`, Response `200` (+1 more)
### Community 298 - "Community 298"
Cohesion: 0.22
@@ -1880,12 +1910,12 @@ Cohesion: 0.22
Nodes (9): require-dev, phpstan/phpstan, phpstan/phpstan-doctrine, phpstan/phpstan-symfony, phpunit/phpunit, symfony/browser-kit, symfony/css-selector, symfony/debug-bundle (+1 more)
### Community 300 - "Community 300"
Cohesion: 0.19
Nodes (6): HealthController, PreRegistrationController, EntityManagerInterface, JsonResponse, Request, JsonResponse
Cohesion: 0.42
Nodes (3): PreRegistrationController, JsonResponse, Request
### Community 301 - "Community 301"
Cohesion: 0.07
Nodes (29): ApiResponse, PaginatedResponse, STATUS_FILTERS, FILTERS, Breakdown, SOURCE_LABEL, Summary, LEVEL_FILTER_OPTIONS (+21 more)
Cohesion: 0.05
Nodes (41): PaginatedResponse, STATUS_FILTERS, AddForm, addSchema, ClinicsPage(), HUES_LIST, FILTERS, Breakdown (+33 more)
### Community 302 - "Community 302"
Cohesion: 0.12
@@ -1896,8 +1926,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.04
Nodes (47): 29. 🟢 `GET` clinic list 🆕, 30. 🟢 `GET` get 🆕, 32. 🔵 `POST` post, 33. 🔵 `POST` image_clinic, 34. 🔵 `POST` image logo, 35. 🟡 `PATCH` patch, 36. 🟢 `GET` get my rate, 38. 🟢 `GET` Unapproved comments (+39 more)
Cohesion: 0.22
Nodes (9): 29. 🟢 `GET` clinic list 🆕, 30. 🟢 `GET` get 🆕, 33. 🔵 `POST` image_clinic, 6. کلینیک (Clinic), هدرهای اضافی, پارامترهای Query, پاسخ‌ها, پاسخ‌ها (+1 more)
### Community 306 - "Community 306"
Cohesion: 0.07
@@ -1944,8 +1974,8 @@ Cohesion: 0.31
Nodes (3): SubscriptionPlanRepository, ManagerRegistry, SubscriptionPlan
### Community 318 - "Community 318"
Cohesion: 0.24
Nodes (4): AuthController, JsonResponse, Request, User
Cohesion: 0.22
Nodes (5): AuthController, RateLimiterFactory, JsonResponse, Request, User
### Community 319 - "Community 319"
Cohesion: 0.39
@@ -2245,7 +2275,7 @@ Nodes (7): initiate(), refund(), reverse(), verify(), PaymentInitResult, Payment
### Community 399 - "Community 399"
Cohesion: 0.23
Nodes (5): AbstractMigration, Schema, Version20260609130407, Schema, Version20260619121047
Nodes (5): AbstractMigration, Schema, Version20260609130407, Schema, Version20260614183527
### Community 401 - "Community 401"
Cohesion: 0.12
@@ -2260,8 +2290,8 @@ Cohesion: 0.27
Nodes (8): MessageBusInterface, MockObject, SmsServiceLookupOnlyTest, SmsLogRepository, SmsMessageTemplateRepository, SmsService, SmsTextResolver, KavehNegarProvider
### Community 414 - "Community 414"
Cohesion: 0.07
Nodes (18): ClinicAddress, ClinicDoctorItem, ClinicInvitation, EditForm, editSchema, HUES_LIST, INV_STATUS_MAP, IRAN_CENTER (+10 more)
Cohesion: 0.10
Nodes (20): edge cases, خلاصهٔ خطاها و اولویت, راه‌حل, راه‌حل, راه‌حل, راه‌حل, رفع خطاهای لاگ سرور (production) — ۱۴۰۵/۰۴/۲۰, ریشه (+12 more)
### Community 418 - "Community 418"
Cohesion: 0.17
@@ -2284,16 +2314,20 @@ Cohesion: 0.35
Nodes (5): PatientController, JsonResponse, PatientSession, Request, User
### Community 435 - "Community 435"
Cohesion: 0.21
Nodes (4): SepGateway, PaymentInitResult, PaymentRefundResult, PaymentVerifyResult
Cohesion: 0.33
Nodes (4): BlogController, JsonResponse, Request, User
### Community 439 - "Community 439"
Cohesion: 0.20
Nodes (4): HealthController, EntityManagerInterface, TenantInsuranceCleanupService, JsonResponse
### Community 440 - "Community 440"
Cohesion: 0.11
Nodes (18): [F10] راهنمای کهنه در `CLAUDE.md`: endpoint `categorys/{bundle}` منتقل شده, [F11] داشبورد دکتر `GET /api/v1/dashboard/doctor` همیشه 500 (فیلد ناموجود در DQL) — ✅ رفع شد, [F1] phpstan: مقایسهٔ همیشه‌درست در محاسبهٔ estimated SMS — ✅ رفع شد, [F2] تست‌های PHPUnit به API خارجی Kavenegar درخواست واقعی می‌زنند, [F3] دیتابیس تست seed نشده — فقط کاربر ادمین وجود دارد, [F4] اسکریپت seeder `create_test_users.php` وجود ندارد, [F5] ادمین با JWT معتبر به `/api/doc` (Swagger UI) دسترسی ندارد (401), [F6] ناسازگاری کدهای خطا بین دامنه‌ها (+10 more)
### Community 451 - "Community 451"
Cohesion: 0.09
Nodes (19): AddForm, addSchema, ClinicsPage(), HUES_LIST, EMPTY, PreRegistration, STATUS_META, STATUS_TABS (+11 more)
Cohesion: 0.08
Nodes (22): Contract, InsuranceOption, KIND_LABEL, ChargeForm, chargeSchema, EMPTY_LOGS, POST_VISIT_VARS, REMINDER_HOUR_OPTIONS (+14 more)
### Community 452 - "Community 452"
Cohesion: 0.33
@@ -2308,8 +2342,8 @@ Cohesion: 0.24
Nodes (10): gridItemStyle, JALALI_MONTHS, jalaliFirstWeekday(), jalaliToGregorian(), navBtnStyle, PersianCalendar(), pf, Props (+2 more)
### Community 459 - "Community 459"
Cohesion: 0.33
Nodes (6): API موجود (نیاز به تغییر ندارند), اپیک‌ها, اپیک ۳ — منشی (Secretary) — تکمیل, تغییرات مورد نیاز, توضیح, نیازمندی‌های کارکردی
Cohesion: 0.40
Nodes (5): API موجود (نیاز به تغییر ندارند), اپیک ۳ — منشی (Secretary) — تکمیل, تغییرات مورد نیاز, توضیح, نیازمندی‌های کارکردی
### Community 460 - "Community 460"
Cohesion: 0.33
@@ -2388,16 +2422,16 @@ Cohesion: 0.36
Nodes (5): StaffController, ClinicStaff, JsonResponse, Request, User
### Community 481 - "Community 481"
Cohesion: 0.39
Nodes (3): ProvinceRepository, ManagerRegistry, Province
Cohesion: 0.38
Nodes (5): Rate, RateRepository, Doctor, ManagerRegistry, User
### Community 482 - "Community 482"
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)
Cohesion: 0.43
Nodes (5): BeforeInstallPromptEvent, usePwaInstall(), PwaInstallBanner(), detectIOS(), PwaLoginCard()
### Community 483 - "Community 483"
Cohesion: 0.18
Nodes (5): MockGateway, PaymentGatewayInterface, PaymentInitResult, PaymentRefundResult, PaymentVerifyResult
Cohesion: 0.43
Nodes (3): InvoiceRepository, Invoice, ManagerRegistry
### Community 486 - "Community 486"
Cohesion: 0.40
@@ -2411,14 +2445,26 @@ Nodes (5): Errors, GET `/api/v1/representation/dashboard/summary`, GET `/api/v1/
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.39
Nodes (3): TagRepository, ManagerRegistry, Tag
### 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)
### Community 494 - "Community 494"
Cohesion: 0.43
Nodes (3): InvoiceItemRepository, InvoiceItem, ManagerRegistry
### Community 495 - "Community 495"
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)
@@ -2436,12 +2482,12 @@ Cohesion: 0.18
Nodes (10): license, private, scripts, build, dev, dev-server, test, test:cov (+2 more)
### Community 499 - "Community 499"
Cohesion: 0.40
Nodes (3): Props, StatTone, TONE
Cohesion: 0.47
Nodes (3): SiteContextController, JsonResponse, Request
### Community 500 - "Community 500"
Cohesion: 0.40
Nodes (5): API موجود (نیاز به endpoint جدید ندارد), اپیک ۷ — داشبورد هوشمند (Smart Dashboard), تغییر مورد نیاز, توضیح, نیازمندی‌های کارکردی
Cohesion: 0.33
Nodes (6): API موجود (نیاز به endpoint جدید ندارد), اپیک‌ها, اپیک ۷ — داشبورد هوشمند (Smart Dashboard), تغییر مورد نیاز, توضیح, نیازمندی‌های کارکردی
### Community 501 - "Community 501"
Cohesion: 0.40
@@ -2515,10 +2561,6 @@ Nodes (4): مجوزها در سیستم, مجوزهای منشی, نقش کار
Cohesion: 0.40
Nodes (4): ساختار فایل‌ها, معماری — تسک ۱۶: ماژول داشبورد دکتر, نمودار جریان, کوئری درآمد سالانه (بر اساس ماه‌های شمسی)
### Community 522 - "Community 522"
Cohesion: 0.33
Nodes (5): Like, LikeRepository, Comment, ManagerRegistry, User
### Community 523 - "Community 523"
Cohesion: 0.15
Nodes (12): افزودن اسم سایت شهر به پیامک کد تأیید (OTP) — به‌صورت اختیاری, زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, پروژه (+4 more)
@@ -2535,10 +2577,6 @@ Nodes (12): ارسال پیامک OTP فقط از طریق Kavenegar VerifyLooku
Cohesion: 0.50
Nodes (4): GET /api/v1/appointment-settings/slots — دریافت اسلات‌های خالی, POST /api/v1/appointment-settings/holidays — ثبت تعطیلی (فقط ادمین), POST /api/v1/appointment-settings/overrides — ثبت Override توسط دکتر, Task-09: API تنظیمات نوبت
### Community 527 - "Community 527"
Cohesion: 0.13
Nodes (5): RepositoryClassMappingTest, KernelTestCase, LoggerInterface, RanginehProvider, DbLoggerTest
### Community 528 - "Community 528"
Cohesion: 0.50
Nodes (4): GET /api/v1/categories/cities — شهرها, GET /api/v1/categories/states — استان‌ها, Task-08: API دسته‌بندی‌ها, سایر Endpoint های دسته‌بندی — الزامی
@@ -2579,10 +2617,6 @@ Nodes (12): اجبار ارسال همه پیامک‌ها از طریق Kaveneg
Cohesion: 0.22
Nodes (3): AppException, SlotTakenException, RuntimeException
### Community 542 - "Community 542"
Cohesion: 0.48
Nodes (3): DoctorInsuranceRepository, DoctorInsurance, ManagerRegistry
### Community 543 - "Community 543"
Cohesion: 0.47
Nodes (3): CommissionService, Payment, Representation
@@ -2620,8 +2654,8 @@ Cohesion: 0.50
Nodes (4): Errors, GET `/api/v1/representation/appointments`, Query Parameters, Response `200`
### Community 552 - "Community 552"
Cohesion: 0.32
Nodes (3): SmsService, SendSmsMessage, SmsProviderInterface
Cohesion: 0.53
Nodes (3): PaymentLog, PaymentLogRepository, ManagerRegistry
### Community 553 - "Community 553"
Cohesion: 0.50
@@ -2687,6 +2721,10 @@ Nodes (4): ریسک‌های بحرانی, ریسک‌های عملیاتی, ر
Cohesion: 0.12
Nodes (15): دیپلوی ClinicPro (Symfony) روی لیارا با Docker, زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی (کد واقعی), وظایف, پروژه (+7 more)
### Community 573 - "Community 573"
Cohesion: 0.40
Nodes (5): Errors, Notes, POST `/api/v1/notification-mobile/request-otp`, Request Body, Response `200`
### Community 577 - "Community 577"
Cohesion: 0.43
Nodes (3): SmsMessageController, JsonResponse, Request
@@ -2768,12 +2806,16 @@ Cohesion: 0.40
Nodes (5): 31. 🟡 `PATCH` patch, Request Body, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 626 - "Community 626"
Cohesion: 0.43
Nodes (3): InvoiceService, Invoice, PatientSession
Cohesion: 0.23
Nodes (7): Money, BillingCalculator, InvoiceService, CoverageRule, ShareBreakdown, Invoice, PatientSession
### Community 631 - "Community 631"
Cohesion: 0.67
Nodes (3): DELETE `/api/v1/representation/{uuid}`, Errors, Response `200`
Cohesion: 0.40
Nodes (5): Errors, GET `/api/v1/admin/clinic/{uuid}/invitations`, Path Parameters, Query Parameters, Response `200`
### Community 632 - "Community 632"
Cohesion: 0.40
Nodes (5): DELETE `/api/v1/billing/tenant-insurances/{uuid}`, GET `/api/v1/billing/tenant-insurances`, PATCH `/api/v1/billing/tenant-insurances/{uuid}`, POST `/api/v1/billing/tenant-insurances`, TenantInsurance — قراردادهای بیمه‌ی tenant (فاز ۱ سیستم صورتحساب)
### Community 633 - "Community 633"
Cohesion: 0.40
@@ -2787,10 +2829,6 @@ Nodes (3): ImageCropModalProps, createImage(), getCroppedImage()
Cohesion: 0.12
Nodes (16): Runbook — تشخیص «ری‌استارت» سرور: recycle عادی یا خرابی واقعی؟, اقدامات تکمیلی روی سرور (خارج از repo), تأیید روی سرور — اسکریپت آماده, جدول تفسیر خروجی اسکریپت, خلاصه یک‌خطی, علامت مشکل, چرا این اتفاق می‌افتاد (و فیکس اعمال‌شده), چک‌لیست رفع (+8 more)
### Community 640 - "Community 640"
Cohesion: 0.53
Nodes (4): Money, BillingCalculator, CoverageRule, ShareBreakdown
### Community 641 - "Community 641"
Cohesion: 0.39
Nodes (3): ClinicStaffRepository, ClinicStaff, ManagerRegistry
@@ -2840,13 +2878,17 @@ Cohesion: 0.31
Nodes (4): ClinicInvitationService, Clinic, ClinicDoctorInvitation, User
### Community 661 - "Community 661"
Cohesion: 0.53
Nodes (3): SessionServiceRepository, ManagerRegistry, SessionService
Cohesion: 0.40
Nodes (5): 32. 🔵 `POST` post, Request Body, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 662 - "Community 662"
Cohesion: 0.20
Nodes (10): Application Logs, DELETE `/api/v1/admin/logs`, GET `/api/v1/admin/logs`, GET `/api/v1/admin/logs/export`, Log Retention, Query Parameters, Query Parameters, Response `200` (+2 more)
### Community 667 - "Community 667"
Cohesion: 0.40
Nodes (5): 35. 🟡 `PATCH` patch, Request Body, مثال Request, هدرهای اضافی, پاسخ‌ها
### Community 671 - "Community 671"
Cohesion: 0.40
Nodes (5): Errors, PATCH `/api/v1/doctor/{uuid}`, Path Parameters, Request Body (`application/json`), Response `200`
@@ -2876,8 +2918,8 @@ Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/auth/switch-context`, Request Body, Response `200`
### Community 684 - "Community 684"
Cohesion: 0.40
Nodes (4): Altcha(), AltchaProps, FA_STRINGS, IntrinsicElements
Cohesion: 0.50
Nodes (4): Error Codes, POST `/api/v1/user/otp-login`, Request Body, Response `200`
### Community 686 - "Community 686"
Cohesion: 0.40
@@ -2903,6 +2945,10 @@ Nodes (4): GET `/api/v1/admin/representations`, Query Parameters, Representation
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`
### Community 695 - "Community 695"
Cohesion: 0.67
Nodes (3): DELETE `/api/v1/sms/template/{uuid}`, Errors, Response `200`
@@ -2923,6 +2969,10 @@ Nodes (4): Errors, POST `/file/upload/clinic_pro/doctor/field_image`, Request, R
Cohesion: 0.17
Nodes (11): رفع خطای `Class "SoapClient" not found` در پرداخت ملت (سرور prod), زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, پروژه (+3 more)
### Community 703 - "Community 703"
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`
@@ -2935,6 +2985,14 @@ Nodes (4): Errors, Path Parameters, POST `/api/v1/clinic-pro/doctor-address/from
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/user/register`, Request Body, Response `201`
### Community 708 - "Community 708"
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/user/login`, Request Body, Response `200`
### Community 710 - "Community 710"
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`
@@ -2955,28 +3013,120 @@ Nodes (3): POST `/api/v1/admin/sms/template/{uuid}/approve`, Request Body (`appl
Cohesion: 0.50
Nodes (4): Errors, GET `/oauth/userinfo`, Headers, Response `200`
### Community 717 - "Community 717"
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/notification-mobile/verify`, Request Body, Response `200`
### Community 718 - "Community 718"
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/user/verify-code`, Request Body, Response `200`
### Community 719 - "Community 719"
Cohesion: 0.50
Nodes (4): Errors, GET `/api/v1/admin/insurances`, Query Parameters, Response `200`
### Community 720 - "Community 720"
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/admin/insurance`, Request Body (`application/json`), Response `201`
### Community 721 - "Community 721"
Cohesion: 0.50
Nodes (4): Errors, PATCH `/api/v1/admin/insurance/{id}`, Path Parameters, Response `200`
### Community 723 - "Community 723"
Cohesion: 0.50
Nodes (4): Errors, POST `/api/v1/insurance/`, Request Body (`application/json`), Response `201`
### Community 724 - "Community 724"
Cohesion: 0.50
Nodes (4): Errors, PATCH `/api/v1/insurance/{id}`, Request Body, Response `200`
### Community 725 - "Community 725"
Cohesion: 0.50
Nodes (4): PUT `/api/v1/insurance-pricing`, Request Body, Response `200`, خطاها
### Community 726 - "Community 726"
Cohesion: 0.50
Nodes (4): Errors, GET `/api/v1/representation/doctors`, Query Parameters, Response `200`
### Community 727 - "Community 727"
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, هدرهای اضافی, پاسخ‌ها
### Community 731 - "Community 731"
Cohesion: 0.67
Nodes (3): Errors, GET `/api/v1/notification-mobile/{target}`, Response `200`
### Community 732 - "Community 732"
Cohesion: 0.67
Nodes (3): GET `/api/v1/billing/tenant-insurances/{uuid}/service-coverage`, PUT `/api/v1/billing/tenant-insurances/{uuid}/service-coverage`, TenantServiceCoverage — پوشش خدمت تحت یک قرارداد بیمه (فاز ۲)
### Community 733 - "Community 733"
Cohesion: 0.67
Nodes (3): GET `/api/v1/insurance-pricing`, Response `200`, خطاها
### Community 734 - "Community 734"
Cohesion: 0.67
Nodes (3): GET `/api/v1/insurances`, Query Parameters, Response `200`
### Community 735 - "Community 735"
Cohesion: 0.67
Nodes (3): POST `/api/v1/admin/insurance/{id}/upload-logo`, Request, Response `200`
### Community 736 - "Community 736"
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`
### Community 739 - "Community 739"
Cohesion: 0.67
Nodes (3): 36. 🟢 `GET` get my rate, هدرهای اضافی, پاسخ‌ها
### Community 740 - "Community 740"
Cohesion: 0.67
Nodes (3): 42. 🔴 `DELETE` delete, هدرهای اضافی, پاسخ‌ها
### Community 741 - "Community 741"
Cohesion: 0.67
Nodes (3): 43. 🟢 `GET` get, هدرهای اضافی, پاسخ‌ها
## Knowledge Gaps
- **4032 isolated node(s):** `ALLOWED_ROLES`, `Pricing`, `ImageCropModalProps`, `TenantInsurance`, `CoverageRow` (+4027 more)
- **4067 isolated node(s):** `ALLOWED_ROLES`, `Pricing`, `ImageCropModalProps`, `TenantInsurance`, `CoverageRow` (+4062 more)
These have ≤1 connection - possible missing edges or undocumented components.
- **149 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes.
- **144 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 684`, `Community 485`?**
_High betweenness centrality (0.068) - this node is a cross-community bridge._
- **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 308`, `Community 58`, `Community 59`, `Community 318`, `Community 64`, `Community 577`, `Community 75`, `Community 77`, `Community 607`, `Community 480`, `Community 230`, `Community 104`, `Community 107`, `Community 109`, `Community 380`, `Community 121`, `Community 122`, `Community 252`?**
_High betweenness centrality (0.042) - this node is a cross-community bridge._
- **Why does `MellatGateway` connect `Community 206` to `Community 16`, `Community 483`, `Community 20`, `Community 527`?**
_High betweenness centrality (0.024) - this node is a cross-community bridge._
- **Why does `Altcha` connect `Community 367` to `Community 0`, `Community 485`?**
_High betweenness centrality (0.067) - this node is a cross-community bridge._
- **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 435`, `Community 308`, `Community 59`, `Community 318`, `Community 64`, `Community 577`, `Community 75`, `Community 77`, `Community 607`, `Community 480`, `Community 230`, `Community 104`, `Community 107`, `Community 109`, `Community 499`, `Community 380`, `Community 121`, `Community 122`, `Community 252`?**
_High betweenness centrality (0.046) - this node is a cross-community bridge._
- **Why does `ApiTestCase` connect `Community 86` to `Community 640`, `Community 397`, `Community 22`, `Community 535`, `Community 534`, `Community 541`, `Community 169`, `Community 562`, `Community 565`, `Community 439`, `Community 574`, `Community 575`, `Community 594`, `Community 82`, `Community 609`, `Community 484`, `Community 618`, `Community 497`, `Community 371`?**
_High betweenness centrality (0.027) - this node is a cross-community bridge._
- **What connects `ALLOWED_ROLES`, `Pricing`, `ImageCropModalProps` to the rest of the system?**
_4032 weakly-connected nodes found - possible documentation gaps or missing edges._
_4067 weakly-connected nodes found - possible documentation gaps or missing edges._
- **Should `Community 0` be split into smaller, more focused modules?**
_Cohesion score 0.05004389815627744 - nodes in this community are weakly interconnected._
_Cohesion score 0.048087431693989074 - nodes in this community are weakly interconnected._
- **Should `Community 1` be split into smaller, more focused modules?**
_Cohesion score 0.028985507246376812 - nodes in this community are weakly interconnected._
- **Should `Community 2` 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
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_constant_errorcodes_php", "label": "ErrorCodes.php", "file_type": "code", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L1"}, {"id": "constant_errorcodes_errorcodes", "label": "ErrorCodes", "file_type": "code", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L5"}, {"id": "constant_errorcodes_errorcodes_message", "label": ".message()", "file_type": "code", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L107"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_constant_errorcodes_php", "target": "constant_errorcodes_errorcodes", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L5", "weight": 1.0}, {"source": "constant_errorcodes_errorcodes", "target": "constant_errorcodes_errorcodes_message", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Constant/ErrorCodes.php", "source_location": "L107", "weight": 1.0}], "raw_calls": []}
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
+2689 -827
View File
File diff suppressed because it is too large Load Diff
+45 -20
View File
@@ -425,8 +425,8 @@
"semantic_hash": ""
},
"assets/home/index.js": {
"mtime": 1783667976.259501,
"ast_hash": "59ed0812495bb819ff1a000ef15a9f40",
"mtime": 1783671823.3429978,
"ast_hash": "29883a5d4108818d3923623845f88f32",
"semantic_hash": ""
},
"assets/react/controllers/Hello.jsx": {
@@ -810,8 +810,8 @@
"semantic_hash": ""
},
"src/Admin/Controller/AdminApiController.php": {
"mtime": 1783663902.0176175,
"ast_hash": "1be6bc6af210c01303d49cf031d05593",
"mtime": 1783747617.3699927,
"ast_hash": "920703ac00621ed56bd030a02c077016",
"semantic_hash": ""
},
"src/Admin/Controller/AdminController.php": {
@@ -1095,8 +1095,8 @@
"semantic_hash": ""
},
"src/ClinicInvitation/Entity/ClinicDoctorInvitation.php": {
"mtime": 1782728407.1951423,
"ast_hash": "097b8f69845070f742ed6b200256d822",
"mtime": 1783747224.0887392,
"ast_hash": "3a35b6a87bac457ae3e399004e248956",
"semantic_hash": ""
},
"src/ClinicInvitation/Repository/ClinicDoctorInvitationRepository.php": {
@@ -1180,13 +1180,13 @@
"semantic_hash": ""
},
"src/Doctor/Controller/DoctorController.php": {
"mtime": 1783569345.7800236,
"ast_hash": "c056cab3f38fa4a224564ac3a2d4b728",
"mtime": 1783747336.6328127,
"ast_hash": "7828b41ebfdf61e887bf38d360c8a8e1",
"semantic_hash": ""
},
"src/Doctor/Entity/Doctor.php": {
"mtime": 1782728407.1960332,
"ast_hash": "2401a8b6ef745ef25853ac52850f0e13",
"mtime": 1783747550.8134816,
"ast_hash": "ee77b315870c0299bc02b08b3e3a8726",
"semantic_hash": ""
},
"src/Doctor/Entity/DoctorAddress.php": {
@@ -1370,8 +1370,8 @@
"semantic_hash": ""
},
"src/Payment/Gateway/MellatGateway.php": {
"mtime": 1783010796.4370883,
"ast_hash": "8cd5f8a90d5b00606c4d2f0d26832d8c",
"mtime": 1783747255.6455548,
"ast_hash": "32314cb20f2960066b05176a51447856",
"semantic_hash": ""
},
"src/Payment/Gateway/MockGateway.php": {
@@ -1535,8 +1535,8 @@
"semantic_hash": ""
},
"src/Shared/Constant/ErrorCodes.php": {
"mtime": 1783666097.015498,
"ast_hash": "6ad487190ee8ba21689c31580d564a4b",
"mtime": 1783747388.311953,
"ast_hash": "54ebad106e4dcfeda8d72953b0fb76bd",
"semantic_hash": ""
},
"src/Shared/Controller/BaseController.php": {
@@ -1560,8 +1560,8 @@
"semantic_hash": ""
},
"src/Shared/EventSubscriber/ExceptionSubscriber.php": {
"mtime": 1782747822.711537,
"ast_hash": "5f93fc4b1ded2b43397437c68599aa0a",
"mtime": 1783747430.565936,
"ast_hash": "7b9c0858f5030435c30e15082456829a",
"semantic_hash": ""
},
"src/Shared/EventSubscriber/SecurityHeadersSubscriber.php": {
@@ -2395,8 +2395,8 @@
"semantic_hash": ""
},
"docs/api/doctor.md": {
"mtime": 1783569345.7776468,
"ast_hash": "70d56cf0705dfbfa35d4ea282536acfe",
"mtime": 1783747505.4473846,
"ast_hash": "59b4a19e6e646fcde2b31cf8cd6ccc46",
"semantic_hash": ""
},
"docs/api/insurance.md": {
@@ -3805,8 +3805,8 @@
"semantic_hash": ""
},
"assets/admin/components/ui/Altcha.tsx": {
"mtime": 1783671609.167859,
"ast_hash": "7ad01997a2ec14f7effc2c7fe2b7f341",
"mtime": 1783672386.2113204,
"ast_hash": "0f7d0eb6c16728729b6698444f3775ac",
"semantic_hash": ""
},
"src/Shared/Captcha/AltchaService.php": {
@@ -3843,5 +3843,30 @@
"mtime": 1783671083.4752364,
"ast_hash": "92b8fc394a330b77058d252b26eb85e5",
"semantic_hash": ""
},
"migrations/Version20260711120000.php": {
"mtime": 1783747563.6199126,
"ast_hash": "5f26681aec61110a0f563a1637647f38",
"semantic_hash": ""
},
"src/Auth/Command/SystemOwnerCommand.php": {
"mtime": 1783747644.3113935,
"ast_hash": "ba2cafb8bf075489371912af24b34757",
"semantic_hash": ""
},
".claude/prompt/fix-server-error-logs-20260711.md": {
"mtime": 1783746878.999338,
"ast_hash": "07ba38da8be11ea753c616a29a133070",
"semantic_hash": ""
},
"docs/scenarios/irimc-doctor-import-ownership.html": {
"mtime": 1783692949.7169843,
"ast_hash": "65046faf3fcf2859db97a594fd66aeb1",
"semantic_hash": ""
},
"docs/scenarios/irimc-doctor-import-ownership.md": {
"mtime": 1783692850.4494128,
"ast_hash": "94803933a144c487e107395efcf6d764",
"semantic_hash": ""
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Doctor profile ownership for IRIMC (سازمان نظام پزشکی) import.
*
* Adds owner_status / source / source_ref / managed_by / claimed_at to `doctors`
* so imported doctors can be stored as "unclaimed" (managed by a system user)
* and later transferred to the real doctor. Existing rows become manual+claimed
* so their behavior is unchanged.
*/
final class Version20260711120000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Doctor profile ownership (owner_status, source, source_ref, managed_by, claimed_at) for IRIMC import';
}
public function up(Schema $schema): void
{
$this->addSql(<<<'SQL'
ALTER TABLE doctors
ADD owner_status VARCHAR(20) NOT NULL DEFAULT 'claimed',
ADD source VARCHAR(20) NOT NULL DEFAULT 'manual',
ADD source_ref VARCHAR(100) DEFAULT NULL,
ADD managed_by INT DEFAULT NULL,
ADD claimed_at INT DEFAULT NULL
SQL);
// فیلترِ «پزشکان بدون‌مالک» و تطبیق idempotency ایمپورت بر پایهٔ (source, medical_system_code)
$this->addSql('CREATE INDEX idx_doctors_owner ON doctors (owner_status)');
$this->addSql('CREATE INDEX idx_doctors_source ON doctors (source, medical_system_code)');
}
public function down(Schema $schema): void
{
$this->addSql('DROP INDEX idx_doctors_source ON doctors');
$this->addSql('DROP INDEX idx_doctors_owner ON doctors');
$this->addSql(<<<'SQL'
ALTER TABLE doctors
DROP owner_status,
DROP source,
DROP source_ref,
DROP managed_by,
DROP claimed_at
SQL);
}
}
+125
View File
@@ -10,6 +10,7 @@ use App\Shared\Service\InputValidator;
use App\Location\Entity\City;
use App\Clinic\Entity\Clinic;
use App\Doctor\Entity\Doctor;
use App\Location\Entity\Province;
use App\Specialty\Entity\Specialty;
use App\Payment\Entity\Payment;
use App\Rating\Entity\Comment;
@@ -28,6 +29,7 @@ use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\StreamedResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\CurrentUser;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[OA\Tag(name: 'Admin')]
@@ -442,6 +444,129 @@ class AdminApiController extends BaseController
return $this->success(['uuid' => $doctor->getUuid()], 201);
}
/**
* ایمپورت یک پزشک از سازمان نظام پزشکی (بدون شماره موبایل).
*
* برخلاف createDoctor، این اندپوینت موبایل نمی‌خواهد: برای هر پزشک یک «کاربر
* جانشین» غیرفعال با شناسهٔ مصنوعی ساخته می‌شود و پروفایل در وضعیت unclaimed
* ذخیره می‌گردد تا بعداً به پزشک واقعی منتقل شود. idempotent بر پایهٔ
* (source, medical_system_code): اجرای مجدد، رکورد موجود را به‌روزرسانی می‌کند.
*/
#[OA\Post(
path: '/api/v1/admin/doctors/import',
summary: 'Import an IRIMC doctor without a mobile number (unclaimed profile)',
security: [['bearerAuth' => []]],
requestBody: new OA\RequestBody(
required: true,
content: new OA\JsonContent(
required: ['name', 'medical_system_code'],
properties: [
new OA\Property(property: 'name', type: 'string'),
new OA\Property(property: 'medical_system_code', type: 'string'),
new OA\Property(property: 'source', type: 'string', default: 'irimc'),
new OA\Property(property: 'source_ref', type: 'string', nullable: true, description: 'profile_url یا شناسهٔ مبدأ'),
new OA\Property(property: 'gender', type: 'string', nullable: true),
new OA\Property(property: 'degree', type: 'string', nullable: true),
new OA\Property(property: 'info', type: 'string', nullable: true),
new OA\Property(property: 'specialties', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
new OA\Property(property: 'states', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
new OA\Property(property: 'cities', type: 'array', items: new OA\Items(type: 'integer'), nullable: true),
]
)
),
responses: [
new OA\Response(response: 201, description: 'Doctor imported (created)'),
new OA\Response(response: 200, description: 'Doctor already existed (updated or skipped)'),
new OA\Response(response: 422, description: 'Validation error'),
]
)]
#[Route('/api/v1/admin/doctors/import', methods: ['POST'])]
public function importDoctor(Request $request, #[CurrentUser] User $admin): JsonResponse
{
$data = json_decode($request->getContent(), true) ?? [];
$name = trim((string) ($data['name'] ?? ''));
$code = trim((string) ($data['medical_system_code'] ?? $data['medicalSystemCode'] ?? ''));
$source = trim((string) ($data['source'] ?? 'irimc')) ?: 'irimc';
if ($name === '') {
return $this->error(ErrorCodes::VALIDATION, 'نام الزامی است', 422, 'name');
}
if ($code === '') {
return $this->error(ErrorCodes::VALIDATION, 'کد نظام پزشکی الزامی است', 422, 'medical_system_code');
}
$doctorRepo = $this->em->getRepository(Doctor::class);
$userRepo = $this->em->getRepository(User::class);
// idempotency: همان پزشکِ منبع → به‌روزرسانی، نه ساخت تکراری
$doctor = $doctorRepo->findOneBy(['source' => $source, 'medicalSystemCode' => $code]);
$created = false;
// پروفایل تصاحب‌شده را با ایمپورت مجدد بازنویسی نکن (مالک واقعی اولویت دارد)
if ($doctor !== null && $doctor->getOwnerStatus() === 'claimed') {
return $this->success(['uuid' => $doctor->getUuid(), 'created' => false, 'skipped' => 'claimed']);
}
if ($doctor === null) {
// کاربر جانشینِ یکتا و غیرفعال؛ شناسهٔ مصنوعی قطعی از روی کد نظام پزشکی
$synthetic = 'imp_' . substr(md5($source . ':' . $code), 0, 14); // ≤ ۱۸ کاراکتر، ASCII، یکتا
$user = $userRepo->findOneBy(['mobileNumber' => $synthetic]);
if ($user === null) {
$user = new User($synthetic);
$user->setRealName($name);
$user->setStatus(0); // جانشین: هرگز لاگین نمی‌کند
$this->em->persist($user);
}
$doctor = new Doctor($user, $name);
$doctor->setSource($source);
$doctor->setOwnerStatus('unclaimed');
$doctor->setActiveDoctorAppointment(false); // تا مالک واقعی برنامهٔ کاری بسازد
$created = true;
}
// فیلدهای مشترک
$doctor->setName($name);
$doctor->setMedicalSystemCode($code);
$doctor->setManagedBy($admin->getId());
if (array_key_exists('source_ref', $data) || array_key_exists('profile_url', $data)) {
$doctor->setSourceRef($data['source_ref'] ?? $data['profile_url'] ?? null);
}
if (!empty($data['gender'])) $doctor->setGender($data['gender']);
if (!empty($data['degree'])) $doctor->setDegree($data['degree']);
if (array_key_exists('info', $data)) $doctor->setInfo($data['info']);
// روابط بر پایهٔ شناسه‌های مرجع (تخصص/استان/شهر)
$this->syncRefCollection($doctor->getSpecialties(), $data['specialties'] ?? null, Specialty::class);
$this->syncRefCollection($doctor->getProvinces(), $data['states'] ?? null, Province::class);
$this->syncRefCollection($doctor->getCities(), $data['cities'] ?? null, City::class);
$this->em->persist($doctor);
$this->em->flush();
return $this->success(
['uuid' => $doctor->getUuid(), 'created' => $created],
$created ? 201 : 200
);
}
/**
* یک مجموعهٔ ManyToMany پزشک را با آرایه‌ای از شناسه‌های مرجع همگام می‌کند.
* اگر $ids null باشد دست نمی‌خورد؛ اگر آرایه باشد، پاک و از نو پر می‌شود.
*/
private function syncRefCollection(\Doctrine\Common\Collections\Collection $col, ?array $ids, string $class): void
{
if ($ids === null) {
return;
}
$col->clear();
foreach ($ids as $id) {
$ref = $this->em->getRepository($class)->find((int) $id);
if ($ref !== null && !$col->contains($ref)) {
$col->add($ref);
}
}
}
// ── Clinics ───────────────────────────────────────────────────────────────
#[Route('/api/v1/admin/clinics', methods: ['GET'])]
+106
View File
@@ -0,0 +1,106 @@
<?php
namespace App\Auth\Command;
use App\Auth\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
/**
* کاربر «مالک سیستمی» (پیش‌فرض موبایل 0000000000) که کرالر با آن به API لاگین می‌کند
* تا پزشکان نظام پزشکی را وارد کند. ROLE_ADMIN دارد تا اندپوینت ایمپورت را صدا بزند.
*
* ساخت/به‌روزرسانی رمز و فعال‌سازی:
* php bin/console app:system-owner 0000000000 --password=secret --activate
* غیرفعال‌سازی پس از پایان کار کرالر:
* php bin/console app:system-owner 0000000000 --deactivate
*/
#[AsCommand(
name: 'app:system-owner',
description: 'Manage the system-owner user used by the IRIMC crawler (create/activate/deactivate).',
)]
class SystemOwnerCommand extends Command
{
public function __construct(
private readonly EntityManagerInterface $em,
private readonly UserPasswordHasherInterface $hasher,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addArgument('mobile', InputArgument::OPTIONAL, 'System-owner mobile identifier', '0000000000')
->addOption('password', 'p', InputOption::VALUE_REQUIRED, 'Set/replace the login password')
->addOption('activate', null, InputOption::VALUE_NONE, 'Activate the account (status=1)')
->addOption('deactivate', null, InputOption::VALUE_NONE, 'Deactivate the account (status=0)');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$mobile = (string) $input->getArgument('mobile');
$password = $input->getOption('password');
$activate = (bool) $input->getOption('activate');
$deactiv = (bool) $input->getOption('deactivate');
if ($activate && $deactiv) {
$io->error('--activate و --deactivate با هم مجاز نیستند.');
return Command::INVALID;
}
$repo = $this->em->getRepository(User::class);
$user = $repo->findOneBy(['mobileNumber' => $mobile]);
$existed = $user !== null;
if (!$existed) {
$user = new User($mobile);
$user->setRealName('مالک سیستمی (کرالر نظام پزشکی)');
if ($password === null) {
$io->error('برای ساخت کاربر جدید، --password الزامی است.');
return Command::INVALID;
}
}
$roles = $user->getRoles();
if (!in_array('ROLE_ADMIN', $roles, true)) {
$roles[] = 'ROLE_ADMIN';
$user->setRoles(array_values(array_unique($roles)));
}
if ($password !== null) {
$user->setPasswordHash($this->hasher->hashPassword($user, (string) $password));
}
// پیش‌فرضِ کاربرِ تازه: فعال، مگر آنکه --deactivate داده شده باشد.
if ($activate) {
$user->setStatus(1);
} elseif ($deactiv) {
$user->setStatus(0);
} elseif (!$existed) {
$user->setStatus(1);
}
$this->em->persist($user);
$this->em->flush();
$io->success(sprintf(
'%s system-owner %s (uuid=%s, status=%d, roles=%s)',
$existed ? 'Updated' : 'Created',
$mobile,
$user->getUuid(),
$user->getStatus(),
implode(',', $user->getRoles()),
));
return Command::SUCCESS;
}
}
@@ -74,7 +74,7 @@ class ClinicDoctorInvitation
$this->clinic = $clinic;
$this->invitedBy = $invitedBy;
$this->mobile = $mobile;
$this->token = bin2hex(random_bytes(48));
$this->token = bin2hex(random_bytes(16));
$this->invitedAt = time();
$this->expiresAt = $this->invitedAt + 72 * 3600;
}
@@ -94,7 +94,7 @@ class ClinicDoctorInvitation
public function refresh(): void
{
$this->token = bin2hex(random_bytes(48));
$this->token = bin2hex(random_bytes(16));
$this->tokenUsed = false;
$this->invitedAt = time();
$this->expiresAt = $this->invitedAt + 72 * 3600;
@@ -41,6 +41,7 @@ class DoctorController extends BaseController
private readonly UserRepository $userRepo,
private readonly FileValidatorService $fileValidator,
private readonly WeeklyScheduleRepository $scheduleRepo,
private readonly \App\Appointment\Repository\AppointmentRepository $appointmentRepo,
private readonly TenantInsuranceCleanupService $insuranceCleanup,
private readonly \App\Representation\Service\DomainContextResolver $domainResolver,
private readonly string $projectDir,
@@ -375,6 +376,10 @@ class DoctorController extends BaseController
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دکتر یافت نشد', 404);
}
if ($this->appointmentRepo->count(['doctor' => $doctor]) > 0) {
return $this->error(ErrorCodes::ERR_CONFLICT_001, 'این پزشک نوبت ثبت‌شده دارد و قابل حذف نیست', 409);
}
$this->insuranceCleanup->purgeForEntity(TenantInsurance::TYPE_DOCTOR, $doctor->getId());
$this->doctorRepo->remove($doctor);
return $this->success(['message' => 'دکتر با موفقیت حذف شد']);
+84
View File
@@ -77,6 +77,26 @@ class Doctor
#[ORM\Column(name: 'notification_mobile', type: 'string', length: 15, nullable: true)]
private ?string $notificationMobile = null;
// ── Profile ownership (IRIMC import) ───────────────────────────────────────
// owner_status: claimed | unclaimed | pending_transfer
#[ORM\Column(name: 'owner_status', type: 'string', length: 20)]
private string $ownerStatus = 'claimed';
// source: manual | irimc
#[ORM\Column(type: 'string', length: 20)]
private string $source = 'manual';
// شناسه رکورد مبدأ (profile_url یا کد نظام پزشکی) برای idempotency و ممیزی
#[ORM\Column(name: 'source_ref', type: 'string', length: 100, nullable: true)]
private ?string $sourceRef = null;
// شناسه کاربری که این پروفایلِ بدون‌مالک را وارد/مدیریت کرده (مثلاً کاربر سیستمی)
#[ORM\Column(name: 'managed_by', type: 'integer', nullable: true)]
private ?int $managedBy = null;
#[ORM\Column(name: 'claimed_at', type: 'integer', nullable: true)]
private ?int $claimedAt = null;
#[ORM\Column(name: 'created_at', type: 'integer')]
private int $createdAt;
@@ -200,6 +220,26 @@ class Doctor
{
return $this->notificationMobile;
}
public function getOwnerStatus(): string
{
return $this->ownerStatus;
}
public function getSource(): string
{
return $this->source;
}
public function getSourceRef(): ?string
{
return $this->sourceRef;
}
public function getManagedBy(): ?int
{
return $this->managedBy;
}
public function getClaimedAt(): ?int
{
return $this->claimedAt;
}
public function getCreatedAt(): int
{
return $this->createdAt;
@@ -312,6 +352,50 @@ class Doctor
$this->touch();
return $this;
}
public function setOwnerStatus(string $v): self
{
$this->ownerStatus = $v;
$this->touch();
return $this;
}
public function setSource(string $v): self
{
$this->source = $v;
$this->touch();
return $this;
}
public function setSourceRef(?string $v): self
{
$this->sourceRef = $v;
$this->touch();
return $this;
}
public function setManagedBy(?int $v): self
{
$this->managedBy = $v;
$this->touch();
return $this;
}
public function setClaimedAt(?int $v): self
{
$this->claimedAt = $v;
$this->touch();
return $this;
}
/**
* انتقال مالکیت پروفایلِ بدون‌مالک به کاربر واقعی پزشک.
* user_id را پر می‌کند، مدیریت سیستمی را برمی‌دارد و وضعیت را claimed می‌کند.
*/
public function transferOwnershipTo(User $user): self
{
$this->user = $user;
$this->managedBy = null;
$this->ownerStatus = 'claimed';
$this->claimedAt = time();
$this->touch();
return $this;
}
private function touch(): void
{
+3
View File
@@ -66,6 +66,9 @@ class MellatGateway implements PaymentGatewayInterface
/** SoapClient تنبل — فقط prod و فقط وقتی لازم شد ساخته می‌شود. */
private function soap(): \SoapClient
{
if (!class_exists(\SoapClient::class)) {
throw new \App\Shared\Exception\AppException(\App\Shared\Constant\ErrorCodes::ERR_PAYMENT_001, null, 503);
}
if ($this->soap === null) {
$this->soap = new \SoapClient($this->wsdlUrl(), [
'trace' => true,
+2
View File
@@ -102,6 +102,7 @@ class ErrorCodes
public const ERR_GONE = 'ERR_GONE';
public const ERR_MOVED = 'ERR_MOVED';
public const ERR_ACCESS_DENIED = 'ERR_ACCESS_DENIED';
public const ERR_METHOD_NOT_ALLOWED_001 = 'ERR_METHOD_NOT_ALLOWED_001';
public static function message(string $code): string
{
@@ -162,6 +163,7 @@ class ErrorCodes
self::ERR_GONE => 'این منبع دیگر در دسترس نیست',
self::ERR_MOVED => 'این منبع منتقل شده است',
self::ERR_ACCESS_DENIED => 'دسترسی مجاز نیست',
self::ERR_METHOD_NOT_ALLOWED_001 => 'متد درخواستی برای این آدرس مجاز نیست',
default => 'خطای ناشناخته',
};
}
@@ -2,12 +2,14 @@
namespace App\Shared\EventSubscriber;
use App\Shared\Constant\ErrorCodes;
use App\Shared\Exception\AppException;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
@@ -108,6 +110,21 @@ class ExceptionSubscriber implements EventSubscriberInterface
return;
}
// Bots/scanners and stray preflight hit disallowed methods on public paths (e.g. POST /).
// Client errors, not server faults: respond 405 and log at notice so they don't flood
// the error channel through the generic fallback below.
if ($exception instanceof MethodNotAllowedHttpException) {
$this->logger->notice('Method not allowed', [
'path' => $event->getRequest()->getPathInfo(),
'method' => $event->getRequest()->getMethod(),
]);
$event->setResponse(new JsonResponse(
['success' => false, 'data' => null, 'errors' => [['code' => ErrorCodes::ERR_METHOD_NOT_ALLOWED_001, 'message' => ErrorCodes::message(ErrorCodes::ERR_METHOD_NOT_ALLOWED_001)]]],
405
));
return;
}
// Generic fallback: never leak stack traces or internal details in API responses.
// The class/message/location go into the log MESSAGE itself so they surface in
// platforms (e.g. Liara) whose default logger only prints the message string.