From a4b07c2f8083e40bb65f3cef32b1a9e2b8444746 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sun, 19 Jul 2026 08:23:57 +0330 Subject: [PATCH] feat(blog): add city_id to blogs for city-specific scoping - Introduced a new nullable city_id column in the blogs table to allow scoping of blog posts to specific cities. - Updated Blog entity to include a ManyToOne relationship with the City entity. - Enhanced BlogController to handle city_id in the request, allowing filtering of posts by city. - Modified BlogRepository to support querying published posts based on city_id. - Added tests to ensure correct behavior for city-scoped and nationwide posts, including creation and updating of posts with city associations. --- assets/admin/pages/BlogFormPage.test.tsx | 4 + assets/admin/pages/BlogFormPage.tsx | 34 ++- assets/admin/types/index.ts | 2 + docs/api/blog.md | 24 +- graphify-out/GRAPH_REPORT.md | 327 +++++++++++------------ migrations/Version20260719044321.php | 42 +++ src/Blog/Controller/BlogController.php | 51 +++- src/Blog/Entity/Blog.php | 27 ++ src/Blog/Repository/BlogRepository.php | 23 +- tests/Blog/BlogCityScopeTest.php | 179 +++++++++++++ 10 files changed, 535 insertions(+), 178 deletions(-) create mode 100644 migrations/Version20260719044321.php create mode 100644 tests/Blog/BlogCityScopeTest.php diff --git a/assets/admin/pages/BlogFormPage.test.tsx b/assets/admin/pages/BlogFormPage.test.tsx index 2a882a2c..d7c1fdc6 100644 --- a/assets/admin/pages/BlogFormPage.test.tsx +++ b/assets/admin/pages/BlogFormPage.test.tsx @@ -16,9 +16,13 @@ import { api } from '@/lib/api'; import BlogFormPage from '@/pages/BlogFormPage'; const post = api.post as ReturnType; +const get = api.get as ReturnType; beforeEach(() => { post.mockReset(); + // فرم شهرها را برای انتخابگر «سراسری / شهر» می‌گیرد + get.mockReset(); + get.mockResolvedValue({ data: [], meta: { totalRecords: 0, totalPages: 0, currentPage: 1 } }); }); describe('BlogFormPage — اعتبارسنجی zod (حالت ساخت)', () => { diff --git a/assets/admin/pages/BlogFormPage.tsx b/assets/admin/pages/BlogFormPage.tsx index b6d6a296..62687f0e 100644 --- a/assets/admin/pages/BlogFormPage.tsx +++ b/assets/admin/pages/BlogFormPage.tsx @@ -9,7 +9,8 @@ import { CKEditor } from '@ckeditor/ckeditor5-react'; import ClassicEditor from '@ckeditor/ckeditor5-build-classic'; import { api } from '../lib/api'; import type { ApiResponse } from '../lib/api'; -import type { Blog } from '../types'; +import type { Blog, City } from '../types'; +import type { PaginatedResponse } from '../lib/api'; import { useAuthStore } from '../stores/authStore'; import PageHeader from '../components/ui/PageHeader'; import SearchableSelect from '../components/ui/SearchableSelect'; @@ -21,6 +22,8 @@ const schema = z.object({ tags: z.string().optional(), status: z.enum(['draft', 'published']), image_url: z.string().optional(), + // null = مقاله سراسری؛ حالت دائمی است نه مقدار تنظیم‌نشده + city_id: z.number().nullable().optional(), }); type FormData = z.infer; @@ -59,6 +62,13 @@ export default function BlogFormPage() { // detail endpoint is double-nested: success(['data' => $blog->toArray()]) const blog = data?.data?.data; + const citiesQuery = useQuery({ + queryKey: ['cities-select'], + queryFn: () => api.get>('/api/v1/admin/cities?limit=200'), + staleTime: 5 * 60_000, + }); + const cityOptions = (citiesQuery.data?.data ?? []).map((c) => ({ value: c.id, label: c.name })); + const { register, handleSubmit, control, watch, setValue, formState: { errors, isSubmitting } } = useForm({ resolver: zodResolver(schema), defaultValues: { status: 'draft' }, @@ -70,6 +80,7 @@ export default function BlogFormPage() { tags: blog.tags?.join(', ') ?? '', status: blog.status, image_url: blog.image_url ?? '', + city_id: blog.city ? Number(blog.city.id) : null, } : undefined, }); @@ -83,6 +94,8 @@ export default function BlogFormPage() { status: d.status, image_url: d.image_url ?? '', tags: d.tags ? d.tags.split(',').map((t) => t.trim()).filter(Boolean) : [], + // همیشه فرستاده می‌شود؛ null یعنی «سراسری» و باید شهر قبلی را پاک کند + city_id: d.city_id ?? null, }); const createMutation = useMutation({ @@ -235,6 +248,25 @@ export default function BlogFormPage() { /> +
+ + ( + field.onChange(v ? Number(v) : null)} + placeholder="سراسری (همه شهرها)" + /> + )} + /> +

+ مقاله سراسری روی همه دامنه‌های شهری نمایش داده می‌شود؛ مقاله شهری فقط به دامنه همان شهر نسبت داده می‌شود. +

+
+
🔗 مصرف‌کننده: سایت عمومی چند-دامنه‌ای (`nobat724_front`) با همین فیلد تصمیم می‌گیرد پست را روی دامنهٔ شهر canonical کند یا روی دامنهٔ اصلی، و در کدام sitemap بگذارد. تغییر معنای `null` قرارداد آن را می‌شکند. + --- ## GET `/api/v1/blog/{slug}` @@ -108,6 +126,7 @@ Create a new blog post. | `tags` | integer[] | ❌ | Array of tag IDs | | `status` | string | ❌ | `"draft"` (default) or `"published"` | | `image_url` | string | ❌ | Cover image path returned by the upload endpoint | +| `city_id` | integer\|null | ❌ | City this post belongs to. **Omitting it, or sending `null`/`0`, creates a nationwide post.** An unknown city id is rejected with `422`. | ### Response `201` ```json @@ -168,6 +187,8 @@ Update a blog post. All fields optional. Send `image_url: ""` to clear the cover image. +`city_id` follows PATCH semantics: **omit it and the post's city is left untouched**; send `null` (or `0`) to turn the post into a nationwide one; send a city id to move it to that city. + ### Response `200` Updated blog object. @@ -177,6 +198,7 @@ Updated blog object. | `ERR_AUTH_001` | 401 | Missing token | | `ERR_AUTH_006` | 403 | Not admin | | `ERR_NOT_FOUND_001` | 404 | Blog not found | +| `ERR_VALIDATION_002` | 422 | Unknown `city_id` | --- diff --git a/graphify-out/GRAPH_REPORT.md b/graphify-out/GRAPH_REPORT.md index 7f5b0202..c144f320 100644 --- a/graphify-out/GRAPH_REPORT.md +++ b/graphify-out/GRAPH_REPORT.md @@ -1,16 +1,16 @@ # Graph Report - clinicpro (2026-07-19) ## Corpus Check -- 1128 files · ~805,112 words +- 1130 files · ~806,671 words - Verdict: corpus is large enough that graph structure adds value. ## Summary -- 13286 nodes · 20627 edges · 984 communities (762 shown, 222 thin omitted) +- 13315 nodes · 20691 edges · 985 communities (759 shown, 226 thin omitted) - Extraction: 98% EXTRACTED · 2% INFERRED · 0% AMBIGUOUS · INFERRED: 424 edges (avg confidence: 0.8) - Token cost: 0 input · 0 output ## Graph Freshness -- Built from commit: `20bdc49e` +- Built from commit: `bd66b213` - Run `git rev-parse HEAD` and compare to check if the graph is stale. - Run `graphify update .` after code changes (no API cost). @@ -982,9 +982,10 @@ - [[_COMMUNITY_Community 981|Community 981]] - [[_COMMUNITY_Community 982|Community 982]] - [[_COMMUNITY_Community 983|Community 983]] +- [[_COMMUNITY_Community 984|Community 984]] ## God Nodes (most connected - your core abstractions) -1. `ApiTestCase` - 200 edges +1. `ApiTestCase` - 202 edges 2. `api` - 134 edges 3. `BaseController` - 96 edges 4. `formatRial()` - 88 edges @@ -1000,25 +1001,25 @@ assets/admin/App.tsx → assets/admin/stores/authStore.ts - `PublicRoute()` --calls--> `useAuthStore` [EXTRACTED] assets/admin/App.tsx → assets/admin/stores/authStore.ts +- `ClinicDoctorsManager()` --calls--> `formatNumber()` [EXTRACTED] + assets/admin/components/ClinicDoctorsManager.tsx → assets/admin/lib/utils.ts - `PatientCaseBanner()` --calls--> `formatDate()` [EXTRACTED] assets/admin/components/PatientCaseBanner.tsx → assets/admin/lib/utils.ts - `setup()` --calls--> `render()` [INFERRED] assets/admin/components/PatientRecordInfoForm.test.tsx → assets/admin/pages/ServiceDetailPage.test.tsx -- `ServiceItemFormModal()` --calls--> `numericField()` [EXTRACTED] - assets/admin/components/ServiceItemFormModal.tsx → assets/admin/lib/forms.ts ## Import Cycles - None detected. -## Communities (984 total, 222 thin omitted) +## Communities (985 total, 226 thin omitted) ### Community 0 - "Community 0" Cohesion: 0.06 -Nodes (18): ClinicAppointmentAccessTest, ClinicOwnerScheduleAccessTest, ClinicDoctorPermissionTest, ClinicDoctorPermissionRepository, ClinicDoctorPermission, ClinicDoctorPermissionChecker, Clinic, Doctor (+10 more) +Nodes (18): ClinicOwnerScheduleAccessTest, ClinicDoctorPermissionTest, ClinicDoctorPermissionRepository, ClinicDoctorPermission, ClinicRecordAccessTest, ClinicDoctorPermissionChecker, Clinic, Doctor (+10 more) ### Community 1 - "Community 1" -Cohesion: 0.03 -Nodes (79): grid, PatientFormOptions, patientFormSchema, PatientFormValues, Props, baseValues, options, setup() (+71 more) +Cohesion: 0.02 +Nodes (78): grid, patientFormSchema, Props, latinDigitsField(), NumericFieldProps, wrap(), cn(), digitsOnly() (+70 more) ### Community 2 - "Community 2" Cohesion: 0.10 @@ -1041,12 +1042,12 @@ Cohesion: 0.17 Nodes (8): SettlementController, SettlementRepository, Settlement, JsonResponse, Request, User, ManagerRegistry, User ### Community 7 - "Community 7" -Cohesion: 0.05 -Nodes (11): DoctorServiceController, DoctorService, Clinic, DoctorServiceRepository, Collection, Doctor, self, User (+3 more) +Cohesion: 0.06 +Nodes (8): DoctorService, Clinic, DoctorServiceRepository, Collection, Doctor, self, User, ManagerRegistry ### Community 8 - "Community 8" -Cohesion: 0.10 -Nodes (21): buildInsurancePayload(), Contract, contractToForm(), EMPTY_FORM, InsuranceFormValues, InsuranceModal(), InsuranceOption, KIND_LABEL (+13 more) +Cohesion: 0.04 +Nodes (56): AppointmentLike, ConfirmAppointmentModal(), METHOD_OPTIONS, Props, rowStyle, ServiceItem, ReplaceAppointmentModal(), toEpoch() (+48 more) ### Community 9 - "Community 9" Cohesion: 0.04 @@ -1066,7 +1067,7 @@ Nodes (49): Account provisioning, Clinic Doctor Invitation API, Console: `app:in ### Community 13 - "Community 13" Cohesion: 0.02 -Nodes (153): RoleRoute(), PickedService, ServicePick, ServiceSlot, ServiceSlotPicker(), get, services, appt (+145 more) +Nodes (157): RoleRoute(), PickedService, ServicePick, ServiceSlot, ServiceSlotPicker(), get, services, appt (+149 more) ### Community 14 - "Community 14" Cohesion: 0.09 @@ -1077,8 +1078,8 @@ Cohesion: 0.25 Nodes (7): PaymentController, Appointment, JsonResponse, Payment, Request, Response, User ### Community 16 - "Community 16" -Cohesion: 0.03 -Nodes (14): AppointmentExpiryServiceTest, PatientSession, SmsWallet, LogPruneService, SecretaryAppointmentScopeTest, AppointmentExpiryService, Appointment, Collection (+6 more) +Cohesion: 0.04 +Nodes (12): AppointmentExpiryServiceTest, PatientSession, SmsWallet, SecretaryAppointmentScopeTest, Appointment, Collection, InventoryPackage, PatientRecord (+4 more) ### Community 17 - "Community 17" Cohesion: 0.05 @@ -1097,12 +1098,12 @@ Cohesion: 0.12 Nodes (5): AdminApiController, RepresentationRepository, JsonResponse, Request, StreamedResponse ### Community 21 - "Community 21" -Cohesion: 0.03 -Nodes (68): ClinicDoctorItem, ClinicDoctorsManager(), ClinicInvitation, HUES_LIST, INV_STATUS_MAP, PatientTagsCell(), TenantTag, TauriStatCards() (+60 more) +Cohesion: 0.02 +Nodes (112): Invoice, InvoiceItem, InvoiceSummaryModal(), SessionConsumable, SessionData, SessionPayment, STATUS_LABEL, PatientTagsCell() (+104 more) ### Community 22 - "Community 22" -Cohesion: 0.15 -Nodes (9): RatingController, Like, LikeRepository, JsonResponse, Request, User, Comment, 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 @@ -1145,8 +1146,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.27 -Nodes (7): AbstractAuthenticator, AuthenticationException, Passport, PasswordAuthenticator, Request, Response, TokenInterface +Cohesion: 0.17 +Nodes (9): AbstractAuthenticator, AuthenticationException, ExceptionSubscriber, ExceptionEvent, Passport, PasswordAuthenticator, Request, Response (+1 more) ### Community 34 - "Community 34" Cohesion: 0.06 @@ -1190,7 +1191,7 @@ Nodes (28): 2.1 Value Objects, 2.2 Entities (جداول), Claim, Claim, Claim ( ### Community 44 - "Community 44" Cohesion: 0.12 -Nodes (3): Blog, self, User +Nodes (4): Blog, City, self, User ### Community 45 - "Community 45" Cohesion: 0.06 @@ -1209,8 +1210,8 @@ Cohesion: 0.11 Nodes (4): Payment, Appointment, self, User ### Community 49 - "Community 49" -Cohesion: 0.05 -Nodes (44): PaymentConfig, PaymentGatewayInfo, usePaymentConfig(), formatDate(), AdminDoctor, DEGREE_BADGE, DEGREE_LABEL, DoctorsPage() (+36 more) +Cohesion: 0.13 +Nodes (12): AdminUserDetail, AVATAR_COLORS, EditForm, editSchema, GENDER_LABELS, getPrimaryRole(), MARITAL_LABELS, MEDICAL_SECTIONS (+4 more) ### Community 50 - "Community 50" Cohesion: 0.07 @@ -1226,7 +1227,7 @@ Nodes (26): الزامات UI, باگ‌فیکس صفحه نوبت‌ها, با ### Community 53 - "Community 53" Cohesion: 0.06 -Nodes (17): AppointmentEvent, AppLogRepository, AppointmentEventRepository, ClaimItemRepository, DoctorClaimRequestRepository, PreRegistrationRepository, SiteConfigRepository, SpecialtyRepository (+9 more) +Nodes (20): ClaimStatusLog, AppLogRepository, ClaimItemRepository, ClaimStatusLogRepository, DoctorClaimRequestRepository, InvoiceItemRepository, PreRegistrationRepository, ProvinceRepository (+12 more) ### Community 54 - "Community 54" Cohesion: 0.10 @@ -1242,11 +1243,11 @@ Nodes (25): Endpoint ها, Fallback Logic, GET /api/v1/sms/balance, GET /api/v1/ ### Community 57 - "Community 57" Cohesion: 0.08 -Nodes (24): Blog API, DELETE `/api/v1/blog/{uuid}`, Errors, Errors, Errors, Errors, Errors, GET `/api/v1/blog/{slug}` (+16 more) +Nodes (25): Blog API, DELETE `/api/v1/blog/{uuid}`, Errors, Errors, Errors, Errors, Errors, GET `/api/v1/blog/{slug}` (+17 more) ### Community 58 - "Community 58" -Cohesion: 0.15 -Nodes (8): Blog, BlogController, BlogRepository, JsonResponse, Request, User, ManagerRegistry, QueryBuilder +Cohesion: 0.29 +Nodes (5): BlogController, City, JsonResponse, Request, User ### Community 59 - "Community 59" Cohesion: 0.22 @@ -1265,8 +1266,8 @@ Cohesion: 0.09 Nodes (28): MethodOption, Props, QUICK_TOMANS, WalletModalSubmit, WalletMode, WalletTransactionModal(), BANK_KEY, BankAccount (+20 more) ### Community 63 - "Community 63" -Cohesion: 0.01 -Nodes (183): ALLOWED_ROLES, PrivateRoute(), PublicRoute(), queryClient, toastStyle, PaginatedResponse, numericField(), emptyFeatures() (+175 more) +Cohesion: 0.02 +Nodes (161): ALLOWED_ROLES, PrivateRoute(), PublicRoute(), queryClient, toastStyle, PaginatedResponse, numericField(), formatDate() (+153 more) ### Community 64 - "Community 64" Cohesion: 0.29 @@ -1342,7 +1343,7 @@ Nodes (30): devDependencies, @babel/core, @babel/preset-env, @babel/preset-react ### Community 86 - "Community 86" Cohesion: 0.03 -Nodes (22): BookingModeImmutableTest, BookingServicesPublicTest, DateOverrideOwnershipTest, LowTierFixesTest, SendCodeMobileRateLimitTest, ClaimsListPaginationTest, CaptchaFlowTest, ServiceItemDetailApiTest (+14 more) +Nodes (24): BookingModeImmutableTest, BookingServicesPublicTest, DateOverrideOwnershipTest, ScheduleOwnershipTest, LowTierFixesTest, SendCodeMobileRateLimitTest, ClaimsListPaginationTest, CaptchaFlowTest (+16 more) ### Community 87 - "Community 87" Cohesion: 0.10 @@ -1365,8 +1366,8 @@ 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.50 -Nodes (4): Errors, PATCH `/api/v1/admin/doctor-service/{id}`, Path Parameters, Response `200` +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" Cohesion: 0.10 @@ -1625,8 +1626,8 @@ Cohesion: 0.12 Nodes (15): Endpoint ها, GET /api/v1/payment/{uuid}, POST /api/v1/payment, POST /api/v1/payment/callback/mellat, Strategy Pattern برای درگاه‌ها, Subscription Payment — POST /api/v1/subscription-payment, ⚠ امنیت: IP Whitelist برای Callback, ⚠ امنیت: جلوگیری از Open Redirect (+7 more) ### Community 160 - "Community 160" -Cohesion: 0.13 -Nodes (15): Date Overrides, DELETE `/api/v1/appointment-settings/date-override/{uuid}`, Errors, Errors, GET `/api/v1/appointment-settings/date-override/list/{doctorUuid}`, GET `/api/v1/appointment-settings/date-override/{uuid}`, PATCH `/api/v1/appointment-settings/date-override/{uuid}`, POST `/api/v1/appointment-settings/date-override` (+7 more) +Cohesion: 0.07 +Nodes (28): Access rule, Appointment Settings API, Available Locations, Booking context (`clinic_uuid`), Context additions (2026-07), Date Overrides, Date overrides are always per-context, DELETE `/api/v1/appointment-settings/date-override/{uuid}` (+20 more) ### Community 161 - "Community 161" Cohesion: 0.07 @@ -1661,8 +1662,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 (6): MellatGatewayTest, ErrorCodesTest, TimezoneTest, KavehNegarProviderTest, TestCase, KavehNegarProvider +Cohesion: 0.17 +Nodes (5): ErrorCodesTest, TimezoneTest, KavehNegarProviderTest, TestCase, KavehNegarProvider ### Community 170 - "Community 170" Cohesion: 0.13 @@ -1781,8 +1782,8 @@ Cohesion: 0.14 Nodes (13): Endpoint ها, PATCH /api/v1/secretary/{uuid}, POST /api/v1/secretary, تسک ۱۴: ماژول منشی, توضیح, زمان تخمینی, ساختار JSON, سیستم مجوزها — Resource-Based Permissions (مقیاس‌پذیر) (+5 more) ### Community 200 - "Community 200" -Cohesion: 0.06 -Nodes (25): DoctorTab, CANCELLED_STATUSES, EMPTY_SLOT_CONFIG, STATUS, turnStatusConfig, EMPTY_REASON_TEXT, emptySlot, occupiedSlot (+17 more) +Cohesion: 0.04 +Nodes (37): DoctorTab, td, th, CANCELLED_STATUSES, EMPTY_SLOT_CONFIG, STATUS, turnStatusConfig, EMPTY_REASON_TEXT (+29 more) ### Community 201 - "Community 201" Cohesion: 0.10 @@ -1805,8 +1806,8 @@ 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 @@ -1873,8 +1874,8 @@ Cohesion: 0.17 Nodes (11): تشخیص عمیق down شدن سرور بعد از ~۱۰ سیکل + وریفای و تکمیل فیکس‌های پایداری, زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, ۱. وریفای فیکس‌های repo (idempotent) (+3 more) ### Community 228 - "Community 228" -Cohesion: 0.04 -Nodes (52): 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 (+44 more) +Cohesion: 0.15 +Nodes (13): Bulk import / export, DELETE `/api/v1/admin/insurance/{id}`, DELETE `/api/v1/insurance/{id}`, EntityInsurancePricing — قیمت‌گذاری ویزیت بر اساس بیمه, GET `/api/v1/billing/tenant-insurances/{uuid}/service-coverage`, GET `/api/v1/insurance/{id}`, Insurance API, PUT `/api/v1/billing/tenant-insurances/{uuid}/service-coverage` (+5 more) ### Community 229 - "Community 229" Cohesion: 0.14 @@ -2125,12 +2126,12 @@ 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.21 -Nodes (5): PatientAttachmentTest, PatientAttachment, PatientAttachmentRepository, ManagerRegistry, PatientRecord +Cohesion: 0.33 +Nodes (4): PatientAttachment, PatientAttachmentRepository, ManagerRegistry, PatientRecord ### Community 295 - "Community 295" -Cohesion: 0.20 -Nodes (8): AbstractController, AdminController, HomeController, SeoController, Response, Response, Request, Response +Cohesion: 0.30 +Nodes (6): AbstractController, AdminController, SeoController, Response, Request, Response ### Community 296 - "Community 296" Cohesion: 0.22 @@ -2166,7 +2167,7 @@ Nodes (16): آماده‌سازی پروژه ClinicPro برای دیپلوی ر ### 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) +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 @@ -2202,7 +2203,7 @@ Nodes (4): InsuranceRepository, Insurance, InsuranceType, ManagerRegistry ### Community 314 - "Community 314" Cohesion: 0.02 -Nodes (127): AppointmentCardData, AppointmentTurnCard(), base, Invoice, InvoiceItem, InvoiceSummaryModal(), SessionConsumable, SessionData (+119 more) +Nodes (104): AppointmentCardData, AppointmentTurnCard(), base, Breadcrumb(), PatientCaseBanner(), Tag, PAYMENT_LABELS, SessionPaymentAccordion() (+96 more) ### Community 315 - "Community 315" Cohesion: 0.20 @@ -2212,10 +2213,6 @@ Nodes (6): AuthController, RateLimiterFactory, ClinicDoctorPermission, JsonRespo Cohesion: 0.12 Nodes (15): mapping صفحات clinicpro → فریم فیگما, الف-۱. توکن‌های رنگ (light + dark), الف-۲. سلکتور رنگ کاربر, الف-۳. ابعاد و رفتار layout, الف-۴. شعاع‌ها و input/button, الف-۵. کامپوننت‌های مشترک مطابق فیگما, ایندکس فریم‌های فیگما (۴ section، node-id دسکتاپ), بخش الف — سیستم طراحی مشترک (یک‌بار، پایه‌ی همه‌ی صفحات) (+7 more) -### Community 317 - "Community 317" -Cohesion: 0.29 -Nodes (3): CorsRegexEnvProcessor, EnvVarProcessorInterface, CorsRegexEnvProcessorTest - ### Community 318 - "Community 318" Cohesion: 0.32 Nodes (4): PatientListFilterTest, Doctor, PatientRecord, TenantTag @@ -2265,8 +2262,8 @@ Cohesion: 0.22 Nodes (8): Query های جدید, بیماران منحصربه‌فرد در بازه, درآمد بر اساس روز (از patient_sessions), فروش اشتراک بر اساس پنل (admin), نوبت‌ها بر اساس روز (admin chart), نکات مهم, هیچ migration لازم نیست, پایگاه داده — تسک ۱۶: داشبورد هوشمند ### Community 333 - "Community 333" -Cohesion: 0.29 -Nodes (7): DELETE `/api/v1/clinic-pro/doctor-address/{id}`, Doctor API, Errors, GET `/api/v1/clinic-pro/doctor-addresses/{doctorId}`, Path Parameters, Response `200`, Response `200` +Cohesion: 0.04 +Nodes (50): `city` / `state` در پاسخ لیست, DELETE `/api/v1/clinic-pro/doctor-address/{id}`, DELETE `/api/v1/doctor/{uuid}`, Doctor API, Errors, Errors, Errors, Errors (+42 more) ### Community 334 - "Community 334" Cohesion: 0.25 @@ -2530,7 +2527,7 @@ Nodes (30): انتقال برنامه کاری، اصلاح فرم تنظیما ### Community 407 - "Community 407" Cohesion: 0.16 -Nodes (11): MessageBusInterface, MockObject, SmsService, SmsServiceLookupOnlyTest, SmsLogRepository, SmsMessageTemplateRepository, SmsService, SmsTextResolver (+3 more) +Nodes (9): MessageBusInterface, MockObject, SmsTextResolver, SmsServiceLookupOnlyTest, SmsLogRepository, SmsMessageTemplateRepository, SmsService, SmsTextResolver (+1 more) ### Community 410 - "Community 410" Cohesion: 0.08 @@ -2541,16 +2538,16 @@ Cohesion: 0.10 Nodes (20): edge cases, خلاصهٔ خطاها و اولویت, راه‌حل, راه‌حل, راه‌حل, راه‌حل, رفع خطاهای لاگ سرور (production) — ۱۴۰۵/۰۴/۲۰, ریشه (+12 more) ### Community 416 - "Community 416" -Cohesion: 0.07 -Nodes (33): EMPTY_CATS, EMPTY_ITEMS, EMPTY_META, EMPTY_PACKAGES, EMPTY_STATS, InventoryItem, InventoryMeta, InventoryPackage (+25 more) +Cohesion: 0.05 +Nodes (47): EMPTY_CATS, EMPTY_ITEMS, EMPTY_META, EMPTY_PACKAGES, EMPTY_STATS, InventoryItem, InventoryMeta, InventoryPackage (+39 more) ### Community 418 - "Community 418" Cohesion: 0.17 Nodes (11): دیپلوی ClinicPro روی Coolify (Docker Compose), رفع اشکال, مراحل دیپلوی, معماری دیپلوی, نکات عملیاتی, چند دامنه فرانت‌اند (مهم), ۱. ساخت Resource در Coolify, ۲. اختصاص دامنه (+3 more) ### Community 421 - "Community 421" -Cohesion: 0.02 -Nodes (100): AppointmentLike, ConfirmAppointmentModal(), METHOD_OPTIONS, Props, rowStyle, ServiceItem, td, th (+92 more) +Cohesion: 0.03 +Nodes (64): Option, STATUS_OPTIONS, ClinicDoctorItem, ClinicDoctorsManager(), ClinicInvitation, HUES_LIST, INV_STATUS_MAP, addMinutes() (+56 more) ### Community 422 - "Community 422" Cohesion: 0.12 @@ -2577,16 +2574,16 @@ Cohesion: 0.18 Nodes (8): ErrorCodes, PatientController, JsonResponse, PatientRecord, PatientRecordScope, PatientSession, Request, User ### Community 435 - "Community 435" -Cohesion: 0.10 -Nodes (16): Command, BackfillAppointmentSessionsCommand, CancelExpiredAppointmentsCommand, PruneLogsCommand, RepairAcceptedInvitationsCommand, SeedSmsMessageTemplatesCommand, InputInterface, OutputInterface (+8 more) +Cohesion: 0.13 +Nodes (13): Command, CancelExpiredAppointmentsCommand, PruneLogsCommand, SeedCategoriesCommand, SystemOwnerCommand, InputInterface, OutputInterface, InputInterface (+5 more) ### Community 436 - "Community 436" Cohesion: 0.12 Nodes (16): `book()` عمومی — الگوی درستِ موجود (کپی از `AppointmentController::book`, خط ۲۴۲–۲۶۳), زمینه, فایل‌های مرتبط, فرم — بدون فیلد کد ملی (کپی از `AppointmentCreatePage.tsx`), مسیر ادمین — بیمار فقط با موبایل (کپی از `MyAppointmentsController::createAppointment`, خط ۸۱–۸۷), نوبت‌دهی ادمین بر اساس کد ملی + موبایل (پرونده یکتا با کد ملی), نکات مهم, هدف (+8 more) ### Community 437 - "Community 437" -Cohesion: 0.16 -Nodes (7): ExceptionSubscriber, NumericFieldNormalizerSubscriber, SecurityHeadersSubscriber, EventSubscriberInterface, ExceptionEvent, RequestEvent, ResponseEvent +Cohesion: 0.24 +Nodes (5): NumericFieldNormalizerSubscriber, SecurityHeadersSubscriber, EventSubscriberInterface, RequestEvent, ResponseEvent ### Community 438 - "Community 438" Cohesion: 0.26 @@ -2600,10 +2597,6 @@ Nodes (11): زمینه, صفحه Twig دعوت پزشک + کوتاه‌کردن 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 441 - "Community 441" -Cohesion: 0.16 -Nodes (7): AbstractMigration, Schema, Version20260609134112, Schema, Version20260619121047, Schema, Version20260713050014 - ### Community 442 - "Community 442" Cohesion: 0.22 Nodes (6): ServiceItemPackageAndAuditTest, Doctor, InventoryItem, InventoryPackage, ServiceItem, ServiceSection @@ -2721,8 +2714,8 @@ Cohesion: 0.10 Nodes (20): book() فعلی فقط slot_start/slot_end می‌گیرد, overlap واقعی از قبل درست است, زمینه, ساخت اسلاتِ ثابت (حالت فعلی = slot mode), فایل‌های مرتبط, متای برنامهٔ هفتگی, مدت خدمت — هست ولی استفاده نمی‌شود, نوبت‌دهی بر اساس مدت سرویس (Service-based booking) — Backend + Admin (+12 more) ### Community 479 - "Community 479" -Cohesion: 0.08 -Nodes (19): Bulk import / export, DELETE `/api/v1/admin/doctor-service/{id}`, Doctor Service API, Errors, Errors, GET `/api/v1/admin/doctor-services`, GET `/api/v1/doctor-services`, Query Parameters (+11 more) +Cohesion: 0.16 +Nodes (6): Authentication, ClinicPro — API Documentation Index, Error Code Reference, Modules, Persian digit normalization (global), Standard Response Envelope ### Community 481 - "Community 481" Cohesion: 0.17 @@ -2889,8 +2882,8 @@ Cohesion: 0.16 Nodes (3): BankAccount, self, User ### Community 533 - "Community 533" -Cohesion: 0.31 -Nodes (4): BaseKernel, Closure, MicroKernelTrait, Kernel +Cohesion: 0.19 +Nodes (6): BaseKernel, Closure, CorsRegexEnvProcessor, EnvVarProcessorInterface, MicroKernelTrait, Kernel ### Community 534 - "Community 534" Cohesion: 0.30 @@ -2985,8 +2978,8 @@ Cohesion: 0.50 Nodes (4): Errors, POST `/api/v1/admin/sms/template/{uuid}/reject`, Request Body, Response `200` ### Community 559 - "Community 559" -Cohesion: 0.25 -Nodes (4): SmsMessageTemplateRepository, SmsTextResolver, SmsMessageTemplate, ManagerRegistry +Cohesion: 0.22 +Nodes (6): SmsMessageController, SmsMessageTemplateRepository, SmsMessageTemplate, JsonResponse, Request, ManagerRegistry ### Community 560 - "Community 560" Cohesion: 0.50 @@ -3037,8 +3030,8 @@ Cohesion: 0.25 Nodes (8): ۲.۲ انواع دسته‌بندی (Category Types), ۲.۲.۱ تگ (Tag), ۲.۲.۲ استان (State), ۲.۲.۳ شهر (City), ۲.۲.۴ بیمه پایه (Basic Insurance), ۲.۲.۵ بیمه مکمل (Supplementary Insurance), ۲.۲.۶ تخصص دکتر (Doctor Specialty), ۲.۲.۷ خدمات دکتر (Doctor Services) ### Community 577 - "Community 577" -Cohesion: 0.08 -Nodes (33): useIssueInvoice(), dateStrToTs(), EDUCATION_OPTS, formValuesToPayload(), GENDER_OPTS, MARITAL_OPTS, profileToFormValues(), REFERRAL_OPTS (+25 more) +Cohesion: 0.07 +Nodes (38): PatientFormOptions, PatientFormValues, baseValues, options, setup(), dateStrToTs(), EDUCATION_OPTS, formValuesToPayload() (+30 more) ### Community 578 - "Community 578" Cohesion: 0.36 @@ -3129,8 +3122,8 @@ Cohesion: 0.34 Nodes (5): RepresentationController, JsonResponse, Representation, Request, User ### Community 608 - "Community 608" -Cohesion: 0.14 -Nodes (7): HealthController, RepositoryClassMappingTest, EntityManagerInterface, KernelTestCase, TenantInsuranceCleanupService, DbLoggerTest, JsonResponse +Cohesion: 0.19 +Nodes (6): BackfillAppointmentSessionsCommand, HealthController, EntityManagerInterface, InputInterface, OutputInterface, JsonResponse ### Community 612 - "Community 612" Cohesion: 0.67 @@ -3157,20 +3150,20 @@ Cohesion: 0.42 Nodes (4): WalletService, PatientSession, User, WalletTransaction ### Community 641 - "Community 641" -Cohesion: 0.23 -Nodes (7): PatientService, Appointment, DiscountRule, PatientRecord, PatientSession, SessionPayment, User +Cohesion: 0.11 +Nodes (14): Money, BillingCalculator, InvoiceService, PatientService, CoverageRule, ShareBreakdown, Invoice, PatientSession (+6 more) ### Community 646 - "Community 646" -Cohesion: 0.16 -Nodes (6): DoctorClaimRequest, DoctorClaimService, PersianTextTest, Doctor, User, PersianText +Cohesion: 0.13 +Nodes (3): NumericFieldNormalizerTest, PersianTextTest, PersianText ### Community 647 - "Community 647" Cohesion: 0.14 Nodes (13): اصلاح فیلتر شهر/استان در لیست عمومی پزشکان (`GET /api/v1/doctors`), زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, پروژه (+5 more) ### Community 648 - "Community 648" -Cohesion: 0.42 -Nodes (3): ServiceItemAuditService, ServiceItem, User +Cohesion: 0.19 +Nodes (7): ServiceItemAuditLogRepository, ServiceItemAuditService, ServiceItemAuditLog, ManagerRegistry, ServiceItem, ServiceItem, User ### Community 649 - "Community 649" Cohesion: 0.14 @@ -3241,8 +3234,8 @@ Cohesion: 0.31 Nodes (3): InventoryItemRepository, InventoryItem, ManagerRegistry ### Community 681 - "Community 681" -Cohesion: 0.36 -Nodes (5): ClinicRecordAccessTest, Clinic, Doctor, PatientRecord, User +Cohesion: 0.27 +Nodes (5): ClinicAppointmentAccessTest, Appointment, Clinic, Doctor, User ### Community 683 - "Community 683" Cohesion: 0.33 @@ -3321,16 +3314,16 @@ Cohesion: 0.12 Nodes (15): رفع باگ: نوبت‌های رزروشده در سایت عمومی «آزاد» نمایش داده می‌شوند, زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, پروژه (+7 more) ### Community 717 - "Community 717" -Cohesion: 0.12 -Nodes (6): ServiceItemDeleteCleanupTest, ServiceCoverageNPlusOneTest, TenantInsuranceCleanupTest, TenantServiceCoverageRepository, ManagerRegistry, TenantServiceCoverage +Cohesion: 0.11 +Nodes (6): ServiceCoverageNPlusOneTest, TenantInsuranceCleanupTest, TenantServiceCoverageRepository, TenantInsuranceCleanupService, ManagerRegistry, TenantServiceCoverage ### Community 718 - "Community 718" Cohesion: 0.36 Nodes (4): WalletTransactionRepository, ManagerRegistry, User, WalletTransaction ### Community 719 - "Community 719" -Cohesion: 0.39 -Nodes (3): ProvinceRepository, ManagerRegistry, Province +Cohesion: 0.33 +Nodes (3): BlogCityScopeTest, Blog, City ### Community 724 - "Community 724" Cohesion: 0.33 @@ -3357,8 +3350,12 @@ Cohesion: 0.15 Nodes (12): call siteهای فعلی PersianDateInput (نباید تغییر کنند — فقط برای اطمینان از سازگاری Props), زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, همه‌ی تقویم‌های پنل ادمین باید شمسی باشند (رفع تقویم میلادی PersianDateInput), وضعیت فعلی (کد مشکل‌دار), وظایف (+4 more) ### Community 733 - "Community 733" -Cohesion: 0.22 -Nodes (8): Access rule, Appointment Settings API, Available Locations, Booking context (`clinic_uuid`), Errors, `GET /api/v1/appointment-settings/available-locations/{doctorUuid}`, Response `200`, Slot Calculation Logic (Reference) +Cohesion: 0.19 +Nodes (5): UniqueConstraintsTest, FinancialBreakdown, FinancialBreakdownRepository, ManagerRegistry, Payment + +### Community 735 - "Community 735" +Cohesion: 0.16 +Nodes (7): AbstractMigration, Schema, Version20260621084558, Schema, Version20260624092459, Schema, Version20260716083939 ### Community 737 - "Community 737" Cohesion: 0.38 @@ -3381,8 +3378,8 @@ Cohesion: 0.50 Nodes (4): Error Codes, POST `/api/v1/pre-registration`, Request Body, Response `200` ### Community 745 - "Community 745" -Cohesion: 0.38 -Nodes (3): SystemOwnerCommand, InputInterface, OutputInterface +Cohesion: 0.29 +Nodes (4): BlogRepository, Blog, ManagerRegistry, QueryBuilder ### Community 747 - "Community 747" Cohesion: 0.12 @@ -3468,17 +3465,13 @@ Nodes (13): زمینه, فایل‌های مرتبط, مشکل / هدف, نکا Cohesion: 0.43 Nodes (5): BeforeInstallPromptEvent, usePwaInstall(), PwaInstallBanner(), detectIOS(), PwaLoginCard() -### Community 780 - "Community 780" -Cohesion: 0.16 -Nodes (6): BillingCalculatorTest, BillingCalculator, Money, BillingCalculator, CoverageRule, ShareBreakdown - ### Community 781 - "Community 781" Cohesion: 0.24 Nodes (4): InventoryPackageItem, InventoryItem, InventoryPackage, self ### Community 782 - "Community 782" -Cohesion: 0.21 -Nodes (7): FinancialBreakdown, FinancialBreakdownRepository, CommissionService, ManagerRegistry, Payment, Payment, Representation +Cohesion: 0.47 +Nodes (3): CommissionService, Payment, Representation ### Community 786 - "Community 786" Cohesion: 0.39 @@ -3537,8 +3530,8 @@ Cohesion: 0.25 Nodes (8): Errors, GET `/api/v1/patient/{uuid}/payments`, GET `/api/v1/patient/{uuid}/wallet`, GET `/api/v1/patient/{uuid}/wallet/transactions`, PATCH `/api/v1/session/{uuid}` — پرداخت مراجعه از کیف پول, POST `/api/v1/patient/{uuid}/wallet/charge`, POST `/api/v1/patient/{uuid}/wallet/withdraw`, مالی بیمار (Financials: پرداخت / تراکنش / کیف‌پول) ### Community 809 - "Community 809" -Cohesion: 0.14 -Nodes (3): LoggerInterface, KavehNegarProvider, RanginehProvider +Cohesion: 0.13 +Nodes (5): RepositoryClassMappingTest, KernelTestCase, LoggerInterface, RanginehProvider, DbLoggerTest ### Community 810 - "Community 810" Cohesion: 0.15 @@ -3552,6 +3545,10 @@ Nodes (5): Rate, RateRepository, Doctor, ManagerRegistry, User Cohesion: 0.38 Nodes (3): DiscountEngine, DiscountRule, PatientSession +### Community 824 - "Community 824" +Cohesion: 0.23 +Nodes (4): GatewayFactory, MellatGateway, MellatGatewayTest, SepGateway + ### Community 825 - "Community 825" Cohesion: 0.32 Nodes (5): SecretaryService, Clinic, Doctor, DoctorSecretary, User @@ -3593,12 +3590,12 @@ Cohesion: 0.47 Nodes (3): BookingContextResolver, Clinic, Doctor ### Community 840 - "Community 840" -Cohesion: 0.43 -Nodes (3): InvoiceItemRepository, InvoiceItem, ManagerRegistry +Cohesion: 0.42 +Nodes (4): DoctorClaimRequest, DoctorClaimService, Doctor, User ### Community 841 - "Community 841" -Cohesion: 0.40 -Nodes (5): Context additions (2026-07), Date overrides are always per-context, `GET /available-locations/{doctorUuid}`, Holidays are global by default, Response fields +Cohesion: 0.27 +Nodes (9): AddTurn(), PatientsCategoryView(), PatientsGridView(), SearchHeaderP(), TurnsFilter(), countFilters(), dayBound(), EMPTY (+1 more) ### Community 842 - "Community 842" Cohesion: 0.43 @@ -3628,6 +3625,10 @@ Nodes (3): CityRepository, City, ManagerRegistry Cohesion: 0.42 Nodes (3): DetachDoctorPermissionTest, Clinic, Doctor +### Community 852 - "Community 852" +Cohesion: 0.36 +Nodes (3): DoctorServiceController, JsonResponse, Request + ### Community 853 - "Community 853" Cohesion: 0.27 Nodes (3): SessionAuditLog, PatientSession, self @@ -3692,6 +3693,10 @@ Nodes (13): افزودن شهر به بلاگ (پیش‌نیاز بلاگ شهر Cohesion: 0.35 Nodes (6): ClinicDoctorPermissionController, Clinic, Doctor, JsonResponse, Request, User +### Community 874 - "Community 874" +Cohesion: 0.31 +Nodes (3): SpecialtyRepository, ManagerRegistry, Specialty + ### Community 875 - "Community 875" Cohesion: 0.48 Nodes (3): SessionServiceRepository, ManagerRegistry, SessionService @@ -3712,10 +3717,6 @@ Nodes (3): GET `/api/v1/representation/{uuid}/dashboard/yearly`, Query Parameter Cohesion: 0.15 Nodes (12): زمینه, فایل‌های مرتبط, فرآیند ثبت و قطعی کردن نوبت (مودال پرداخت + پرونده), مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, پروژه (+4 more) -### Community 890 - "Community 890" -Cohesion: 0.40 -Nodes (5): Errors, GET `/api/v1/clinic/my-doctor/{doctorUuid}`, Path Parameters, Response `200`, Schedule Fields Notes - ### Community 899 - "Community 899" Cohesion: 0.15 Nodes (12): دسترسی پزشک و مدیر کلینیک به پرونده‌های کلینیک, زمینه, فایل‌های مرتبط, مشکل / هدف, نکات مهم, وضعیت فعلی, وظایف, پروژه (+4 more) @@ -3733,8 +3734,8 @@ Cohesion: 0.40 Nodes (5): Errors, GET `/api/v1/clinic/doctor-list/{clinicUuid}`, Path Parameters, Query Parameters, Response `200` ### Community 903 - "Community 903" -Cohesion: 0.18 -Nodes (8): FormValues, schema, SectionDef, SectionId, SECTIONS, Settings, SettingsPage(), TaxHistoryRow +Cohesion: 0.43 +Nodes (3): AppointmentEvent, AppointmentEventRepository, ManagerRegistry ### Community 904 - "Community 904" Cohesion: 0.41 @@ -3744,10 +3745,6 @@ Nodes (4): SessionInsuranceShareTest, Doctor, Insurance, ServiceItem Cohesion: 0.18 Nodes (10): زمینه, نکات مهم, هدف, وظایف, پاک‌سازی رکوردهای آلودهٔ پزشک و کلینیک, پروژه, ۱. گزارش دامنهٔ آلودگی (اول اندازه‌گیری، بعد حذف), ۲. پاک‌سازی (+2 more) -### Community 907 - "Community 907" -Cohesion: 0.40 -Nodes (5): Errors, PATCH `/api/v1/doctor/{uuid}`, Path Parameters, Request Body (`application/json`), Response `200` - ### Community 908 - "Community 908" Cohesion: 0.50 Nodes (4): GET `/api/v1/admin/secretaries`, Query Parameters, Response `200`, Secretary Management @@ -3785,8 +3782,8 @@ Cohesion: 0.50 Nodes (4): 38. 🟢 `GET` Unapproved comments, هدرهای اضافی, پارامترهای Query, پاسخ‌ها ### Community 920 - "Community 920" -Cohesion: 0.31 -Nodes (3): InvoiceService, Invoice, PatientSession +Cohesion: 0.38 +Nodes (3): RepairAcceptedInvitationsCommand, InputInterface, OutputInterface ### Community 923 - "Community 923" Cohesion: 0.25 @@ -3797,17 +3794,13 @@ Cohesion: 0.22 Nodes (9): Clinic API, Clinic Doctor Permissions, Errors, GET `/api/v1/clinics`, POST `/file/upload/clinic_pro/clinic/field_clinic_logo`, Query Parameters, Request, Response `200` (+1 more) ### Community 926 - "Community 926" -Cohesion: 0.36 -Nodes (4): ServiceItemAuditLogRepository, ServiceItemAuditLog, ManagerRegistry, ServiceItem +Cohesion: 0.47 +Nodes (3): SeedSmsMessageTemplatesCommand, InputInterface, OutputInterface ### Community 928 - "Community 928" Cohesion: 0.43 Nodes (3): SmsTemplateRepository, SmsTemplate, ManagerRegistry -### Community 929 - "Community 929" -Cohesion: 0.40 -Nodes (5): Errors, PATCH `/api/v1/clinic-pro/doctor-address/{id}`, Path Parameters, Request Body, Response `200` - ### Community 930 - "Community 930" Cohesion: 0.50 Nodes (3): PatientRecordScopeResolver, PatientRecordScope, User @@ -3816,33 +3809,17 @@ Nodes (3): PatientRecordScopeResolver, PatientRecordScope, User Cohesion: 0.43 Nodes (4): AppointmentConfirmationService, Appointment, PatientSession, User -### Community 932 - "Community 932" -Cohesion: 0.50 -Nodes (4): DELETE `/api/v1/doctor/{uuid}`, Errors, Path Parameters, Response `200` - -### Community 933 - "Community 933" -Cohesion: 0.43 -Nodes (3): ClaimStatusLog, ClaimStatusLogRepository, ManagerRegistry - -### Community 934 - "Community 934" -Cohesion: 0.50 -Nodes (4): Errors, POST `/api/v1/doctor`, Request Body (`application/json`), Response `201` - ### Community 935 - "Community 935" -Cohesion: 0.50 -Nodes (4): Errors, GET `/api/v1/doctor/{uuid}`, Path Parameters, Response `200` - -### Community 936 - "Community 936" -Cohesion: 0.50 -Nodes (4): Errors, POST `/file/upload/clinic_pro/doctor/field_image`, Request, Response `200` +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 937 - "Community 937" Cohesion: 0.38 Nodes (3): NormalizeScheduleFormatCommand, InputInterface, OutputInterface ### Community 938 - "Community 938" -Cohesion: 0.43 -Nodes (3): SmsMessageController, JsonResponse, Request +Cohesion: 0.50 +Nodes (4): Errors, GET `/api/v1/admin/insurances`, Query Parameters, Response `200` ### Community 939 - "Community 939" Cohesion: 0.29 @@ -3850,28 +3827,40 @@ Nodes (3): ApiError, { refreshMock, logoutMock }, replaceMock ### Community 940 - "Community 940" Cohesion: 0.50 -Nodes (4): Errors, POST `/api/v1/clinic-pro/doctor-address`, Request Body, Response `201` +Nodes (4): Errors, POST `/api/v1/admin/insurance`, Request Body (`application/json`), Response `201` ### Community 941 - "Community 941" -Cohesion: 0.43 -Nodes (3): SmsLogRepository, SmsLog, ManagerRegistry +Cohesion: 0.20 +Nodes (6): SmsLogRepository, SmsService, SmsLog, ManagerRegistry, SendSmsMessage, SmsProviderInterface ### Community 942 - "Community 942" Cohesion: 0.48 Nodes (3): SmsSettingsRepository, SmsSettings, ManagerRegistry ### Community 943 - "Community 943" -Cohesion: 0.47 -Nodes (3): SeedCategoriesCommand, InputInterface, OutputInterface +Cohesion: 0.50 +Nodes (4): Errors, PATCH `/api/v1/admin/insurance/{id}`, Path Parameters, Response `200` + +### Community 944 - "Community 944" +Cohesion: 0.50 +Nodes (4): Errors, POST `/api/v1/insurance/`, Request Body (`application/json`), Response `201` ### Community 945 - "Community 945" Cohesion: 0.50 -Nodes (4): Errors, Path Parameters, POST `/api/v1/clinic-pro/doctor-address/from-clinic/{clinicUuid}`, Response `201` +Nodes (4): Errors, PATCH `/api/v1/insurance/{id}`, Request Body, Response `200` ### Community 950 - "Community 950" Cohesion: 0.53 Nodes (3): PaymentLog, PaymentLogRepository, ManagerRegistry +### Community 951 - "Community 951" +Cohesion: 0.50 +Nodes (4): GET `/api/v1/insurance-pricing`, Query Parameters, Response `200`, خطاها + +### Community 952 - "Community 952" +Cohesion: 0.50 +Nodes (4): PUT `/api/v1/insurance-pricing`, Request Body, Response `200`, خطاها + ### Community 953 - "Community 953" Cohesion: 0.40 Nodes (5): Appointment API, Errors, GET `/api/v1/appointment-booking-services/{doctorUuid}`, Response `200`, Single-appointment access model @@ -3940,13 +3929,9 @@ Nodes (4): Errors, GET `/api/v1/appointment/{uuid}/events`, Path Parameters, Res Cohesion: 0.50 Nodes (4): GET /api/v1/my/appointments, GET `/api/v1/my/appointments/today-stats`, Query Parameters, Response `200` -### Community 971 - "Community 971" -Cohesion: 0.50 -Nodes (4): `city` / `state` در پاسخ لیست, GET `/api/v1/doctors`, Query Parameters, Response `200` - ### Community 972 - "Community 972" -Cohesion: 0.50 -Nodes (4): Errors, POST `/api/v1/admin/doctor-service`, Request Body (`application/json`), Response `201` +Cohesion: 0.67 +Nodes (3): GET `/api/v1/insurances`, Query Parameters, Response `200` ### Community 973 - "Community 973" Cohesion: 0.50 @@ -3978,7 +3963,7 @@ Nodes (3): POST `/api/v1/admin/sms/template/{uuid}/approve`, Request Body (`appl ### Community 980 - "Community 980" Cohesion: 0.67 -Nodes (3): 29. 🟢 `GET` clinic list 🆕, پارامترهای Query, پاسخ‌ها +Nodes (3): POST `/api/v1/admin/insurance/{id}/upload-logo`, Request, Response `200` ### Community 981 - "Community 981" Cohesion: 0.67 @@ -3992,25 +3977,29 @@ Nodes (3): 42. 🔴 `DELETE` delete, هدرهای اضافی, پاسخ‌ها Cohesion: 0.67 Nodes (3): 43. 🟢 `GET` get, هدرهای اضافی, پاسخ‌ها +### Community 984 - "Community 984" +Cohesion: 0.67 +Nodes (3): 36. 🟢 `GET` get my rate, هدرهای اضافی, پاسخ‌ها + ## Knowledge Gaps -- **5108 isolated node(s):** `ALLOWED_ROLES`, `get`, `patch`, `appt`, `ModalKind` (+5103 more) +- **5110 isolated node(s):** `ALLOWED_ROLES`, `get`, `patch`, `appt`, `ModalKind` (+5105 more) These have ≤1 connection - possible missing edges or undocumented components. -- **222 thin communities (<3 nodes) omitted from report** — run `graphify query` to explore isolated nodes. +- **226 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 923`, `Community 485`?** - _High betweenness centrality (0.106) - this node is a cross-community bridge._ -- **Why does `ApiTestCase` connect `Community 86` to `Community 0`, `Community 640`, `Community 646`, `Community 775`, `Community 904`, `Community 779`, `Community 652`, `Community 16`, `Community 784`, `Community 658`, `Community 403`, `Community 659`, `Community 531`, `Community 918`, `Community 534`, `Community 535`, `Community 921`, `Community 538`, `Community 539`, `Community 541`, `Community 927`, `Community 161`, `Community 804`, `Community 294`, `Community 424`, `Community 681`, `Community 685`, `Community 431`, `Community 815`, `Community 817`, `Community 562`, `Community 690`, `Community 565`, `Community 951`, `Community 952`, `Community 697`, `Community 442`, `Community 826`, `Community 828`, `Community 445`, `Community 574`, `Community 575`, `Community 318`, `Community 962`, `Community 837`, `Community 710`, `Community 583`, `Community 715`, `Community 717`, `Community 594`, `Community 82`, `Community 852`, `Community 851`, `Community 856`, `Community 480`, `Community 609`, `Community 482`, `Community 483`, `Community 484`, `Community 608`, `Community 486`, `Community 743`, `Community 871`, `Community 744`, `Community 874`, `Community 107`, `Community 492`, `Community 497`, `Community 887`, `Community 633`, `Community 765`?** - _High betweenness centrality (0.048) - this node is a cross-community bridge._ + _High betweenness centrality (0.105) - this node is a cross-community bridge._ +- **Why does `BaseController` connect `Community 108` to `Community 771`, `Community 6`, `Community 265`, `Community 138`, `Community 139`, `Community 14`, `Community 654`, `Community 655`, `Community 529`, `Community 15`, `Community 275`, `Community 20`, `Community 22`, `Community 26`, `Community 676`, `Community 295`, `Community 559`, `Community 433`, `Community 438`, `Community 58`, `Community 59`, `Community 315`, `Community 699`, `Community 444`, `Community 64`, `Community 75`, `Community 77`, `Community 852`, `Community 861`, `Community 607`, `Community 739`, `Community 230`, `Community 104`, `Community 873`, `Community 107`, `Community 109`, `Community 494`, `Community 245`, `Community 121`, `Community 122`, `Community 252`?** + _High betweenness centrality (0.054) - this node is a cross-community bridge._ +- **Why does `ApiTestCase` connect `Community 86` to `Community 0`, `Community 640`, `Community 646`, `Community 775`, `Community 904`, `Community 907`, `Community 652`, `Community 779`, `Community 16`, `Community 784`, `Community 658`, `Community 403`, `Community 659`, `Community 531`, `Community 918`, `Community 22`, `Community 534`, `Community 921`, `Community 538`, `Community 539`, `Community 535`, `Community 541`, `Community 927`, `Community 161`, `Community 929`, `Community 804`, `Community 424`, `Community 681`, `Community 685`, `Community 431`, `Community 815`, `Community 817`, `Community 562`, `Community 690`, `Community 565`, `Community 697`, `Community 442`, `Community 826`, `Community 828`, `Community 445`, `Community 574`, `Community 575`, `Community 318`, `Community 837`, `Community 710`, `Community 583`, `Community 840`, `Community 715`, `Community 717`, `Community 719`, `Community 594`, `Community 82`, `Community 851`, `Community 856`, `Community 733`, `Community 480`, `Community 609`, `Community 482`, `Community 483`, `Community 484`, `Community 608`, `Community 486`, `Community 743`, `Community 871`, `Community 744`, `Community 107`, `Community 492`, `Community 497`, `Community 887`, `Community 633`, `Community 765`?** + _High betweenness centrality (0.052) - this node is a cross-community bridge._ - **What connects `ALLOWED_ROLES`, `get`, `patch` to the rest of the system?** - _5108 weakly-connected nodes found - possible documentation gaps or missing edges._ + _5110 weakly-connected nodes found - possible documentation gaps or missing edges._ - **Should `Community 0` be split into smaller, more focused modules?** - _Cohesion score 0.06138975966562173 - nodes in this community are weakly interconnected._ + _Cohesion score 0.06392405063291139 - nodes in this community are weakly interconnected._ - **Should `Community 1` be split into smaller, more focused modules?** - _Cohesion score 0.025068870523415977 - nodes in this community are weakly interconnected._ + _Cohesion score 0.024390243902439025 - nodes in this community are weakly interconnected._ - **Should `Community 2` be split into smaller, more focused modules?** - _Cohesion score 0.09523809523809523 - nodes in this community are weakly interconnected._ -- **Should `Community 3` be split into smaller, more focused modules?** _Cohesion score 0.09523809523809523 - nodes in this community are weakly interconnected._ \ No newline at end of file diff --git a/migrations/Version20260719044321.php b/migrations/Version20260719044321.php new file mode 100644 index 00000000..14dd4f59 --- /dev/null +++ b/migrations/Version20260719044321.php @@ -0,0 +1,42 @@ +addSql('ALTER TABLE blogs ADD city_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE blogs ADD CONSTRAINT FK_F41BCA708BAC62AF FOREIGN KEY (city_id) REFERENCES cities (id) ON DELETE SET NULL'); + $this->addSql('CREATE INDEX idx_blogs_city ON blogs (city_id)'); + } + + public function down(Schema $schema): void + { + $this->addSql('ALTER TABLE blogs DROP FOREIGN KEY FK_F41BCA708BAC62AF'); + $this->addSql('DROP INDEX idx_blogs_city ON blogs'); + $this->addSql('ALTER TABLE blogs DROP city_id'); + } +} diff --git a/src/Blog/Controller/BlogController.php b/src/Blog/Controller/BlogController.php index 2503787a..168c3b40 100644 --- a/src/Blog/Controller/BlogController.php +++ b/src/Blog/Controller/BlogController.php @@ -5,6 +5,7 @@ namespace App\Blog\Controller; use App\Auth\Entity\User; use App\Blog\Entity\Blog; use App\Blog\Repository\BlogRepository; +use App\Location\Repository\CityRepository; use App\Shared\Constant\ErrorCodes; use App\Shared\Controller\BaseController; use App\Shared\Service\FileValidatorService; @@ -20,10 +21,31 @@ class BlogController extends BaseController { public function __construct( private readonly BlogRepository $blogRepo, + private readonly CityRepository $cityRepo, private readonly FileValidatorService $fileValidator, private readonly string $projectDir, ) {} + /** + * city_id ورودی ادمین را به Entity تبدیل می‌کند. + * مقدار خالی/صفر/null یعنی «سراسری» و عمداً به null نگاشت می‌شود. + * + * @throws \App\Shared\Exception\AppException وقتی شناسهٔ شهر نامعتبر باشد + */ + private function resolveCity(mixed $cityId): ?\App\Location\Entity\City + { + if ($cityId === null || $cityId === '' || (int) $cityId === 0) { + return null; + } + + $city = $this->cityRepo->find((int) $cityId); + if ($city === null) { + throw new \App\Shared\Exception\AppException(ErrorCodes::ERR_VALIDATION_002, 'شهر یافت نشد', 422); + } + + return $city; + } + // ── Public list/detail ──────────────────────────────────────────────────── #[OA\Get( @@ -32,6 +54,13 @@ class BlogController extends BaseController parameters: [ new OA\Parameter(name: 'page', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 1)), new OA\Parameter(name: 'limit', in: 'query', required: false, schema: new OA\Schema(type: 'integer', default: 20, maximum: 50)), + new OA\Parameter( + name: 'city_id', + in: 'query', + required: false, + description: 'Scope to one city: returns that city\'s posts plus nationwide posts (city_id IS NULL). Omit to return every published post.', + schema: new OA\Schema(type: 'integer') + ), ], responses: [ new OA\Response( @@ -58,12 +87,18 @@ class BlogController extends BaseController #[Route('/api/v1/blogs', methods: ['GET'])] public function list(Request $request): JsonResponse { - $page = max(1, (int) $request->query->get('page', 1)); - $limit = min(50, max(1, (int) $request->query->get('limit', 20))); - $tag = $request->query->get('tag') ?: null; + $page = max(1, (int) $request->query->get('page', 1)); + $limit = min(50, max(1, (int) $request->query->get('limit', 20))); + $tag = $request->query->get('tag') ?: null; + $cityId = $request->query->get('city_id') !== null + ? max(1, (int) $request->query->get('city_id')) + : null; - $blogs = array_map(fn(Blog $b) => $b->toListArray(), $this->blogRepo->findPublished($page, $limit, $tag)); - $total = $this->blogRepo->countPublished($tag); + $blogs = array_map( + fn(Blog $b) => $b->toListArray(), + $this->blogRepo->findPublished($page, $limit, $tag, $cityId) + ); + $total = $this->blogRepo->countPublished($tag, $cityId); return $this->paginated($blogs, $total, $page, $limit); } @@ -127,6 +162,7 @@ class BlogController extends BaseController new OA\Property(property: 'summary', type: 'string', nullable: true), new OA\Property(property: 'tags', type: 'array', items: new OA\Items(type: 'string'), nullable: true), new OA\Property(property: 'status', type: 'string', enum: ['draft', 'published'], nullable: true), + new OA\Property(property: 'city_id', type: 'integer', nullable: true, description: 'City this post belongs to. Omit or null for a nationwide post.'), ] ) ), @@ -190,6 +226,8 @@ class BlogController extends BaseController if (!empty($data['tags'])) $blog->setTags((array)$data['tags']); if (!empty($data['status'])) $blog->setStatus($data['status']); if (!empty($data['image_url'])) $blog->setImageUrl($data['image_url']); + // نبودِ city_id یعنی سراسری — پس همیشه اعمال می‌شود، نه فقط وقتی مقدار دارد. + $blog->setCity($this->resolveCity($data['city_id'] ?? null)); // Ensure slug uniqueness if ($this->blogRepo->findBySlug($blog->getSlug()) !== null) { @@ -214,6 +252,7 @@ class BlogController extends BaseController new OA\Property(property: 'summary', type: 'string', nullable: true), new OA\Property(property: 'tags', type: 'array', items: new OA\Items(type: 'string'), nullable: true), new OA\Property(property: 'status', type: 'string', enum: ['draft', 'published'], nullable: true), + new OA\Property(property: 'city_id', type: 'integer', nullable: true, description: 'City this post belongs to. Omit or null for a nationwide post.'), ] ) ), @@ -263,6 +302,8 @@ class BlogController extends BaseController if (array_key_exists('tags', $data)) $blog->setTags((array)$data['tags']); if (array_key_exists('status', $data)) $blog->setStatus($data['status']); if (array_key_exists('image_url', $data)) $blog->setImageUrl($data['image_url'] ?: null); + // PATCH: فقط وقتی صریحاً فرستاده شد تغییر کند. ارسال null یعنی «سراسری‌اش کن». + if (array_key_exists('city_id', $data)) $blog->setCity($this->resolveCity($data['city_id'])); $this->blogRepo->save($blog); diff --git a/src/Blog/Entity/Blog.php b/src/Blog/Entity/Blog.php index 05a95ec3..266a83db 100644 --- a/src/Blog/Entity/Blog.php +++ b/src/Blog/Entity/Blog.php @@ -3,6 +3,7 @@ namespace App\Blog\Entity; use App\Auth\Entity\User; +use App\Location\Entity\City; use Doctrine\ORM\Mapping as ORM; use App\Blog\Repository\BlogRepository; use Symfony\Component\Uid\Uuid; @@ -10,6 +11,7 @@ use Symfony\Component\Uid\Uuid; #[ORM\Entity(repositoryClass: BlogRepository::class)] #[ORM\Table(name: 'blogs')] #[ORM\Index(columns: ['status', 'created_at'], name: 'idx_blogs_status')] +#[ORM\Index(columns: ['city_id'], name: 'idx_blogs_city')] class Blog { public const STATUS_DRAFT = 'draft'; @@ -46,6 +48,13 @@ class Blog #[ORM\JoinColumn(name: 'author_id', referencedColumnName: 'id', nullable: false, onDelete: 'RESTRICT')] private User $author; + // شهر پست. NULL معنای دائمی دارد: «پست سراسری» که روی دامنهٔ اصلی canonical + // می‌شود. سایت عمومی چند-دامنه‌ای بر پایهٔ همین تفکیک تصمیم می‌گیرد پست را روی + // دامنهٔ شهر نشان دهد یا روی دامنهٔ اصلی. + #[ORM\ManyToOne(targetEntity: City::class)] + #[ORM\JoinColumn(name: 'city_id', referencedColumnName: 'id', nullable: true, onDelete: 'SET NULL')] + private ?City $city = null; + #[ORM\Column(type: 'json')] private array $tags = []; @@ -80,6 +89,7 @@ class Blog public function getAuthor(): User { return $this->author; } public function getTags(): array { return $this->tags; } public function getStatus(): string { return $this->status; } + public function getCity(): ?City { return $this->city; } public function setTitle(string $v): self { $this->title = $v; $this->touch(); return $this; } public function setSlug(string $v): self { $this->slug = $v; $this->touch(); return $this; } @@ -89,6 +99,8 @@ class Blog public function setImagePath(?string $v): self { $this->imagePath = $v; $this->touch(); return $this; } public function setTags(array $v): self { $this->tags = $v; $this->touch(); return $this; } public function setStatus(string $v): self { $this->status = $v; $this->touch(); return $this; } + /** null = پست سراسری (روی همهٔ دامنه‌ها، canonical روی دامنهٔ اصلی) */ + public function setCity(?City $v): self { $this->city = $v; $this->touch(); return $this; } private function touch(): void { $this->updatedAt = time(); } @@ -112,11 +124,25 @@ class Blog 'tags' => $this->tags, 'status' => $this->status, 'author' => $this->authorToArray(), + 'city' => $this->cityToArray(), 'created_at' => $this->createdAt, 'updated_at' => $this->updatedAt, ]; } + /** null = پست سراسری. مصرف‌کننده روی همین null تصمیم canonical می‌گیرد. */ + private function cityToArray(): ?array + { + if ($this->city === null) { + return null; + } + + return [ + 'id' => (string) $this->city->getId(), + 'name' => $this->city->getName(), + ]; + } + private function authorToArray(): ?array { if ($this->author === null) { @@ -144,6 +170,7 @@ class Blog 'image_url' => $this->imageUrl, 'tags' => $this->tags, 'status' => $this->status, + 'city' => $this->cityToArray(), 'created_at' => $this->createdAt, ]; } diff --git a/src/Blog/Repository/BlogRepository.php b/src/Blog/Repository/BlogRepository.php index b3097d84..85b4ffeb 100644 --- a/src/Blog/Repository/BlogRepository.php +++ b/src/Blog/Repository/BlogRepository.php @@ -14,9 +14,11 @@ class BlogRepository extends ServiceEntityRepository public function findBySlug(string $slug): ?Blog { return $this->findOneBy(['slug' => $slug]); } /** @return Blog[] published, newest first */ - public function findPublished(int $page = 1, int $limit = 20, ?string $tag = null): array + public function findPublished(int $page = 1, int $limit = 20, ?string $tag = null, ?int $cityId = null): array { $qb = $this->createQueryBuilder('b') + ->leftJoin('b.city', 'c') + ->addSelect('c') ->where('b.status = :status') ->setParameter('status', Blog::STATUS_PUBLISHED) ->orderBy('b.createdAt', 'DESC') @@ -24,11 +26,12 @@ class BlogRepository extends ServiceEntityRepository ->setMaxResults($limit); $this->applyTagFilter($qb, $tag); + $this->applyCityFilter($qb, $cityId); return $qb->getQuery()->getResult(); } - public function countPublished(?string $tag = null): int + public function countPublished(?string $tag = null, ?int $cityId = null): int { $qb = $this->createQueryBuilder('b') ->select('COUNT(b.id)') @@ -36,10 +39,26 @@ class BlogRepository extends ServiceEntityRepository ->setParameter('status', Blog::STATUS_PUBLISHED); $this->applyTagFilter($qb, $tag); + $this->applyCityFilter($qb, $cityId); return (int) $qb->getQuery()->getSingleScalarResult(); } + /** + * دامنهٔ یک شهر باید پست‌های همان شهر **و** پست‌های سراسری را ببیند — پست + * سراسری (city NULL) روی همهٔ دامنه‌ها منتشر است، فقط canonicalش روی دامنهٔ اصلی + * می‌نشیند. بدون شرط NULL، دامنه‌های شهری محتوای عمومی را از دست می‌دادند. + */ + private function applyCityFilter(\Doctrine\ORM\QueryBuilder $qb, ?int $cityId): void + { + if ($cityId === null) { + return; + } + + $qb->andWhere('b.city = :cityId OR b.city IS NULL') + ->setParameter('cityId', $cityId); + } + private function applyTagFilter(\Doctrine\ORM\QueryBuilder $qb, ?string $tag): void { if ($tag === null || $tag === '') { diff --git a/tests/Blog/BlogCityScopeTest.php b/tests/Blog/BlogCityScopeTest.php new file mode 100644 index 00000000..75e83438 --- /dev/null +++ b/tests/Blog/BlogCityScopeTest.php @@ -0,0 +1,179 @@ +em->persist($province); + $city = new City($name, $province); + $this->em->persist($city); + + return $city; + } + + private function makePost(string $title, ?City $city): Blog + { + $blog = new Blog($this->createUser(['ROLE_ADMIN']), $title, 'متن آزمایشی مقاله برای تست'); + $blog->setStatus(Blog::STATUS_PUBLISHED)->setCity($city); + $this->em->persist($blog); + + return $blog; + } + + /** @return array published list keyed by title */ + private function listBy(string $query = ''): array + { + $this->client->request('GET', '/api/v1/blogs?limit=50' . $query); + $this->assertSame(200, $this->responseCode()); + $payload = json_decode($this->client->getResponse()->getContent(), true); + + $byTitle = []; + foreach ($payload['data'] as $row) { + $byTitle[$row['title']] = $row; + } + + return $byTitle; + } + + public function testCityScopedListReturnsCityPostsPlusNationwide(): void + { + $yasuj = $this->makeCity('یاسوج'); + $tabriz = $this->makeCity('تبریز'); + $tag = bin2hex(random_bytes(4)); + + $this->makePost("یاسوجی-$tag", $yasuj); + $this->makePost("تبریزی-$tag", $tabriz); + $this->makePost("سراسری-$tag", null); + $this->em->flush(); + + $scoped = $this->listBy('&city_id=' . $yasuj->getId()); + + $this->assertArrayHasKey("یاسوجی-$tag", $scoped, 'city post missing'); + $this->assertArrayHasKey("سراسری-$tag", $scoped, 'nationwide post must appear on a city domain'); + $this->assertArrayNotHasKey("تبریزی-$tag", $scoped, 'another city\'s post leaked into the list'); + } + + public function testUnscopedListReturnsEveryPublishedPost(): void + { + $yasuj = $this->makeCity('یاسوج'); + $tag = bin2hex(random_bytes(4)); + + $this->makePost("یاسوجی-$tag", $yasuj); + $this->makePost("سراسری-$tag", null); + $this->em->flush(); + + $all = $this->listBy(); + + $this->assertArrayHasKey("یاسوجی-$tag", $all); + $this->assertArrayHasKey("سراسری-$tag", $all); + } + + public function testCityAppearsInListAndDetailPayload(): void + { + $yasuj = $this->makeCity('یاسوج'); + $tag = bin2hex(random_bytes(4)); + + $cityPost = $this->makePost("یاسوجی-$tag", $yasuj); + $natPost = $this->makePost("سراسری-$tag", null); + $this->em->flush(); + + $list = $this->listBy(); + $this->assertSame( + ['id' => (string) $yasuj->getId(), 'name' => 'یاسوج'], + $list["یاسوجی-$tag"]['city'] + ); + $this->assertNull($list["سراسری-$tag"]['city'], 'nationwide post must report city: null'); + + $this->client->request('GET', '/api/v1/blog/' . $cityPost->getSlug()); + $this->assertSame(200, $this->responseCode()); + $detail = json_decode($this->client->getResponse()->getContent(), true)['data']['data']; + $this->assertSame('یاسوج', $detail['city']['name']); + + $this->client->request('GET', '/api/v1/blog/' . $natPost->getSlug()); + $this->assertSame(200, $this->responseCode()); + $natDetail = json_decode($this->client->getResponse()->getContent(), true)['data']['data']; + $this->assertNull($natDetail['city']); + } + + public function testAdminCanCreatePostWithAndWithoutCity(): void + { + $admin = $this->createUser(['ROLE_ADMIN']); + $yasuj = $this->makeCity('یاسوج'); + $this->em->flush(); + + $withCity = $this->authJson('POST', '/api/v1/blog', $admin, [ + 'title' => 'مقاله شهری ' . bin2hex(random_bytes(3)), + 'body' => 'متن آزمایشی مقاله برای تست', + 'city_id' => $yasuj->getId(), + ]); + $this->assertSame(201, $this->responseCode()); + $this->assertSame('یاسوج', $withCity['data']['data']['city']['name']); + + $nationwide = $this->authJson('POST', '/api/v1/blog', $admin, [ + 'title' => 'مقاله سراسری ' . bin2hex(random_bytes(3)), + 'body' => 'متن آزمایشی مقاله برای تست', + ]); + $this->assertSame(201, $this->responseCode()); + $this->assertNull($nationwide['data']['data']['city'], 'omitting city_id must mean nationwide'); + } + + public function testAdminCanMovePostBetweenCityAndNationwide(): void + { + $admin = $this->createUser(['ROLE_ADMIN']); + $yasuj = $this->makeCity('یاسوج'); + $post = $this->makePost('مقاله ' . bin2hex(random_bytes(3)), null); + $this->em->flush(); + + $assigned = $this->authJson('PATCH', '/api/v1/blog/' . $post->getUuid(), $admin, [ + 'city_id' => $yasuj->getId(), + ]); + $this->assertSame(200, $this->responseCode()); + $this->assertSame('یاسوج', $assigned['data']['data']['city']['name']); + + $cleared = $this->authJson('PATCH', '/api/v1/blog/' . $post->getUuid(), $admin, [ + 'city_id' => null, + ]); + $this->assertSame(200, $this->responseCode()); + $this->assertNull($cleared['data']['data']['city'], 'city_id: null must make the post nationwide again'); + } + + public function testPatchWithoutCityIdLeavesCityUntouched(): void + { + $admin = $this->createUser(['ROLE_ADMIN']); + $yasuj = $this->makeCity('یاسوج'); + $post = $this->makePost('مقاله ' . bin2hex(random_bytes(3)), $yasuj); + $this->em->flush(); + + $updated = $this->authJson('PATCH', '/api/v1/blog/' . $post->getUuid(), $admin, [ + 'summary' => 'خلاصه جدید', + ]); + $this->assertSame(200, $this->responseCode()); + $this->assertSame('یاسوج', $updated['data']['data']['city']['name'], 'PATCH must not silently clear the city'); + } + + public function testUnknownCityIdIsRejected(): void + { + $admin = $this->createUser(['ROLE_ADMIN']); + $this->em->flush(); + + $this->authJson('POST', '/api/v1/blog', $admin, [ + 'title' => 'مقاله ' . bin2hex(random_bytes(3)), + 'body' => 'متن آزمایشی مقاله برای تست', + 'city_id' => 999999, + ]); + $this->assertSame(422, $this->responseCode()); + } +}