Compare commits
14
Commits
68b8b05630
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40b1da64be | ||
|
|
8e4bb3ca7b | ||
|
|
6e3d574e38 | ||
|
|
4f76cff503 | ||
|
|
b24f45cc83 | ||
|
|
1f64b516d2 | ||
|
|
5d2594ff87 | ||
|
|
8a43297e24 | ||
|
|
ed2d2ec74a | ||
|
|
5986fad5a1 | ||
|
|
57ea7b8c59 | ||
|
|
1d13212d47 | ||
|
|
2459625c41 | ||
|
|
ee2682e222 |
@@ -17,7 +17,9 @@
|
|||||||
# Replace mariadb-XXXXXXXX / redis-XXXXXXXX with the real hostname shown on each
|
# Replace mariadb-XXXXXXXX / redis-XXXXXXXX with the real hostname shown on each
|
||||||
# resource's page (Internal URL). serverVersion MUST match the MariaDB resource (11.8).
|
# resource's page (Internal URL). serverVersion MUST match the MariaDB resource (11.8).
|
||||||
DATABASE_URL="mysql://clinic:DB_PASSWORD@mariadb-XXXXXXXX:3306/clinic_pro?serverVersion=mariadb-11.8.0&charset=utf8mb4"
|
DATABASE_URL="mysql://clinic:DB_PASSWORD@mariadb-XXXXXXXX:3306/clinic_pro?serverVersion=mariadb-11.8.0&charset=utf8mb4"
|
||||||
REDIS_URL="redis://redis-XXXXXXXX:6379"
|
# retry_interval/tcp_keepalive: قطع کوتاه اتصال به redis بیسروصدا دوباره برقرار میشود
|
||||||
|
# و «Connection lost» بهصورت warning در app_log نمینشیند.
|
||||||
|
REDIS_URL="redis://redis-XXXXXXXX:6379?timeout=5&read_timeout=5&retry_interval=100&tcp_keepalive=60"
|
||||||
# stream_max_entries caps the Redis stream so the queue cannot grow without bound
|
# stream_max_entries caps the Redis stream so the queue cannot grow without bound
|
||||||
MESSENGER_TRANSPORT_DSN="redis://redis-XXXXXXXX:6379/messages?stream_max_entries=20000"
|
MESSENGER_TRANSPORT_DSN="redis://redis-XXXXXXXX:6379/messages?stream_max_entries=20000"
|
||||||
# If the Redis resource has a password: redis://:PASSWORD@redis-XXXXXXXX:6379
|
# If the Redis resource has a password: redis://:PASSWORD@redis-XXXXXXXX:6379
|
||||||
|
|||||||
+3
-1
@@ -31,7 +31,9 @@ MESSENGER_TRANSPORT_DSN=redis://redis:6379/messages
|
|||||||
###< symfony/messenger ###
|
###< symfony/messenger ###
|
||||||
|
|
||||||
###> Redis ###
|
###> Redis ###
|
||||||
REDIS_URL=redis://redis:6379
|
# retry_interval/tcp_keepalive: قطع کوتاه اتصال به redis بیسروصدا دوباره برقرار میشود
|
||||||
|
# و «Connection lost» بهصورت warning در app_log نمینشیند.
|
||||||
|
REDIS_URL=redis://redis:6379?timeout=5&read_timeout=5&retry_interval=100&tcp_keepalive=60
|
||||||
###< Redis ###
|
###< Redis ###
|
||||||
|
|
||||||
###> Auth ###
|
###> Auth ###
|
||||||
|
|||||||
+3
-1
@@ -22,7 +22,9 @@ JWT_PASSPHRASE= # openssl rand -hex 32 (JWT keypair is generated with i
|
|||||||
# put both + this app on the SAME private network, then copy their private hosts here.
|
# put both + this app on the SAME private network, then copy their private hosts here.
|
||||||
# serverVersion MUST match the MariaDB service (11.8).
|
# serverVersion MUST match the MariaDB service (11.8).
|
||||||
DATABASE_URL="mysql://<user>:<pass>@<db-private-host>:3306/<db>?serverVersion=mariadb-11.8.0&charset=utf8mb4"
|
DATABASE_URL="mysql://<user>:<pass>@<db-private-host>:3306/<db>?serverVersion=mariadb-11.8.0&charset=utf8mb4"
|
||||||
REDIS_URL="redis://<redis-private-host>:6379"
|
# retry_interval/tcp_keepalive: قطع کوتاه اتصال به redis بیسروصدا دوباره برقرار میشود
|
||||||
|
# و «Connection lost» بهصورت warning در app_log نمینشیند.
|
||||||
|
REDIS_URL="redis://<redis-private-host>:6379?timeout=5&read_timeout=5&retry_interval=100&tcp_keepalive=60"
|
||||||
MESSENGER_TRANSPORT_DSN="redis://<redis-private-host>:6379/messages"
|
MESSENGER_TRANSPORT_DSN="redis://<redis-private-host>:6379/messages"
|
||||||
# If the Redis service has a password: redis://:<pass>@<redis-private-host>:6379
|
# If the Redis service has a password: redis://:<pass>@<redis-private-host>:6379
|
||||||
|
|
||||||
|
|||||||
@@ -74,10 +74,15 @@ function percentsToStrings(map?: Record<string, number>): Record<string, string>
|
|||||||
return Object.fromEntries(Object.entries(map ?? {}).map(([k, v]) => [k, String(v)]));
|
return Object.fromEntries(Object.entries(map ?? {}).map(([k, v]) => [k, String(v)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** درصد پوششِ قابل ثبت: عددی بین ۱ تا ۱۰۰ — صفر یعنی قرارداد آن نوع خدمت را پوشش نمیدهد. */
|
/**
|
||||||
|
* درصد پوششِ قابل ثبت: عددی بین ۰ تا ۱۰۰.
|
||||||
|
*
|
||||||
|
* صفر مقدار معتبری است و یعنی «این قرارداد آن نوع خدمت را پوشش نمیدهد» — سهم بیمار
|
||||||
|
* صددرصد. آنچه رد میشود خالیماندنِ فیلد است، نه صفر بودنش.
|
||||||
|
*/
|
||||||
export function isValidPercent(raw?: string): boolean {
|
export function isValidPercent(raw?: string): boolean {
|
||||||
const n = Number(raw);
|
const n = Number(raw);
|
||||||
return raw !== undefined && raw !== '' && Number.isFinite(n) && n > 0 && n <= 100;
|
return raw !== undefined && raw !== '' && Number.isFinite(n) && n >= 0 && n <= 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export interface SessionCardData {
|
|||||||
base_insurance_discount_percent?: number;
|
base_insurance_discount_percent?: number;
|
||||||
supplementary_discount_percent?: number;
|
supplementary_discount_percent?: number;
|
||||||
doctor_name?: string | null;
|
doctor_name?: string | null;
|
||||||
|
/** پزشکِ نوبتِ این مراجعه — محدودهٔ قرارداد بیمه از روی او حل میشود. */
|
||||||
|
doctor_uuid?: string | null;
|
||||||
final_price_rials?: number;
|
final_price_rials?: number;
|
||||||
// تفکیک بیمه — سرور محاسبه میکند (PatientSession::applyShares)
|
// تفکیک بیمه — سرور محاسبه میکند (PatientSession::applyShares)
|
||||||
gross_total_rials?: number;
|
gross_total_rials?: number;
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ interface AppointmentLike {
|
|||||||
insurance_service_category?: string | null;
|
insurance_service_category?: string | null;
|
||||||
insurance_base_id?: number | null;
|
insurance_base_id?: number | null;
|
||||||
insurance_supplementary_id?: number | null;
|
insurance_supplementary_id?: number | null;
|
||||||
|
doctor?: { uuid?: string | null } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -159,7 +160,7 @@ export default function ConfirmAppointmentModal({
|
|||||||
const appt: AppointmentLike | null = detail ?? appointment ?? null;
|
const appt: AppointmentLike | null = detail ?? appointment ?? null;
|
||||||
|
|
||||||
// ── بیمه: نوع خدمت + بیمهٔ پایهٔ نوبت ──────────────────────────────────────
|
// ── بیمه: نوع خدمت + بیمهٔ پایهٔ نوبت ──────────────────────────────────────
|
||||||
const insurance = useAppointmentInsurance(open);
|
const insurance = useAppointmentInsurance(open, appt?.doctor?.uuid ?? null);
|
||||||
|
|
||||||
// نوبتِ بدون هزینهٔ ویزیت، سرِ ساختِ مراجعه «قیمت ویزیت آزاد» تنظیمات را میگیرد؛
|
// نوبتِ بدون هزینهٔ ویزیت، سرِ ساختِ مراجعه «قیمت ویزیت آزاد» تنظیمات را میگیرد؛
|
||||||
// مودال هم باید همان را نشان دهد، وگرنه صفر نشان میدهد و مبلغ ثبتشده فرق میکند.
|
// مودال هم باید همان را نشان دهد، وگرنه صفر نشان میدهد و مبلغ ثبتشده فرق میکند.
|
||||||
|
|||||||
@@ -72,7 +72,8 @@ describe('TurnsTimeline', () => {
|
|||||||
|
|
||||||
it('نوبتِ دارای بیمه، چیپ «نوع خدمت · بیمه» نشان میدهد', async () => {
|
it('نوبتِ دارای بیمه، چیپ «نوع خدمت · بیمه» نشان میدهد', async () => {
|
||||||
(api.get as ReturnType<typeof vi.fn>).mockImplementation((url: string) =>
|
(api.get as ReturnType<typeof vi.fn>).mockImplementation((url: string) =>
|
||||||
url === '/api/v1/billing/tenant-insurances'
|
// با پزشکِ نوبت، آدرس `?doctor_uuid=…&inherit=1` هم میگیرد.
|
||||||
|
url.startsWith('/api/v1/billing/tenant-insurances')
|
||||||
? Promise.resolve({ success: true, data: { data: [{
|
? Promise.resolve({ success: true, data: { data: [{
|
||||||
insurance_id: 3, insurance_name: 'بیمه ایران', insurance_kind: 'basic', is_active: true,
|
insurance_id: 3, insurance_name: 'بیمه ایران', insurance_kind: 'basic', is_active: true,
|
||||||
coverage_percent: 70, franchise_percent: 0, annual_ceiling_rials: null,
|
coverage_percent: 70, franchise_percent: 0, annual_ceiling_rials: null,
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ function OccupiedCard({
|
|||||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||||
// نام بیمه فقط با نگاشت از قراردادهای کششده به دست میآید؛ payload نوبت نامی ندارد
|
// نام بیمه فقط با نگاشت از قراردادهای کششده به دست میآید؛ payload نوبت نامی ندارد
|
||||||
// تا لیستهای نوبت به N+1 نیفتند.
|
// تا لیستهای نوبت به N+1 نیفتند.
|
||||||
const insurance = useAppointmentInsurance(!!a.insurance_base_id);
|
const insurance = useAppointmentInsurance(!!a.insurance_base_id, a.doctor_uuid ?? null);
|
||||||
const insuranceChip = [
|
const insuranceChip = [
|
||||||
a.insurance_service_category_label,
|
a.insurance_service_category_label,
|
||||||
insurance.insuranceNameOf(a.insurance_base_id),
|
insurance.insuranceNameOf(a.insurance_base_id),
|
||||||
|
|||||||
@@ -105,11 +105,20 @@ export default function CreateStep({ recordUuid, profile, onCreated, onCancel, e
|
|||||||
const { data: packagesData } = useQuery<ApiResponse<PackageRow[]>>({
|
const { data: packagesData } = useQuery<ApiResponse<PackageRow[]>>({
|
||||||
queryKey: ['inventory-packages'], queryFn: () => api.get('/api/v1/inventory-packages'),
|
queryKey: ['inventory-packages'], queryFn: () => api.get('/api/v1/inventory-packages'),
|
||||||
});
|
});
|
||||||
|
// مراجعهٔ برخاسته از نوبت، قراردادِ پزشکِ همان نوبت را دارد نه قراردادِ محیط پنل:
|
||||||
|
// در کلینیک، قرارداد روی خودِ پزشک ثبت میشود و بدون این محدوده، فهرست بیمهها
|
||||||
|
// خالی میآمد و بیمهٔ ثبتشدهٔ همان مراجعه هم در فرم پیدا نمیشد.
|
||||||
|
const insuranceScope = editSession?.doctor_uuid
|
||||||
|
? `?doctor_uuid=${encodeURIComponent(editSession.doctor_uuid)}&inherit=1`
|
||||||
|
: '';
|
||||||
|
|
||||||
const { data: contractsData } = useQuery<{ data: { data: Contract[] } }>({
|
const { data: contractsData } = useQuery<{ data: { data: Contract[] } }>({
|
||||||
queryKey: ['tenant-insurances'], queryFn: () => api.get('/api/v1/billing/tenant-insurances'),
|
queryKey: ['tenant-insurances', editSession?.doctor_uuid ?? null],
|
||||||
|
queryFn: () => api.get(`/api/v1/billing/tenant-insurances${insuranceScope}`),
|
||||||
});
|
});
|
||||||
const { data: pricingData } = useQuery<{ data: { free_visit_price_rials: number; require_visit_price: boolean } }>({
|
const { data: pricingData } = useQuery<{ data: { free_visit_price_rials: number; require_visit_price: boolean } }>({
|
||||||
queryKey: ['insurance-pricing'], queryFn: () => api.get('/api/v1/insurance-pricing'),
|
queryKey: ['insurance-pricing', editSession?.doctor_uuid ?? null],
|
||||||
|
queryFn: () => api.get(`/api/v1/insurance-pricing${insuranceScope}`),
|
||||||
});
|
});
|
||||||
const freeVisit = (pricingData as any)?.data?.free_visit_price_rials ?? 0;
|
const freeVisit = (pricingData as any)?.data?.free_visit_price_rials ?? 0;
|
||||||
const requireVisit = (pricingData as any)?.data?.require_visit_price ?? false;
|
const requireVisit = (pricingData as any)?.data?.require_visit_price ?? false;
|
||||||
|
|||||||
@@ -22,16 +22,23 @@ interface PricingPayload {
|
|||||||
* بههمراه محاسبهٔ سهم — مشترک بین مودال «قطعی کردن نوبت» و صفحهٔ ویرایش نوبت تا
|
* بههمراه محاسبهٔ سهم — مشترک بین مودال «قطعی کردن نوبت» و صفحهٔ ویرایش نوبت تا
|
||||||
* هر دو یک قاعده را نشان دهند.
|
* هر دو یک قاعده را نشان دهند.
|
||||||
*/
|
*/
|
||||||
export function useAppointmentInsurance(enabled: boolean) {
|
export function useAppointmentInsurance(enabled: boolean, doctorUuid?: string | null) {
|
||||||
|
// بدون پزشک، محیطِ خودِ کاربر پرسیده میشود — همان رفتار قبلی برای مطب شخصی.
|
||||||
|
//
|
||||||
|
// با پزشک، `inherit=1` هم میرود: نوبتِ ثبتشده در کلینیک محیطش «کلینیک» است ولی
|
||||||
|
// قرارداد بیمه معمولاً روی خودِ پزشک ذخیره شده. سرور اول تنظیم پزشک را میدهد و
|
||||||
|
// در نبودش تنظیم کلینیک را — دقیقاً همان چیزی که سرِ قطعیکردن اعمال میشود.
|
||||||
|
const scopeQuery = doctorUuid ? `?doctor_uuid=${encodeURIComponent(doctorUuid)}&inherit=1` : '';
|
||||||
|
|
||||||
const pricingQuery = useQuery<ApiResponse<PricingPayload>>({
|
const pricingQuery = useQuery<ApiResponse<PricingPayload>>({
|
||||||
queryKey: ['insurance-pricing'],
|
queryKey: ['insurance-pricing', doctorUuid ?? null],
|
||||||
queryFn: () => api.get('/api/v1/insurance-pricing'),
|
queryFn: () => api.get(`/api/v1/insurance-pricing${scopeQuery}`),
|
||||||
enabled,
|
enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
const contractsQuery = useQuery<ApiResponse<{ data: TenantContract[] }>>({
|
const contractsQuery = useQuery<ApiResponse<{ data: TenantContract[] }>>({
|
||||||
queryKey: ['tenant-insurances'],
|
queryKey: ['tenant-insurances', doctorUuid ?? null],
|
||||||
queryFn: () => api.get('/api/v1/billing/tenant-insurances'),
|
queryFn: () => api.get(`/api/v1/billing/tenant-insurances${scopeQuery}`),
|
||||||
enabled,
|
enabled,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+7
-7
@@ -10,12 +10,12 @@
|
|||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"ext-soap": "*",
|
"ext-soap": "*",
|
||||||
"altcha-org/altcha": "^2.0",
|
"altcha-org/altcha": "^2.1",
|
||||||
"doctrine/doctrine-bundle": ">=2.18.3",
|
"doctrine/doctrine-bundle": ">=2.19.0",
|
||||||
"doctrine/doctrine-migrations-bundle": "*",
|
"doctrine/doctrine-migrations-bundle": "*",
|
||||||
"doctrine/orm": "^3.6",
|
"doctrine/orm": "^3.6.8",
|
||||||
"lexik/jwt-authentication-bundle": "*",
|
"lexik/jwt-authentication-bundle": "*",
|
||||||
"nelmio/api-doc-bundle": "*",
|
"nelmio/api-doc-bundle": ">=5.11.1",
|
||||||
"nelmio/cors-bundle": "*",
|
"nelmio/cors-bundle": "*",
|
||||||
"symfony/asset": "7.4.*",
|
"symfony/asset": "7.4.*",
|
||||||
"symfony/cache": "7.4.*",
|
"symfony/cache": "7.4.*",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"symfony/webpack-encore-bundle": "^2.4.1",
|
"symfony/webpack-encore-bundle": "^2.4.1",
|
||||||
"symfony/yaml": "7.4.*",
|
"symfony/yaml": "7.4.*",
|
||||||
"twig/twig": ">=3.28",
|
"twig/twig": ">=3.28",
|
||||||
"zircote/swagger-php": ">=6.4"
|
"zircote/swagger-php": ">=6.6"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-plugins": {
|
"allow-plugins": {
|
||||||
@@ -98,10 +98,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"phpstan/phpstan": "^2.2.5",
|
"phpstan/phpstan": "^2.2.8",
|
||||||
"phpstan/phpstan-doctrine": "^2.0.28",
|
"phpstan/phpstan-doctrine": "^2.0.28",
|
||||||
"phpstan/phpstan-symfony": "^2.0.20",
|
"phpstan/phpstan-symfony": "^2.0.20",
|
||||||
"phpunit/phpunit": "^12.5.31",
|
"phpunit/phpunit": "^12.5.33",
|
||||||
"symfony/browser-kit": "7.4.*",
|
"symfony/browser-kit": "7.4.*",
|
||||||
"symfony/css-selector": "7.4.*",
|
"symfony/css-selector": "7.4.*",
|
||||||
"symfony/debug-bundle": "7.4.*",
|
"symfony/debug-bundle": "7.4.*",
|
||||||
|
|||||||
Generated
+283
-283
File diff suppressed because it is too large
Load Diff
+15
-1
@@ -459,7 +459,15 @@ Get appointment detail.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
> `doctor.specialties` آرایه (ممکن است خالی)؛ `address` اولین آدرس پزشک است (ممکن است `null` اگر پزشک آدرسی ندارد). `address.map.latitude/longitude` رشته یا `null`. تاریخها Unix.
|
|
||||||
|
> ℹ️ تغییر بیمهٔ نوبت با `PATCH` سهمهای مراجعهٔ همان نوبت را هم دوباره حساب میکند: پذیرش گاهی اول نوبت را قطعی میکند و بعد بیمه را اصلاح میکند، و بدون این، مراجعه روی محاسبهٔ اول میماند و صفحهٔ پرداخت سهمِ بیمه را از بیمار میخواهد. پرداختهای ثبتشده دست نمیخورند؛ فقط مبلغ قابلپرداخت اصلاح میشود. پاسخ `PATCH` هم مثل `GET` نشانیِ واقعیِ نوبت را برمیگرداند.
|
||||||
|
|
||||||
|
> ℹ️ `address` is the venue recorded on this appointment (`address_id`) — the doctor's own
|
||||||
|
> office or the clinic branch, whichever the booking was made at — and it is the only place
|
||||||
|
> `telephone` is returned. When that record carries no number, the clinic's own number takes
|
||||||
|
> its place. Public doctor and clinic responses never carry a phone number.
|
||||||
|
|
||||||
|
> `doctor.specialties` آرایه (ممکن است خالی)؛ `address` آدرسِ ثبتشدهٔ همین نوبت است و برای نوبتهای قدیمیِ بدون `address_id` ممکن است `null` باشد. `address.map.latitude/longitude` رشته یا `null`. تاریخها Unix.
|
||||||
|
|
||||||
### انتخاب بیمهٔ نوبت
|
### انتخاب بیمهٔ نوبت
|
||||||
|
|
||||||
@@ -547,6 +555,12 @@ Get all appointments for the authenticated user.
|
|||||||
|
|
||||||
**Permission:** `AUTH` — عمداً بدون مجوزِ رجیستری.
|
**Permission:** `AUTH` — عمداً بدون مجوزِ رجیستری.
|
||||||
|
|
||||||
|
> **فیلتر محیط اینجا اعمال نمیشود.** رکورد در محیطِ پزشکِ مقصد ثبت میشود، ولی
|
||||||
|
> مالکش از راه `user_id` تعیین میشود. کاربری که خودش صاحب محیط دیگری است — پزشک،
|
||||||
|
> منشی، مالک کلینیک — با فیلترِ محیطِ خودش رکورد خودش را نمیدید و ۴۰۴ میگرفت.
|
||||||
|
> دورزدن فیلتر فقط از راه `App\Shared\Tenant\TenantFilterScope` انجام میشود و
|
||||||
|
> مجوز دستنخورده باقی میماند.
|
||||||
|
|
||||||
> این اندپوینت `a.user = خودِ کاربر` را میدهد، یعنی نوبتهای خودِ فرد **بهعنوان
|
> این اندپوینت `a.user = خودِ کاربر` را میدهد، یعنی نوبتهای خودِ فرد **بهعنوان
|
||||||
> بیمار**، نه دادهٔ محیط. مصرفکنندهاش داشبورد بیمار در `nobat724_front` است.
|
> بیمار**، نه دادهٔ محیط. مصرفکنندهاش داشبورد بیمار در `nobat724_front` است.
|
||||||
> آدیت ۲۰۲۶-۰۸-۰۷ آن را در فهرست گَپها آورده بود؛ در ۲۰۲۶-۰۸-۰۸ مثبت کاذب تشخیص
|
> آدیت ۲۰۲۶-۰۸-۰۷ آن را در فهرست گَپها آورده بود؛ در ۲۰۲۶-۰۸-۰۸ مثبت کاذب تشخیص
|
||||||
|
|||||||
+3
-1
@@ -167,6 +167,8 @@ limited to the whitelist under `PATCH /api/v1/clinic/{uuid}`.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> 🔒 `phone`/`phone_number` are `null` unless the caller may edit the clinic (`can_edit: true`). The number is not public data; the patient sees the venue phone on their own appointment instead.
|
||||||
|
>
|
||||||
> `city`/`state`/`map`/`location`/`phone`/`phone_number` are all resolved from the clinic's **address** (`DoctorAddress` linked by `clinic_id`), not from columns on the clinic. `location` and `phone`/`phone_number` fall back to the deprecated `clinics.address` / `clinics.telephone` columns only when the address record has no value — reading them from different rows made one response describe two different places. Each is an array with a single object (or empty `[]` if the clinic has no address). `doctors` is a **count**; the actual doctor list comes from `GET /api/v1/clinic/doctor-list/{clinicUuid}` (`doctor_list` here is always `null`).
|
> `city`/`state`/`map`/`location`/`phone`/`phone_number` are all resolved from the clinic's **address** (`DoctorAddress` linked by `clinic_id`), not from columns on the clinic. `location` and `phone`/`phone_number` fall back to the deprecated `clinics.address` / `clinics.telephone` columns only when the address record has no value — reading them from different rows made one response describe two different places. Each is an array with a single object (or empty `[]` if the clinic has no address). `doctors` is a **count**; the actual doctor list comes from `GET /api/v1/clinic/doctor-list/{clinicUuid}` (`doctor_list` here is always `null`).
|
||||||
|
|
||||||
### معنای `is_active`
|
### معنای `is_active`
|
||||||
@@ -330,7 +332,7 @@ List clinics with pagination.
|
|||||||
| `doctors_count` | integer | Number of doctors linked to the clinic |
|
| `doctors_count` | integer | Number of doctors linked to the clinic |
|
||||||
| `city` | string\|null | City name, resolved from the clinic's address (`DoctorAddress`) |
|
| `city` | string\|null | City name, resolved from the clinic's address (`DoctorAddress`) |
|
||||||
| `state` | string\|null | Province name, resolved from the clinic's address (`DoctorAddress`) |
|
| `state` | string\|null | Province name, resolved from the clinic's address (`DoctorAddress`) |
|
||||||
| `phone` / `phone_number` | string\|null | Contact number from the clinic's address (`DoctorAddress`), falling back to the deprecated `clinics.telephone` column |
|
| `phone` / `phone_number` | string\|null | Contact number from the clinic's address (`DoctorAddress`), falling back to the deprecated `clinics.telephone` column. `null` for anyone who cannot edit the clinic, and always `null` in the public list. |
|
||||||
| `24_7` | boolean | Open 24/7 flag |
|
| `24_7` | boolean | Open 24/7 flag |
|
||||||
| `field_working_days` | string\|null | Working days/hours description |
|
| `field_working_days` | string\|null | Working days/hours description |
|
||||||
|
|
||||||
|
|||||||
+9
-2
@@ -133,7 +133,7 @@ limited to the whitelist under `PATCH /api/v1/doctor/{uuid}`.
|
|||||||
"address": [],
|
"address": [],
|
||||||
"state": [],
|
"state": [],
|
||||||
"city": [],
|
"city": [],
|
||||||
"clinics": [{ "uuid": "...", "name": "کلینیک الوند", "address": "...", "telephone": "..." }],
|
"clinics": [{ "uuid": "...", "name": "کلینیک الوند", "address": "...", "telephone": null }],
|
||||||
"representation": { "id": 12, "uuid": "9c1...", "full_name": "علی محمدی" }
|
"representation": { "id": 12, "uuid": "9c1...", "full_name": "علی محمدی" }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,6 +143,13 @@ limited to the whitelist under `PATCH /api/v1/doctor/{uuid}`.
|
|||||||
> ⚠️ **Double-nested:** Frontend extracts with `data?.data?.data`
|
> ⚠️ **Double-nested:** Frontend extracts with `data?.data?.data`
|
||||||
>
|
>
|
||||||
> ℹ️ `representation` نمایندهی مالکِ پزشک است؛ برای پزشکِ بدون نماینده `null`.
|
> ℹ️ `representation` نمایندهی مالکِ پزشک است؛ برای پزشکِ بدون نماینده `null`.
|
||||||
|
>
|
||||||
|
> 🔒 **Phone numbers are not public.** `address[].telephone` and `clinics[].telephone`
|
||||||
|
> are `null` for anonymous callers and only carry a value when `can_edit` is `true`
|
||||||
|
> (the profile owner, its representative, or an admin). Street address and map
|
||||||
|
> coordinates stay public — a patient needs them to find the place. The venue phone
|
||||||
|
> reaches the patient through their own appointment (`GET /api/v1/appointments/user`),
|
||||||
|
> not through the public profile.
|
||||||
|
|
||||||
### Errors
|
### Errors
|
||||||
| Code | HTTP | Description |
|
| Code | HTTP | Description |
|
||||||
@@ -171,7 +178,7 @@ Get doctor detail for clinic owner — only doctors who are members of the authe
|
|||||||
"uuid": "...",
|
"uuid": "...",
|
||||||
"title": "علی احمدی",
|
"title": "علی احمدی",
|
||||||
"specialties": [...],
|
"specialties": [...],
|
||||||
"clinics": [{ "uuid": "...", "name": "کلینیک نور", "address": "...", "telephone": "..." }]
|
"clinics": [{ "uuid": "...", "name": "کلینیک نور", "address": "...", "telephone": "021..." }]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -407,9 +407,16 @@ entity جاری از `#[CurrentUser]` resolve میشود: نقش `ROLE_DOCTOR
|
|||||||
| Param | Type | Required | Description |
|
| Param | Type | Required | Description |
|
||||||
|-------|------|----------|-------------|
|
|-------|------|----------|-------------|
|
||||||
| `doctor_uuid` | string (UUID) | ❌ | قیمتگذاری همان پزشک را برمیگرداند بهجای موجودیت کاربر جاری. برای تبهای نوبتدهی پنل کلینیک. |
|
| `doctor_uuid` | string (UUID) | ❌ | قیمتگذاری همان پزشک را برمیگرداند بهجای موجودیت کاربر جاری. برای تبهای نوبتدهی پنل کلینیک. |
|
||||||
|
| `inherit` | bool | ❌ | «برای نوبتِ این پزشک واقعاً چه قیمتی اعمال میشود؟» — اول قیمت خودِ پزشک، در نبودش قیمت کلینیکی که کاربر در آن ایستاده. مودال قطعیکردن نوبت آن را میفرستد؛ صفحهٔ تنظیمات نه، چون آنجا باید ردیفِ خودِ پزشک ویرایش شود. |
|
||||||
|
|
||||||
با `doctor_uuid`، دسترسی اینگونه بررسی میشود: `ROLE_ADMIN`، خودِ پزشک، مالک کلینیکی که پزشک عضو آن است، یا پزشکِ عضو همان کلینیک با مجوز `services.view` (برای `PUT`: `services.update`). در غیر این صورت `403 ERR_ACCESS_DENIED`؛ پزشکِ ناموجود `404 ERR_NOT_FOUND_001`. بدون این پارامتر رفتار قبلی (موجودیت کاربر جاری) دستنخورده است.
|
با `doctor_uuid`، دسترسی اینگونه بررسی میشود: `ROLE_ADMIN`، خودِ پزشک، مالک کلینیکی که پزشک عضو آن است، یا پزشکِ عضو همان کلینیک با مجوز `services.view` (برای `PUT`: `services.update`). در غیر این صورت `403 ERR_ACCESS_DENIED`؛ پزشکِ ناموجود `404 ERR_NOT_FOUND_001`. بدون این پارامتر رفتار قبلی (موجودیت کاربر جاری) دستنخورده است.
|
||||||
|
|
||||||
|
> ℹ️ ردیفهای قیمتگذاری با `doctor_uuid` بیرون از TenantFilter خوانده میشوند: مقصدِ این
|
||||||
|
> تنظیم پزشک است در حالی که محیط فعالِ مالکِ کلینیک، خودِ کلینیک است. مجوزش همان بررسی
|
||||||
|
> بالاست. تا پیش از این، خواندن به محیط کاربر محدود میشد و ردیف موجود دیده نمیشد —
|
||||||
|
> هر ذخیره یک ردیف تازه میساخت (قیدِ یکتا ردیفِ ویزیت آزاد را نمیگیرد چون `insurance_id`
|
||||||
|
> آنجا `NULL` است) و مقدار ذخیرهشده هرگز به پنل برنمیگشت.
|
||||||
|
|
||||||
### Response `200`
|
### Response `200`
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -513,11 +520,18 @@ entity جاری از `#[CurrentUser]` resolve میشود: نقش `ROLE_DOCTOR
|
|||||||
|
|
||||||
tenant از `#[CurrentUser]` با `App\Patient\Security\PatientRecordScopeResolver` resolve میشود — همان رزولور پروندهها و صورتحسابها، تا قرارداد بیمه و صورتحسابی که از آن ساخته میشود هرگز به دو محیط متفاوت نیفتند. محیط فعال (`UserActiveContext`) تعیینکننده است، نه صرفاً ترتیب نقشها؛ مالک کلینیکی که خودش پزشک هم هست، قراردادهای **کلینیک** خود را میبیند.
|
tenant از `#[CurrentUser]` با `App\Patient\Security\PatientRecordScopeResolver` resolve میشود — همان رزولور پروندهها و صورتحسابها، تا قرارداد بیمه و صورتحسابی که از آن ساخته میشود هرگز به دو محیط متفاوت نیفتند. محیط فعال (`UserActiveContext`) تعیینکننده است، نه صرفاً ترتیب نقشها؛ مالک کلینیکی که خودش پزشک هم هست، قراردادهای **کلینیک** خود را میبیند.
|
||||||
|
|
||||||
|
**محدودهٔ تنظیم — اول پزشک، بعد کلینیک:** نوبتی که در کلینیک ثبت میشود محیطش «کلینیک» است، ولی تنظیمات بیمه معمولاً روی خودِ پزشک ذخیره شدهاند. هنگام محاسبه — انتخاب بیمهٔ نوبت، نوع خدمت، و قیمت ویزیت — اول تنظیمِ خودِ پزشک خوانده میشود و فقط در نبودِ آن تنظیمِ کلینیک. هر سه مورد جدا سنجیده میشوند: پزشکی که قرارداد بیمهٔ خودش را دارد ولی قیمت ویزیت را به کلینیک سپرده، هرکدام را از جای درست میگیرد. مرجع: `App\Insurance\Service\InsuranceScopeResolver`.
|
||||||
|
|
||||||
|
**درصد صفر:** `0` مقدار معتبری است و یعنی «این قرارداد آن نوع خدمت را پوشش نمیدهد» (سهم بیمار صددرصد). آنچه رد میشود، خالیماندنِ درصدِ یک نوع خدمتِ فعال است؛ پیشفرض مرکزیِ صفر هم «تعییننشده» حساب میشود، نه انتخابِ صفر.
|
||||||
|
|
||||||
**تنظیمات per-doctor در کلینیک چندپزشکه:** درصد و شرایط هر بیمه میتواند برای هر پزشک متفاوت باشد. همهٔ اندپوینتهای زیر یک پارامتر اختیاری `doctor_uuid` میپذیرند (در `GET`/`DELETE` از query، در `POST`/`PATCH`/`PUT` از بدنه). با آن، قرارداد بهجای موجودیتِ tenantِ کاربر جاری، بهازای پزشک هدف (`entity_type='doctor'`) خوانده/نوشته میشود — دقیقاً مثل `insurance-pricing`. **بدون** آن، رفتار قبلی (tenant کاربر جاری) دستنخورده میماند (سازگاری عقبرو). دسترسی با `doctor_uuid` هم مثل `insurance-pricing` بررسی میشود: `ROLE_ADMIN`، خودِ پزشک، یا کاربرِ عضو/مالکِ کلینیکِ آن پزشک با مجوز `services.view` (برای نوشتن `services.update`)؛ در غیر این صورت `403 ERR_ACCESS_DENIED`، و پزشکِ ناموجود `404 ERR_NOT_FOUND_001`.
|
**تنظیمات per-doctor در کلینیک چندپزشکه:** درصد و شرایط هر بیمه میتواند برای هر پزشک متفاوت باشد. همهٔ اندپوینتهای زیر یک پارامتر اختیاری `doctor_uuid` میپذیرند (در `GET`/`DELETE` از query، در `POST`/`PATCH`/`PUT` از بدنه). با آن، قرارداد بهجای موجودیتِ tenantِ کاربر جاری، بهازای پزشک هدف (`entity_type='doctor'`) خوانده/نوشته میشود — دقیقاً مثل `insurance-pricing`. **بدون** آن، رفتار قبلی (tenant کاربر جاری) دستنخورده میماند (سازگاری عقبرو). دسترسی با `doctor_uuid` هم مثل `insurance-pricing` بررسی میشود: `ROLE_ADMIN`، خودِ پزشک، یا کاربرِ عضو/مالکِ کلینیکِ آن پزشک با مجوز `services.view` (برای نوشتن `services.update`)؛ در غیر این صورت `403 ERR_ACCESS_DENIED`، و پزشکِ ناموجود `404 ERR_NOT_FOUND_001`.
|
||||||
|
|
||||||
### GET `/api/v1/billing/tenant-insurances`
|
### GET `/api/v1/billing/tenant-insurances`
|
||||||
لیست قراردادهای tenant جاری — **آخرین نسخهٔ هر بیمه، فعال یا غیرفعال** (برای toggle فعال/غیرفعال در UI مدیریت بیمه). `insurance_kind` = `kind` قرارداد در صورت تعیین، وگرنه نوع بیمه از کاتالوگ.
|
لیست قراردادهای tenant جاری — **آخرین نسخهٔ هر بیمه، فعال یا غیرفعال** (برای toggle فعال/غیرفعال در UI مدیریت بیمه). `insurance_kind` = `kind` قرارداد در صورت تعیین، وگرنه نوع بیمه از کاتالوگ.
|
||||||
|
|
||||||
|
> پارامتر `inherit=1` همان قاعدهٔ محدودهٔ بیمه را اعمال میکند: اول قراردادهای خودِ پزشک، و اگر پزشک هیچ قراردادی نداشته باشد قراردادهای کلینیکی که کاربر در آن ایستاده. بدون این پارامتر، پاسخ دقیقاً همان محیطِ هدف است — چیزی که صفحهٔ تنظیمات برای ویرایش لازم دارد.
|
||||||
|
|
||||||
|
|
||||||
**Query:** `doctor_uuid` (اختیاری) — قراردادهای همان پزشک را برمیگرداند (نگاه کنید به «تنظیمات per-doctor» بالا).
|
**Query:** `doctor_uuid` (اختیاری) — قراردادهای همان پزشک را برمیگرداند (نگاه کنید به «تنظیمات per-doctor» بالا).
|
||||||
|
|
||||||
**Permission:** `AUTH` (doctor/clinic)
|
**Permission:** `AUTH` (doctor/clinic)
|
||||||
|
|||||||
@@ -131,6 +131,12 @@ List the **authenticated user's own** payments (derived from the token — there
|
|||||||
|
|
||||||
**Permission:** `IS_AUTHENTICATED_FULLY`
|
**Permission:** `IS_AUTHENTICATED_FULLY`
|
||||||
|
|
||||||
|
> **فیلتر محیط اینجا اعمال نمیشود.** رکورد در محیطِ پزشکِ مقصد ثبت میشود، ولی
|
||||||
|
> مالکش از راه `user_id` تعیین میشود. کاربری که خودش صاحب محیط دیگری است — پزشک،
|
||||||
|
> منشی، مالک کلینیک — با فیلترِ محیطِ خودش رکورد خودش را نمیدید و ۴۰۴ میگرفت.
|
||||||
|
> دورزدن فیلتر فقط از راه `App\Shared\Tenant\TenantFilterScope` انجام میشود و
|
||||||
|
> مجوز دستنخورده باقی میماند.
|
||||||
|
|
||||||
### Query Parameters
|
### Query Parameters
|
||||||
| Param | Type | Default | Description |
|
| Param | Type | Default | Description |
|
||||||
|-------|------|---------|-------------|
|
|-------|------|---------|-------------|
|
||||||
@@ -405,6 +411,12 @@ Get payment status and details.
|
|||||||
|
|
||||||
**Permission:** `AUTH` — must be the payment owner or `ROLE_ADMIN`
|
**Permission:** `AUTH` — must be the payment owner or `ROLE_ADMIN`
|
||||||
|
|
||||||
|
> **فیلتر محیط اینجا اعمال نمیشود.** رکورد در محیطِ پزشکِ مقصد ثبت میشود، ولی
|
||||||
|
> مالکش از راه `user_id` تعیین میشود. کاربری که خودش صاحب محیط دیگری است — پزشک،
|
||||||
|
> منشی، مالک کلینیک — با فیلترِ محیطِ خودش رکورد خودش را نمیدید و ۴۰۴ میگرفت.
|
||||||
|
> دورزدن فیلتر فقط از راه `App\Shared\Tenant\TenantFilterScope` انجام میشود و
|
||||||
|
> مجوز دستنخورده باقی میماند.
|
||||||
|
|
||||||
### Path Parameters
|
### Path Parameters
|
||||||
| Param | Type | Description |
|
| Param | Type | Description |
|
||||||
|-------|------|-------------|
|
|-------|------|-------------|
|
||||||
|
|||||||
@@ -76,6 +76,30 @@ clinic_uuid صریحِ درخواست > UserActiveContext ذخیرهشده
|
|||||||
| SQL خام DBAL | ❌ |
|
| SQL خام DBAL | ❌ |
|
||||||
| فرزندان aggregate | ❌ — همیشه از ریشه JOIN کن |
|
| فرزندان aggregate | ❌ — همیشه از ریشه JOIN کن |
|
||||||
|
|
||||||
|
### استثنای مجاز: دادهٔ «مالِ خودِ کاربر»
|
||||||
|
|
||||||
|
چند رکورد در محیطِ یک مطب ثبت میشوند ولی مالکشان بیمار است، نه آن مطب: پرداختِ نوبت و
|
||||||
|
خودِ نوبتِ بیمار. مجوزشان با `user_id` بررسی میشود، نه با محیط.
|
||||||
|
|
||||||
|
اینجا فیلتر چیزی به امنیت اضافه نمیکند و فقط ضرر میزند: کاربری که خودش صاحب محیط
|
||||||
|
دیگری است — پزشکی که از مطب دیگری نوبت میگیرد، منشیای که جایی بیمار است — رکورد
|
||||||
|
خودش را نمیدید و صفحهٔ نتیجهٔ پرداخت ۴۰۴ میشد.
|
||||||
|
|
||||||
|
تنها راه مجاز دورزدن، `App\Shared\Tenant\TenantFilterScope::withoutFilter()` است.
|
||||||
|
عمداً یک کلاس جداست تا جاهای دورزدن قابل شمردن بمانند، و فیلتر را در `finally`
|
||||||
|
برمیگرداند تا بقیهٔ همان درخواست دوباره محدود شود.
|
||||||
|
|
||||||
|
مصرفکنندگان فعلی — هر سه کاربرمحور، نه محیطمحور:
|
||||||
|
|
||||||
|
| اندپوینت | چرا |
|
||||||
|
|---|---|
|
||||||
|
| `GET /api/v1/payment/{uuid}` | پرداختکننده باید پرداخت خودش را ببیند |
|
||||||
|
| `GET /api/v1/my/payments` | همان، به شکل فهرست |
|
||||||
|
| `GET /api/v1/appointments/user` | نوبتهای خودِ فرد بهعنوان بیمار |
|
||||||
|
|
||||||
|
`PayerSeesOwnPaymentTest` هر سه را میبندد و همزمان تأیید میکند که پرداختِ کاربر
|
||||||
|
دیگر همچنان ۴۰۳ میگیرد.
|
||||||
|
|
||||||
**فیلتر جایگزین authorization نیست.** `AppointmentAccessChecker`، `ClinicDoctorAccessChecker`، `SecretaryAccessChecker` و `PatientRecordScopeResolver` سر جایشان میمانند: آنها «چه کاری مجاز است» را جواب میدهند، فیلتر فقط «کدام ردیفها».
|
**فیلتر جایگزین authorization نیست.** `AppointmentAccessChecker`، `ClinicDoctorAccessChecker`، `SecretaryAccessChecker` و `PatientRecordScopeResolver` سر جایشان میمانند: آنها «چه کاری مجاز است» را جواب میدهند، فیلتر فقط «کدام ردیفها».
|
||||||
|
|
||||||
### تغییر رفتار: ۴۰۴ بهجای ۴۰۳
|
### تغییر رفتار: ۴۰۴ بهجای ۴۰۳
|
||||||
|
|||||||
@@ -1535,5 +1535,9 @@
|
|||||||
"1533": "Community 1533",
|
"1533": "Community 1533",
|
||||||
"1534": "Community 1534",
|
"1534": "Community 1534",
|
||||||
"1535": "Community 1535",
|
"1535": "Community 1535",
|
||||||
"1536": "Community 1536"
|
"1536": "Community 1536",
|
||||||
|
"1537": "Community 1537",
|
||||||
|
"1538": "Community 1538",
|
||||||
|
"1539": "Community 1539",
|
||||||
|
"1540": "Community 1540"
|
||||||
}
|
}
|
||||||
|
|||||||
+311
-300
File diff suppressed because it is too large
Load Diff
graphify-out/cache/ast/v0.8.44/01b63a0de55c5012246af8f415215e282024b99ad721411e44b22fc33759b904.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/049887f378df22103e52ad2164bdb307b97a66fd4cdb5f3f6ba55a42adff021e.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/09b36818cfdcdfc6af661f93e591dcf98b3a69c854c2dc2aa8c424c4110d9f4d.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/0b14e9e30faf94a2eea5ff3e5d2f3dacacd05b061b685cd0c92663102a87f3a2.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/0d8a03b4fa8d0d0c7c43242f61646ae97436a3ec2a562e8c7b3cdd8355494d43.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/0dfda763f44b6a57fa3696e575575886aeef00860fe3c5e75617608ca8d0c92c.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/0f58c9ff62fbd70b31b66e1bef38b78d97ab53ff80c5d184ef53baed8de2bb51.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/1cb561d88ded300b155e235433c5a1e5deed1acce065d1443856c49af2f5cde5.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/2463950a550e7243167a1651b77bc8b2ab0cef0121f2d7b30d1676c1e9f83430.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/2b275dc8813c902b6310d1cfeb7433cccf7bacab7a81286bdb8253cf61815159.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/2c325baa9f0c7cea01b0ad71060eb7b1f0aa1567c9a1c5d770e2e10051154f18.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/2da4701aa95f219f0e0350b3f564ff0377392f3815c446f9be464cab3f9ff7fa.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/39bd0796eef51b09b5a5015ab91c0d40fee6d77e8c86defcbd2def0789ed4d39.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/3a92c5b6cd8fb8faf62734dab803042be4dcd697a33d0533fb886f74ebbad9d1.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/3d78baf9eed4b5ce77d1f02332165cf3ff515558ec3859eb32f020504bbffd2b.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/3ded4eb3d25a2a0e2a97f67c58f5ba54454f78606355fd3b1477061a4d3eb6c3.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/3e8c88204cd2cd1ec1395865e18d7d34fe547f6f55ae61c7b31f301c748b4dc2.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/41a30cf4e79db78669677a0897799c069186ca3fff38d641834f32cfb0ccd72a.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/44a1b4140bf984e76344d3b4acda948ee60d35cb6d78ac9a7f70d300e6731e14.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/458121cf01a175fe6a6c8a0c5b2614ecc24fb335883afe9c59136352add8b791.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/48bd744fa6f42fa611841a8665db0e300da0209950a6ad7869374549a74f24f7.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/4c1f84509a6182c762fb56bd9c6b4efb3ef16d32cb19349de0f48f3d589ee279.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/4eb95f4df2f6f90d4aecd4c027aeec7174cda286990ba3d54fdbb243c01070fa.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/512392553ad01326d953730e964a07235fa9e81e2340c3198073545fc318e02f.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/569721fc586894965c8fdf214a0aa72d2e229a67435c92a1bbd5b452ba2918ab.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/570c5237bc10efa2ef08c79dd34512386dafbd5bf89f0aa51b5f16c1e48f3d67.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/5841462095ca5316deba7c8809f25651cb647a91064af2793d929324cdc08d17.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/5969ab02ef386bf027db1dabd367d8bfbb0785adb9532e2f7db3d703fcbb4553.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/5b584f4433682c7a8181e29c5c29b62a409d8ba783770624cc175b4952f67489.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/5bde238a7966e3f7acf2ee09c0289aad8ca3db05f4f1567bf6bf7a985766433d.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/5c8e247e2be9270215080b4320df87b25edf84b921ea6fe5127b81c8ab99cc09.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/5e794860b5160fed84522cd250769442cf7bee1ca44ac4f6adb718b32195463f.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/72e7110ea44cfb7e410659dc613c5c9606d5eaa99b76fa86d96f7cd88fc518b7.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/7452299da1335249792a4af5308bbd3625f878389a438ba3e80b10ccf8f009dc.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/9413ea3e40fa0ca49128823b2b9a9571b0e62b67d016a066d07080ac750593fa.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/959f90e5b23e7d091564aad1853f9b163b9f788e036c13e13ba43a67e390e9e1.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/a569a64a728dd08fbf8eabcf91c363981aa76daeced1aca499bc49a4a3a86eff.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/aa069a47992878cd2a57f4c60a03f17ee4c7e36c2ef5975a090f71679308e7ec.json
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_tenant_tenantfilterscope_php", "label": "TenantFilterScope.php", "file_type": "code", "source_file": "src/Shared/Tenant/TenantFilterScope.php", "source_location": "L1"}, {"id": "tenant_tenantfilterscope_tenantfilterscope", "label": "TenantFilterScope", "file_type": "code", "source_file": "src/Shared/Tenant/TenantFilterScope.php", "source_location": "L16"}, {"id": "tenant_tenantfilterscope_tenantfilterscope_construct", "label": ".__construct()", "file_type": "code", "source_file": "src/Shared/Tenant/TenantFilterScope.php", "source_location": "L21"}, {"id": "tenant_tenantfilterscope_tenantfilterscope_apply", "label": ".apply()", "file_type": "code", "source_file": "src/Shared/Tenant/TenantFilterScope.php", "source_location": "L24"}, {"id": "tenant_tenantfilterscope_tenantfilterscope_withoutfilter", "label": ".withoutFilter()", "file_type": "code", "source_file": "src/Shared/Tenant/TenantFilterScope.php", "source_location": "L45"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_tenant_tenantfilterscope_php", "target": "entitymanagerinterface", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "src/Shared/Tenant/TenantFilterScope.php", "source_location": "L5", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_tenant_tenantfilterscope_php", "target": "tenant_tenantfilterscope_tenantfilterscope", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Tenant/TenantFilterScope.php", "source_location": "L16", "weight": 1.0}, {"source": "tenant_tenantfilterscope_tenantfilterscope", "target": "tenant_tenantfilterscope_tenantfilterscope_construct", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Tenant/TenantFilterScope.php", "source_location": "L21", "weight": 1.0}, {"source": "tenant_tenantfilterscope_tenantfilterscope", "target": "tenant_tenantfilterscope_tenantfilterscope_apply", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Tenant/TenantFilterScope.php", "source_location": "L24", "weight": 1.0}, {"source": "tenant_tenantfilterscope_tenantfilterscope", "target": "tenant_tenantfilterscope_tenantfilterscope_withoutfilter", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Tenant/TenantFilterScope.php", "source_location": "L45", "weight": 1.0}, {"source": "tenant_tenantfilterscope_tenantfilterscope_withoutfilter", "target": "tenant_tenantfilterscope_tenantfilterscope_apply", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "src/Shared/Tenant/TenantFilterScope.php", "source_location": "L59", "weight": 1.0}], "raw_calls": [{"caller_nid": "tenant_tenantfilterscope_tenantfilterscope_apply", "callee": "setParameter", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Tenant/TenantFilterScope.php", "source_location": "L28", "receiver": null}, {"caller_nid": "tenant_tenantfilterscope_tenantfilterscope_apply", "callee": "setParameter", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Tenant/TenantFilterScope.php", "source_location": "L28", "receiver": null}, {"caller_nid": "tenant_tenantfilterscope_tenantfilterscope_apply", "callee": "enable", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Tenant/TenantFilterScope.php", "source_location": "L28", "receiver": null}, {"caller_nid": "tenant_tenantfilterscope_tenantfilterscope_apply", "callee": "getFilters", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Tenant/TenantFilterScope.php", "source_location": "L28", "receiver": null}, {"caller_nid": "tenant_tenantfilterscope_tenantfilterscope_withoutfilter", "callee": "getFilters", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Tenant/TenantFilterScope.php", "source_location": "L47", "receiver": null}, {"caller_nid": "tenant_tenantfilterscope_tenantfilterscope_withoutfilter", "callee": "isEnabled", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Tenant/TenantFilterScope.php", "source_location": "L48", "receiver": null}, {"caller_nid": "tenant_tenantfilterscope_tenantfilterscope_withoutfilter", "callee": "disable", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Tenant/TenantFilterScope.php", "source_location": "L51", "receiver": null}, {"caller_nid": "tenant_tenantfilterscope_tenantfilterscope_withoutfilter", "callee": "$query", "is_member_call": false, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Tenant/TenantFilterScope.php", "source_location": "L55", "receiver": null}]}
|
||||||
graphify-out/cache/ast/v0.8.44/aafb894aaea2de995f133a4b7432c26efca6805bdb6ac602e3a0701ce97ae77d.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/ac6d1c4a6beb80579f95afa9e69791886c418fc0110fe88c2ee7b4cfb76ba0b7.json
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_landing_landingpage_php", "label": "LandingPage.php", "file_type": "code", "source_file": "src/Shared/Landing/LandingPage.php", "source_location": "L1"}, {"id": "landing_landingpage_landingpage", "label": "LandingPage", "file_type": "code", "source_file": "src/Shared/Landing/LandingPage.php", "source_location": "L11"}, {"id": "landing_landingpage_landingpage_construct", "label": ".__construct()", "file_type": "code", "source_file": "src/Shared/Landing/LandingPage.php", "source_location": "L19"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_landing_landingpage_php", "target": "landing_landingpage_landingpage", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Landing/LandingPage.php", "source_location": "L11", "weight": 1.0}, {"source": "landing_landingpage_landingpage", "target": "landing_landingpage_landingpage_construct", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Landing/LandingPage.php", "source_location": "L19", "weight": 1.0}], "raw_calls": []}
|
||||||
graphify-out/cache/ast/v0.8.44/bc41882a81e08de70fad6dbcf87abe8f1f0504e20837c8c0466ad204cd673951.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/be4e9669a6088e5bebf2c17a9e653a0c6ae29b225359efd470b38684114f8c15.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/c1bc652a029edf60f2a89b3f509cb6654dc0af5d8c904d77c526134f059645ec.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/c5378b47beecc8d86d52c1b1bcc769709d4eee11baf6c3ebb7e8e5ad76624cdb.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/cc2bb2f0f6cc508e8e34e56587e94b208687c185e3a593572fad668151dd047d.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/cc4bbf7e54fe580d7971571bbb972550be65747a7f08a58603986aee041f6a5b.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/cce9c14b90c37986f177bbcc5d88b0749a516086b1eb0654da07a4dedc439955.json
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_landing_pages_md", "label": "landing-pages.md", "file_type": "document", "source_file": "docs/landing-pages.md", "source_location": "L1"}, {"id": "docs_landing_pages_\u0635\u0641\u062d\u0627\u062a_\u0641\u0631\u0648\u062f_\u0633\u0626\u0648\u06cc\u06cc", "label": "\u0635\u0641\u062d\u0627\u062a \u0641\u0631\u0648\u062f \u0633\u0626\u0648\u06cc\u06cc", "file_type": "document", "source_file": "docs/landing-pages.md", "source_location": "L1"}, {"id": "docs_landing_pages_\u0645\u062a\u0646_\u0647\u0631_\u0635\u0641\u062d\u0647_\u06a9\u062c\u0627\u0633\u062a", "label": "\u0645\u062a\u0646 \u0647\u0631 \u0635\u0641\u062d\u0647 \u06a9\u062c\u0627\u0633\u062a", "file_type": "document", "source_file": "docs/landing-pages.md", "source_location": "L16"}, {"id": "docs_landing_pages_\u0686\u0631\u0627_\u0631\u062c\u06cc\u0633\u062a\u0631\u06cc_\u0646\u0647_\u0647\u0641\u062a_\u0641\u0627\u06cc\u0644_twig", "label": "\u0686\u0631\u0627 \u0631\u062c\u06cc\u0633\u062a\u0631\u06cc\u060c \u0646\u0647 \u0647\u0641\u062a \u0641\u0627\u06cc\u0644 Twig", "file_type": "document", "source_file": "docs/landing-pages.md", "source_location": "L25"}, {"id": "docs_landing_pages_\u062a\u0645_\u0645\u0634\u062a\u0631\u06a9", "label": "\u062a\u0645 \u0645\u0634\u062a\u0631\u06a9", "file_type": "document", "source_file": "docs/landing-pages.md", "source_location": "L30"}, {"id": "docs_landing_pages_\u0631\u0648\u062a", "label": "\u0631\u0648\u062a", "file_type": "document", "source_file": "docs/landing-pages.md", "source_location": "L46"}, {"id": "docs_landing_pages_\u0633\u0626\u0648", "label": "\u0633\u0626\u0648", "file_type": "document", "source_file": "docs/landing-pages.md", "source_location": "L53"}, {"id": "docs_landing_pages_\u0646\u06a9\u062a\u0647_\u0645\u062d\u062a\u0648\u0627\u06cc\u06cc", "label": "\u0646\u06a9\u062a\u0647\u0654 \u0645\u062d\u062a\u0648\u0627\u06cc\u06cc", "file_type": "document", "source_file": "docs/landing-pages.md", "source_location": "L64"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_landing_pages_md", "target": "docs_landing_pages_\u0635\u0641\u062d\u0627\u062a_\u0641\u0631\u0648\u062f_\u0633\u0626\u0648\u06cc\u06cc", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/landing-pages.md", "source_location": "L1", "weight": 1.0}, {"source": "docs_landing_pages_\u0635\u0641\u062d\u0627\u062a_\u0641\u0631\u0648\u062f_\u0633\u0626\u0648\u06cc\u06cc", "target": "docs_landing_pages_\u0645\u062a\u0646_\u0647\u0631_\u0635\u0641\u062d\u0647_\u06a9\u062c\u0627\u0633\u062a", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/landing-pages.md", "source_location": "L16", "weight": 1.0}, {"source": "docs_landing_pages_\u0635\u0641\u062d\u0627\u062a_\u0641\u0631\u0648\u062f_\u0633\u0626\u0648\u06cc\u06cc", "target": "docs_landing_pages_\u0686\u0631\u0627_\u0631\u062c\u06cc\u0633\u062a\u0631\u06cc_\u0646\u0647_\u0647\u0641\u062a_\u0641\u0627\u06cc\u0644_twig", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/landing-pages.md", "source_location": "L25", "weight": 1.0}, {"source": "docs_landing_pages_\u0635\u0641\u062d\u0627\u062a_\u0641\u0631\u0648\u062f_\u0633\u0626\u0648\u06cc\u06cc", "target": "docs_landing_pages_\u062a\u0645_\u0645\u0634\u062a\u0631\u06a9", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/landing-pages.md", "source_location": "L30", "weight": 1.0}, {"source": "docs_landing_pages_\u0635\u0641\u062d\u0627\u062a_\u0641\u0631\u0648\u062f_\u0633\u0626\u0648\u06cc\u06cc", "target": "docs_landing_pages_\u0631\u0648\u062a", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/landing-pages.md", "source_location": "L46", "weight": 1.0}, {"source": "docs_landing_pages_\u0635\u0641\u062d\u0627\u062a_\u0641\u0631\u0648\u062f_\u0633\u0626\u0648\u06cc\u06cc", "target": "docs_landing_pages_\u0633\u0626\u0648", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/landing-pages.md", "source_location": "L53", "weight": 1.0}, {"source": "docs_landing_pages_\u0635\u0641\u062d\u0627\u062a_\u0641\u0631\u0648\u062f_\u0633\u0626\u0648\u06cc\u06cc", "target": "docs_landing_pages_\u0646\u06a9\u062a\u0647_\u0645\u062d\u062a\u0648\u0627\u06cc\u06cc", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/landing-pages.md", "source_location": "L64", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
graphify-out/cache/ast/v0.8.44/d230d52722271a14cffcb54eb95e5c3aa42eeab359c6cf65b33c0d9ac3a99a53.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/d9f53787ebfe7774a85830b9a98b8a6d1d9fbc2c99e347766d330fd3a6cfb27f.json
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_migrations_version20260810052544_php", "label": "Version20260810052544.php", "file_type": "code", "source_file": "migrations/Version20260810052544.php", "source_location": "L1"}, {"id": "migrations_version20260810052544_version20260810052544", "label": "Version20260810052544", "file_type": "code", "source_file": "migrations/Version20260810052544.php", "source_location": "L13"}, {"id": "abstractmigration", "label": "AbstractMigration", "file_type": "code", "source_file": "", "source_location": ""}, {"id": "migrations_version20260810052544_version20260810052544_getdescription", "label": ".getDescription()", "file_type": "code", "source_file": "migrations/Version20260810052544.php", "source_location": "L15"}, {"id": "migrations_version20260810052544_version20260810052544_up", "label": ".up()", "file_type": "code", "source_file": "migrations/Version20260810052544.php", "source_location": "L20"}, {"id": "schema", "label": "Schema", "file_type": "code", "source_file": "migrations/Version20260810052544.php", "source_location": "L20"}, {"id": "migrations_version20260810052544_version20260810052544_down", "label": ".down()", "file_type": "code", "source_file": "migrations/Version20260810052544.php", "source_location": "L26"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_migrations_version20260810052544_php", "target": "schema", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "migrations/Version20260810052544.php", "source_location": "L7", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_migrations_version20260810052544_php", "target": "abstractmigration", "relation": "imports", "context": "import", "confidence": "EXTRACTED", "source_file": "migrations/Version20260810052544.php", "source_location": "L8", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_migrations_version20260810052544_php", "target": "migrations_version20260810052544_version20260810052544", "relation": "contains", "confidence": "EXTRACTED", "source_file": "migrations/Version20260810052544.php", "source_location": "L13", "weight": 1.0}, {"source": "migrations_version20260810052544_version20260810052544", "target": "abstractmigration", "relation": "inherits", "confidence": "EXTRACTED", "source_file": "migrations/Version20260810052544.php", "source_location": "L13", "weight": 1.0}, {"source": "migrations_version20260810052544_version20260810052544", "target": "migrations_version20260810052544_version20260810052544_getdescription", "relation": "method", "confidence": "EXTRACTED", "source_file": "migrations/Version20260810052544.php", "source_location": "L15", "weight": 1.0}, {"source": "migrations_version20260810052544_version20260810052544", "target": "migrations_version20260810052544_version20260810052544_up", "relation": "method", "confidence": "EXTRACTED", "source_file": "migrations/Version20260810052544.php", "source_location": "L20", "weight": 1.0}, {"source": "migrations_version20260810052544_version20260810052544_up", "target": "schema", "relation": "references", "confidence": "EXTRACTED", "source_file": "migrations/Version20260810052544.php", "source_location": "L20", "weight": 1.0, "context": "parameter_type"}, {"source": "migrations_version20260810052544_version20260810052544", "target": "migrations_version20260810052544_version20260810052544_down", "relation": "method", "confidence": "EXTRACTED", "source_file": "migrations/Version20260810052544.php", "source_location": "L26", "weight": 1.0}, {"source": "migrations_version20260810052544_version20260810052544_down", "target": "schema", "relation": "references", "confidence": "EXTRACTED", "source_file": "migrations/Version20260810052544.php", "source_location": "L26", "weight": 1.0, "context": "parameter_type"}], "raw_calls": [{"caller_nid": "migrations_version20260810052544_version20260810052544_up", "callee": "addSql", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/migrations/Version20260810052544.php", "source_location": "L22", "receiver": null}, {"caller_nid": "migrations_version20260810052544_version20260810052544_up", "callee": "addSql", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/migrations/Version20260810052544.php", "source_location": "L23", "receiver": null}, {"caller_nid": "migrations_version20260810052544_version20260810052544_down", "callee": "addSql", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/migrations/Version20260810052544.php", "source_location": "L28", "receiver": null}, {"caller_nid": "migrations_version20260810052544_version20260810052544_down", "callee": "addSql", "is_member_call": true, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/migrations/Version20260810052544.php", "source_location": "L29", "receiver": null}]}
|
||||||
graphify-out/cache/ast/v0.8.44/de42e119148fb37b748205df9420fb6729f0119633815a7d6519f646118bfdfb.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/df30ce76b1077bced03afb24a2c29561ff1db4fa875081371e04bee83ac74b83.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/df7077f44c59334b0f7af62834c2513230644ac33b581db77f84cd94f92bfcb1.json
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_guide_readme_md", "label": "README.md", "file_type": "document", "source_file": "docs/guide/README.md", "source_location": "L1"}, {"id": "guide_readme_\u0631\u0627\u0647\u0646\u0645\u0627\u06cc_\u06a9\u0627\u0631\u0628\u0631\u06cc_\u06a9\u0644\u06cc\u0646\u06cc\u06a9_\u067e\u0631\u0648", "label": "\u0631\u0627\u0647\u0646\u0645\u0627\u06cc \u06a9\u0627\u0631\u0628\u0631\u06cc \u06a9\u0644\u06cc\u0646\u06cc\u06a9\u200c\u067e\u0631\u0648", "file_type": "document", "source_file": "docs/guide/README.md", "source_location": "L1"}, {"id": "guide_readme_\u0631\u0627\u0647\u0646\u0645\u0627\u0647\u0627", "label": "\u0631\u0627\u0647\u0646\u0645\u0627\u0647\u0627", "file_type": "document", "source_file": "docs/guide/README.md", "source_location": "L6"}, {"id": "guide_readme_\u0686\u0637\u0648\u0631_\u0628\u0647_\u0631\u0648\u0632_\u0646\u06af\u0647_\u062f\u0627\u0634\u062a\u0647_\u0645\u06cc_\u0634\u0648\u062f", "label": "\u0686\u0637\u0648\u0631 \u0628\u0647\u200c\u0631\u0648\u0632 \u0646\u06af\u0647 \u062f\u0627\u0634\u062a\u0647 \u0645\u06cc\u200c\u0634\u0648\u062f", "file_type": "document", "source_file": "docs/guide/README.md", "source_location": "L16"}, {"id": "guide_readme_\u062d\u0633\u0627\u0628_\u0647\u0627\u06cc_\u0646\u0645\u0648\u0646\u0647", "label": "\u062d\u0633\u0627\u0628\u200c\u0647\u0627\u06cc \u0646\u0645\u0648\u0646\u0647", "file_type": "document", "source_file": "docs/guide/README.md", "source_location": "L33"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_guide_readme_md", "target": "guide_readme_\u0631\u0627\u0647\u0646\u0645\u0627\u06cc_\u06a9\u0627\u0631\u0628\u0631\u06cc_\u06a9\u0644\u06cc\u0646\u06cc\u06a9_\u067e\u0631\u0648", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/guide/README.md", "source_location": "L1", "weight": 1.0}, {"source": "guide_readme_\u0631\u0627\u0647\u0646\u0645\u0627\u06cc_\u06a9\u0627\u0631\u0628\u0631\u06cc_\u06a9\u0644\u06cc\u0646\u06cc\u06a9_\u067e\u0631\u0648", "target": "guide_readme_\u0631\u0627\u0647\u0646\u0645\u0627\u0647\u0627", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/guide/README.md", "source_location": "L6", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_guide_readme_md", "target": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_guide_independent_doctor_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "docs/guide/README.md", "source_location": "L10", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_guide_readme_md", "target": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_guide_clinic_owner_doctor_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "docs/guide/README.md", "source_location": "L11", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_guide_readme_md", "target": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_guide_clinic_manager_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "docs/guide/README.md", "source_location": "L12", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_guide_readme_md", "target": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_guide_secretary_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "docs/guide/README.md", "source_location": "L13", "weight": 1.0}, {"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_guide_readme_md", "target": "users_hamed_pj_my_pj_clinic_pro_clinicpro_docs_guide_staff_md", "relation": "references", "confidence": "EXTRACTED", "source_file": "docs/guide/README.md", "source_location": "L14", "weight": 1.0}, {"source": "guide_readme_\u0631\u0627\u0647\u0646\u0645\u0627\u06cc_\u06a9\u0627\u0631\u0628\u0631\u06cc_\u06a9\u0644\u06cc\u0646\u06cc\u06a9_\u067e\u0631\u0648", "target": "guide_readme_\u0686\u0637\u0648\u0631_\u0628\u0647_\u0631\u0648\u0632_\u0646\u06af\u0647_\u062f\u0627\u0634\u062a\u0647_\u0645\u06cc_\u0634\u0648\u062f", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/guide/README.md", "source_location": "L16", "weight": 1.0}, {"source": "guide_readme_\u0631\u0627\u0647\u0646\u0645\u0627\u06cc_\u06a9\u0627\u0631\u0628\u0631\u06cc_\u06a9\u0644\u06cc\u0646\u06cc\u06a9_\u067e\u0631\u0648", "target": "guide_readme_\u062d\u0633\u0627\u0628_\u0647\u0627\u06cc_\u0646\u0645\u0648\u0646\u0647", "relation": "contains", "confidence": "EXTRACTED", "source_file": "docs/guide/README.md", "source_location": "L33", "weight": 1.0}], "input_tokens": 0, "output_tokens": 0}
|
||||||
graphify-out/cache/ast/v0.8.44/e452d34ad91e2512cbb23f97ff989e1d8b932804bae7a274638919e20b2df21f.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/e78d3aa29731a3c7c8895ba06c7a81be33907edae367f2b7695b23dd64d5970f.json
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_tenant_globaltables_php", "label": "GlobalTables.php", "file_type": "code", "source_file": "src/Shared/Tenant/GlobalTables.php", "source_location": "L1"}, {"id": "tenant_globaltables_globaltables", "label": "GlobalTables", "file_type": "code", "source_file": "src/Shared/Tenant/GlobalTables.php", "source_location": "L16"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_tenant_globaltables_php", "target": "tenant_globaltables_globaltables", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Tenant/GlobalTables.php", "source_location": "L16", "weight": 1.0}], "raw_calls": []}
|
||||||
graphify-out/cache/ast/v0.8.44/ebf06374dcfd9af35435ef667a949aeecf63bffeb1ae954b62e61da103bac62d.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/edaa572d1f556bcf7cc47906b37e182d2b7807a7c2504b5064ed135147fde17d.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/ee98b6893d077f6dda509731c1663e69fb5056a30a15934b16e339cdead18555.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/f1e85abfc35ea36cb91521cfd8f9ff476fbcdb80c44285d7f42905b8071ce191.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/f357807cbb06cfe8d6825fbce58bc1f71c6dc18462a69dc57745a559df33d2e8.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/f60bffb123ec59ed62b7a0af3df7f99fb03def8f128f73a39800694a399d05f7.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/f79240b9dae08aa6b431b4aae60ea9736d7fe4e62f624672adce6a59bd2c98a8.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/f8890bb3ebec0298d831a5e0a8386ca55e961ca3022c18ec1ff7d4f1d2271d26.json
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
{"nodes": [{"id": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_landing_landingregistry_php", "label": "LandingRegistry.php", "file_type": "code", "source_file": "src/Shared/Landing/LandingRegistry.php", "source_location": "L1"}, {"id": "landing_landingregistry_landingregistry", "label": "LandingRegistry", "file_type": "code", "source_file": "src/Shared/Landing/LandingRegistry.php", "source_location": "L11"}, {"id": "landing_landingregistry_landingregistry_all", "label": ".all()", "file_type": "code", "source_file": "src/Shared/Landing/LandingRegistry.php", "source_location": "L25"}, {"id": "landing_landingregistry_landingregistry_find", "label": ".find()", "file_type": "code", "source_file": "src/Shared/Landing/LandingRegistry.php", "source_location": "L30"}, {"id": "landingpage", "label": "LandingPage", "file_type": "code", "source_file": "src/Shared/Landing/LandingRegistry.php", "source_location": "L30"}, {"id": "landing_landingregistry_landingregistry_build", "label": ".build()", "file_type": "code", "source_file": "src/Shared/Landing/LandingRegistry.php", "source_location": "L36"}], "edges": [{"source": "users_hamed_pj_my_pj_clinic_pro_clinicpro_src_shared_landing_landingregistry_php", "target": "landing_landingregistry_landingregistry", "relation": "contains", "confidence": "EXTRACTED", "source_file": "src/Shared/Landing/LandingRegistry.php", "source_location": "L11", "weight": 1.0}, {"source": "landing_landingregistry_landingregistry", "target": "landing_landingregistry_landingregistry_all", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Landing/LandingRegistry.php", "source_location": "L25", "weight": 1.0}, {"source": "landing_landingregistry_landingregistry", "target": "landing_landingregistry_landingregistry_find", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Landing/LandingRegistry.php", "source_location": "L30", "weight": 1.0}, {"source": "landing_landingregistry_landingregistry_find", "target": "landingpage", "relation": "references", "confidence": "EXTRACTED", "source_file": "src/Shared/Landing/LandingRegistry.php", "source_location": "L30", "weight": 1.0, "context": "return_type"}, {"source": "landing_landingregistry_landingregistry", "target": "landing_landingregistry_landingregistry_build", "relation": "method", "confidence": "EXTRACTED", "source_file": "src/Shared/Landing/LandingRegistry.php", "source_location": "L36", "weight": 1.0}, {"source": "landing_landingregistry_landingregistry_all", "target": "landing_landingregistry_landingregistry_build", "relation": "calls", "context": "call", "confidence": "EXTRACTED", "source_file": "src/Shared/Landing/LandingRegistry.php", "source_location": "L27", "weight": 1.0}], "raw_calls": [{"caller_nid": "landing_landingregistry_landingregistry_build", "callee": "array_column", "is_member_call": false, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Landing/LandingRegistry.php", "source_location": "L202", "receiver": null}, {"caller_nid": "landing_landingregistry_landingregistry_build", "callee": "array_map", "is_member_call": false, "source_file": "/Users/hamed/pj/my_pj/clinic_pro/clinicpro/src/Shared/Landing/LandingRegistry.php", "source_location": "L203", "receiver": null}]}
|
||||||
graphify-out/cache/ast/v0.8.44/fc42ab61118a7067ac276e9c58cf40af863f97c462a6f7d4fe3896cbc1df5f16.json
Vendored
+1
File diff suppressed because one or more lines are too long
graphify-out/cache/ast/v0.8.44/fe657c9f2b908a97742f50c0475ba3848415f5f0fbd42a9249f9f20406e46c89.json
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+22327
-3312
File diff suppressed because it is too large
Load Diff
+623
-313
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* پاکسازی ردیفهای تکراریِ «قیمت ویزیت آزاد».
|
||||||
|
*
|
||||||
|
* قید یکتای (entity_type, entity_id, insurance_id) ردیف ویزیت آزاد را نمیپوشاند،
|
||||||
|
* چون `insurance_id` آنجا NULL است و MySQL چند NULL را تکراری نمیشمارد. تا وقتی
|
||||||
|
* خواندنِ ردیفِ موجود زیر TenantFilter کور بود، هر بار ذخیره یک ردیف تازه میساخت.
|
||||||
|
* تازهترین ردیف همان چیزی است که کاربر آخرینبار ثبت کرده، پس بقیه حذف میشوند.
|
||||||
|
*/
|
||||||
|
final class Version20260818131524 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return 'Collapse duplicate free-visit pricing rows to the newest one per tenant';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$this->addSql(<<<'SQL'
|
||||||
|
DELETE p FROM entity_insurance_pricing p
|
||||||
|
JOIN (
|
||||||
|
SELECT entity_type, entity_id, MAX(id) AS keep_id
|
||||||
|
FROM entity_insurance_pricing
|
||||||
|
WHERE insurance_id IS NULL
|
||||||
|
GROUP BY entity_type, entity_id
|
||||||
|
HAVING COUNT(*) > 1
|
||||||
|
) newest
|
||||||
|
ON newest.entity_type = p.entity_type
|
||||||
|
AND newest.entity_id = p.entity_id
|
||||||
|
WHERE p.insurance_id IS NULL
|
||||||
|
AND p.id <> newest.keep_id
|
||||||
|
SQL);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// ردیفهای حذفشده تکراری بودند؛ برگرداندنشان همان وضعیت خراب را باز میسازد.
|
||||||
|
$this->throwIrreversibleMigrationException('Duplicate pricing rows are not restored.');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,8 @@ class AppointmentController extends BaseController
|
|||||||
private readonly \App\Resource\Service\ResourceBookingSlotService $resourceSlots,
|
private readonly \App\Resource\Service\ResourceBookingSlotService $resourceSlots,
|
||||||
private readonly \App\Resource\Service\PublicResourceBookingService $publicResources,
|
private readonly \App\Resource\Service\PublicResourceBookingService $publicResources,
|
||||||
private readonly \App\Treatment\Service\SessionBookingLink $sessionLink,
|
private readonly \App\Treatment\Service\SessionBookingLink $sessionLink,
|
||||||
|
private readonly \App\Shared\Tenant\TenantFilterScope $tenantScope,
|
||||||
|
private readonly \App\Patient\Service\PatientService $patients,
|
||||||
private readonly \Psr\Log\LoggerInterface $logger,
|
private readonly \Psr\Log\LoggerInterface $logger,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -724,7 +726,7 @@ class AppointmentController extends BaseController
|
|||||||
return $this->error(ErrorCodes::ERR_ACCESS_DENIED, 'دسترسی ممنوع', 403);
|
return $this->error(ErrorCodes::ERR_ACCESS_DENIED, 'دسترسی ممنوع', 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
$payload = $appointment->toArray();
|
$payload = $this->appointmentWithVenue($appointment);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* نوبتِ سرویسیِ کلینیک `service_section_id` را روی خودِ نوبت نمینویسد؛ فقط
|
* نوبتِ سرویسیِ کلینیک `service_section_id` را روی خودِ نوبت نمینویسد؛ فقط
|
||||||
@@ -884,10 +886,55 @@ class AppointmentController extends BaseController
|
|||||||
#[Route('/api/v1/appointments/user', methods: ['GET'])]
|
#[Route('/api/v1/appointments/user', methods: ['GET'])]
|
||||||
public function listByUser(Request $request, #[CurrentUser] User $user): JsonResponse
|
public function listByUser(Request $request, #[CurrentUser] User $user): JsonResponse
|
||||||
{
|
{
|
||||||
$status = $request->query->get('status');
|
$status = $request->query->get('status');
|
||||||
$appointments = $this->appointmentRepo->findByUser($user, $status);
|
|
||||||
|
|
||||||
return $this->success(['data' => array_map(fn(Appointment $a) => $a->toArray(), $appointments)]);
|
// نوبتِ بیمار در محیطِ پزشکِ مقصد ثبت میشود. کاربری که خودش صاحب محیط دیگری
|
||||||
|
// است — پزشک، منشی، مالک کلینیک — با فیلترِ محیطِ خودش نوبتهای خودش را
|
||||||
|
// نمیدید. صاحب اینجا با `user_id` تعیین میشود، نه با محیط.
|
||||||
|
// `toArray()` هم داخل محدوده است: proxyهای پزشک و کلینیک آنجا باز میشوند و
|
||||||
|
// با فیلترِ برگشته، Doctrine `EntityNotFoundException` میدهد.
|
||||||
|
$rows = $this->tenantScope->withoutFilter(fn () => array_map(
|
||||||
|
fn(Appointment $a) => $this->appointmentWithVenue($a),
|
||||||
|
$this->appointmentRepo->findByUser($user, $status),
|
||||||
|
));
|
||||||
|
|
||||||
|
return $this->success(['data' => $rows]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* نوبت بههمراه نشانیِ واقعیِ همان نوبت — با شمارهٔ تماس.
|
||||||
|
*
|
||||||
|
* `Appointment::toArray()` نشانی را از «اولین آدرسِ پزشک» برمیداشت، پس بیماری که
|
||||||
|
* نوبتش در کلینیک یا در شعبهٔ دوم بود، نشانیِ مطبِ شخصیِ پزشک را میدید. آدرسِ درست
|
||||||
|
* روی خودِ نوبت ثبت شده (`address_id`) و همانجا برای مطب و کلینیک یکسان کار میکند.
|
||||||
|
*
|
||||||
|
* شماره هم اینجا برمیگردد و نه در پاسخهای عمومی: مخاطبِ این متد یا صاحب نوبت است
|
||||||
|
* یا کسی که `canView()` تأییدش کرده. اگر خودِ آدرس شماره نداشته باشد، شمارهٔ کلینیکِ
|
||||||
|
* همان نوبت جایش مینشیند تا بیمار بیراهارتباطی نماند.
|
||||||
|
*/
|
||||||
|
private function appointmentWithVenue(Appointment $appointment): array
|
||||||
|
{
|
||||||
|
$payload = $appointment->toArray();
|
||||||
|
|
||||||
|
$addressId = $appointment->getAddressId();
|
||||||
|
$address = $addressId !== null ? $this->addressRepo->find($addressId) : null;
|
||||||
|
|
||||||
|
if ($address !== null) {
|
||||||
|
$payload['address'] = $address->toArray(null, true);
|
||||||
|
} elseif ($payload['address'] !== null) {
|
||||||
|
// نوبتهای قدیمی `address_id` ندارند؛ همان آدرسِ حدسیِ toArray() میماند،
|
||||||
|
// ولی دستکم بدون شمارهٔ اشتباه نمایش داده نمیشود.
|
||||||
|
$payload['address']['telephone'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (($payload['address']['telephone'] ?? null) === null && $payload['address'] !== null) {
|
||||||
|
$clinic = $appointment->getClinic();
|
||||||
|
if ($clinic !== null) {
|
||||||
|
$payload['address']['telephone'] = $clinic->getTelephone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function canView(Appointment $a, User $user): bool
|
private function canView(Appointment $a, User $user): bool
|
||||||
@@ -1436,7 +1483,12 @@ class AppointmentController extends BaseController
|
|||||||
if ($completed) {
|
if ($completed) {
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success(['data' => $appointment->toArray()]);
|
// انتخاب بیمه میتواند بعد از ساختهشدنِ مراجعه عوض شود — پذیرش اول نوبت را
|
||||||
|
// قطعی میکند و بعد بیمه را اصلاح میکند. بدون این، سهمهای مراجعه روی همان
|
||||||
|
// محاسبهٔ اول میمانند و صفحهٔ پرداخت سهمِ بیمه را از بیمار میخواهد.
|
||||||
|
$this->patients->resyncInsuranceShares($appointment);
|
||||||
|
|
||||||
|
return $this->success(['data' => $this->appointmentWithVenue($appointment)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Insurance\Enum\InsuranceType;
|
|||||||
use App\Insurance\Enum\ServiceCategory;
|
use App\Insurance\Enum\ServiceCategory;
|
||||||
use App\Insurance\Repository\InsuranceRepository;
|
use App\Insurance\Repository\InsuranceRepository;
|
||||||
use App\Insurance\Repository\TenantInsuranceRepository;
|
use App\Insurance\Repository\TenantInsuranceRepository;
|
||||||
|
use App\Insurance\Service\InsuranceScopeResolver;
|
||||||
use App\Insurance\Service\TenantServiceCategoryService;
|
use App\Insurance\Service\TenantServiceCategoryService;
|
||||||
use App\Shared\Constant\ErrorCodes;
|
use App\Shared\Constant\ErrorCodes;
|
||||||
use App\Shared\Exception\AppException;
|
use App\Shared\Exception\AppException;
|
||||||
@@ -22,6 +23,7 @@ class AppointmentInsuranceService
|
|||||||
private readonly TenantServiceCategoryService $serviceCategories,
|
private readonly TenantServiceCategoryService $serviceCategories,
|
||||||
private readonly TenantInsuranceRepository $tenantInsuranceRepo,
|
private readonly TenantInsuranceRepository $tenantInsuranceRepo,
|
||||||
private readonly InsuranceRepository $insuranceRepo,
|
private readonly InsuranceRepository $insuranceRepo,
|
||||||
|
private readonly InsuranceScopeResolver $scope,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,6 +41,32 @@ class AppointmentInsuranceService
|
|||||||
: ['doctor', (int) $appointment->getDoctor()->getId()];
|
: ['doctor', (int) $appointment->getDoctor()->getId()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* محیطی که تنظیمات بیمهٔ این نوبت از آن خوانده میشود: اول خودِ پزشک، در نبودِ
|
||||||
|
* تنظیمِ او کلینیکِ همان نوبت. {@see InsuranceScopeResolver}
|
||||||
|
*
|
||||||
|
* جدا از {@see tenantOf()} است: مالکیتِ نوبت — و پرونده و پرداختش — همچنان با
|
||||||
|
* کلینیک است؛ این فقط میگوید قرارداد و درصدها را کجا پیدا کنیم.
|
||||||
|
*
|
||||||
|
* @return array{0: string, 1: int}
|
||||||
|
*/
|
||||||
|
public function contractScopeOf(Appointment $appointment): array
|
||||||
|
{
|
||||||
|
return $this->scope->forContracts(
|
||||||
|
(int) $appointment->getDoctor()->getId(),
|
||||||
|
$appointment->getClinic()?->getId() !== null ? (int) $appointment->getClinic()->getId() : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array{0: string, 1: int} */
|
||||||
|
public function categoryScopeOf(Appointment $appointment): array
|
||||||
|
{
|
||||||
|
return $this->scope->forServiceCategories(
|
||||||
|
(int) $appointment->getDoctor()->getId(),
|
||||||
|
$appointment->getClinic()?->getId() !== null ? (int) $appointment->getClinic()->getId() : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* نوع خدمتِ مؤثر برای محاسبه: انتخابِ نوبت، وگرنه تنها نوع فعالِ tenant،
|
* نوع خدمتِ مؤثر برای محاسبه: انتخابِ نوبت، وگرنه تنها نوع فعالِ tenant،
|
||||||
* وگرنه سرپایی (رفتار تاریخیِ ویزیت).
|
* وگرنه سرپایی (رفتار تاریخیِ ویزیت).
|
||||||
@@ -49,7 +77,7 @@ class AppointmentInsuranceService
|
|||||||
return $appointment->getInsuranceServiceCategory();
|
return $appointment->getInsuranceServiceCategory();
|
||||||
}
|
}
|
||||||
|
|
||||||
[$entityType, $entityId] = $this->tenantOf($appointment);
|
[$entityType, $entityId] = $this->categoryScopeOf($appointment);
|
||||||
|
|
||||||
return $this->serviceCategories->defaultCategory($entityType, $entityId) ?? ServiceCategory::Outpatient;
|
return $this->serviceCategories->defaultCategory($entityType, $entityId) ?? ServiceCategory::Outpatient;
|
||||||
}
|
}
|
||||||
@@ -63,23 +91,26 @@ class AppointmentInsuranceService
|
|||||||
*/
|
*/
|
||||||
public function apply(Appointment $appointment, array $data): void
|
public function apply(Appointment $appointment, array $data): void
|
||||||
{
|
{
|
||||||
[$entityType, $entityId] = $this->tenantOf($appointment);
|
// هر کدام محیط خودش را دارد: پزشکی که قرارداد بیمهٔ خودش را دارد ولی نوع
|
||||||
|
// خدمات را به کلینیک سپرده، هر دو را از جای درست میگیرد.
|
||||||
|
[$categoryType, $categoryId] = $this->categoryScopeOf($appointment);
|
||||||
|
[$contractType, $contractId] = $this->contractScopeOf($appointment);
|
||||||
|
|
||||||
if (array_key_exists('insurance_service_category', $data)) {
|
if (array_key_exists('insurance_service_category', $data)) {
|
||||||
$appointment->setInsuranceServiceCategory(
|
$appointment->setInsuranceServiceCategory(
|
||||||
$this->resolveCategory($data['insurance_service_category'], $entityType, $entityId)
|
$this->resolveCategory($data['insurance_service_category'], $categoryType, $categoryId)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (array_key_exists('insurance_base_id', $data)) {
|
if (array_key_exists('insurance_base_id', $data)) {
|
||||||
$appointment->setInsuranceBaseId(
|
$appointment->setInsuranceBaseId(
|
||||||
$this->resolveInsuranceId($data['insurance_base_id'], $entityType, $entityId, false)
|
$this->resolveInsuranceId($data['insurance_base_id'], $contractType, $contractId, false)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (array_key_exists('insurance_supplementary_id', $data)) {
|
if (array_key_exists('insurance_supplementary_id', $data)) {
|
||||||
$appointment->setInsuranceSupplementaryId(
|
$appointment->setInsuranceSupplementaryId(
|
||||||
$this->resolveInsuranceId($data['insurance_supplementary_id'], $entityType, $entityId, true)
|
$this->resolveInsuranceId($data['insurance_supplementary_id'], $contractType, $contractId, true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,16 +19,35 @@ class UserActiveContextRepository extends ServiceEntityRepository
|
|||||||
return $this->findOneBy(['user' => $user]);
|
return $this->findOneBy(['user' => $user]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ثبت یا بهروزرسانی محیط فعال کاربر — مقاوم در برابر رقابت همزمانی.
|
||||||
|
*
|
||||||
|
* پنل هنگام بالا آمدن چند درخواست `/oauth/userinfo` را تقریباً همزمان میفرستد و
|
||||||
|
* وقتی کاربر فقط یک محیط دارد، همهشان میخواستند همان ردیف را بسازند: هر دو
|
||||||
|
* `findByUser()` را `null` میدیدند و دومی با
|
||||||
|
* `Duplicate entry for key 'PRIMARY'` میترکید (و در همان حال EntityManager بسته
|
||||||
|
* میشد). درج با `ON DUPLICATE KEY UPDATE` این مسابقه را حذف میکند.
|
||||||
|
*/
|
||||||
public function upsert(User $user, string $dbUuid, string $dbType): UserActiveContext
|
public function upsert(User $user, string $dbUuid, string $dbType): UserActiveContext
|
||||||
{
|
{
|
||||||
$ctx = $this->findByUser($user);
|
$ctx = $this->findByUser($user);
|
||||||
if ($ctx === null) {
|
if ($ctx !== null) {
|
||||||
$ctx = new UserActiveContext($user, $dbUuid, $dbType);
|
|
||||||
$this->getEntityManager()->persist($ctx);
|
|
||||||
} else {
|
|
||||||
$ctx->setContext($dbUuid, $dbType);
|
$ctx->setContext($dbUuid, $dbType);
|
||||||
|
$this->getEntityManager()->flush();
|
||||||
|
|
||||||
|
return $ctx;
|
||||||
}
|
}
|
||||||
$this->getEntityManager()->flush();
|
|
||||||
return $ctx;
|
$this->getEntityManager()->getConnection()->executeStatement(
|
||||||
|
'INSERT INTO user_active_context (user_id, db_uuid, db_type, updated_at)
|
||||||
|
VALUES (:user, :uuid, :type, :now)
|
||||||
|
ON DUPLICATE KEY UPDATE db_uuid = VALUES(db_uuid), db_type = VALUES(db_type), updated_at = VALUES(updated_at)',
|
||||||
|
['user' => $user->getId(), 'uuid' => $dbUuid, 'type' => $dbType, 'now' => time()],
|
||||||
|
);
|
||||||
|
|
||||||
|
// ردیف بیرون از ORM ساخته شد، پس entity باید از دیتابیس خوانده شود تا
|
||||||
|
// identity map همان چیزی را نشان دهد که واقعاً ذخیره شده است.
|
||||||
|
return $this->findByUser($user)
|
||||||
|
?? throw new \RuntimeException('user_active_context row vanished right after upsert');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class InvoiceService
|
|||||||
private readonly InsuranceRepository $insuranceRepo,
|
private readonly InsuranceRepository $insuranceRepo,
|
||||||
private readonly EventDispatcherInterface $events,
|
private readonly EventDispatcherInterface $events,
|
||||||
private readonly ActorIdentityResolver $actorIdentity,
|
private readonly ActorIdentityResolver $actorIdentity,
|
||||||
|
private readonly \App\Insurance\Service\InsuranceScopeResolver $insuranceScope,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** @var array<int, string|null> نام بیمهها، یکبار در هر درخواست. */
|
/** @var array<int, string|null> نام بیمهها، یکبار در هر درخواست. */
|
||||||
@@ -107,10 +108,16 @@ class InvoiceService
|
|||||||
$visitCategory = $session->getInsuranceServiceCategory() ?? ServiceCategory::Outpatient;
|
$visitCategory = $session->getInsuranceServiceCategory() ?? ServiceCategory::Outpatient;
|
||||||
$invoice->setServiceCategory($session->getInsuranceServiceCategory());
|
$invoice->setServiceCategory($session->getInsuranceServiceCategory());
|
||||||
|
|
||||||
|
// قرارداد بیمه جای دیگری غیر از محیطِ صورتحساب نشسته است: صورتحسابِ نوبتِ
|
||||||
|
// کلینیک به کلینیک تعلق دارد، ولی قرارداد معمولاً روی خودِ پزشک ثبت شده. با
|
||||||
|
// محیط صورتحساب، هر سهم بیمه صفر درمیآمد — بیمار کل مبلغ را بدهکار میشد و
|
||||||
|
// مطالبهای هم برای بیمه ساخته نمیشد. {@see InsuranceScopeResolver}
|
||||||
|
[$coverageType, $coverageId] = $this->coverageScope($session, $entityType, $entityId);
|
||||||
|
|
||||||
$visitPrice = $session->getVisitPriceRials();
|
$visitPrice = $session->getVisitPriceRials();
|
||||||
if ($visitPrice > 0) {
|
if ($visitPrice > 0) {
|
||||||
$baseRule = $this->tenantInsuranceService->coverageRule($entityType, $entityId, $baseId, $visitCategory);
|
$baseRule = $this->tenantInsuranceService->coverageRule($coverageType, $coverageId, $baseId, $visitCategory);
|
||||||
$suppRule = $this->tenantInsuranceService->coverageRule($entityType, $entityId, $suppId, $visitCategory);
|
$suppRule = $this->tenantInsuranceService->coverageRule($coverageType, $coverageId, $suppId, $visitCategory);
|
||||||
$breakdown = $this->calculator->calculateItem(new Money($visitPrice), $baseRule, $suppRule);
|
$breakdown = $this->calculator->calculateItem(new Money($visitPrice), $baseRule, $suppRule);
|
||||||
$invoice->addItem(new InvoiceItem($invoice, 'ویزیت', $visitPrice, 1, $breakdown, null));
|
$invoice->addItem(new InvoiceItem($invoice, 'ویزیت', $visitPrice, 1, $breakdown, null));
|
||||||
}
|
}
|
||||||
@@ -122,8 +129,8 @@ class InvoiceService
|
|||||||
$unitPrice = $item->getPriceRials();
|
$unitPrice = $item->getPriceRials();
|
||||||
$total = new Money($unitPrice * $qty);
|
$total = new Money($unitPrice * $qty);
|
||||||
|
|
||||||
$baseRule = $this->tenantInsuranceService->coverageRuleForService($entityType, $entityId, $baseId, $item->getId());
|
$baseRule = $this->tenantInsuranceService->coverageRuleForService($coverageType, $coverageId, $baseId, $item->getId());
|
||||||
$suppRule = $this->tenantInsuranceService->coverageRuleForService($entityType, $entityId, $suppId, $item->getId());
|
$suppRule = $this->tenantInsuranceService->coverageRuleForService($coverageType, $coverageId, $suppId, $item->getId());
|
||||||
|
|
||||||
$breakdown = $this->calculator->calculateItem($total, $baseRule, $suppRule);
|
$breakdown = $this->calculator->calculateItem($total, $baseRule, $suppRule);
|
||||||
$invoice->addItem(new InvoiceItem($invoice, $item->getName(), $unitPrice, $qty, $breakdown, $item->getId()));
|
$invoice->addItem(new InvoiceItem($invoice, $item->getName(), $unitPrice, $qty, $breakdown, $item->getId()));
|
||||||
@@ -132,6 +139,27 @@ class InvoiceService
|
|||||||
$invoice->recalculateTotals();
|
$invoice->recalculateTotals();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* محیطی که قرارداد بیمهٔ این مراجعه در آن ثبت شده — اول پزشکِ نوبت، بعد کلینیکش.
|
||||||
|
*
|
||||||
|
* مراجعهٔ دستی نوبتی ندارد، پس پزشکی هم برای پرسیدن نیست و همان محیط صورتحساب
|
||||||
|
* میماند؛ آنجا قرارداد در همان محیط ثبت میشود.
|
||||||
|
*
|
||||||
|
* @return array{0: string, 1: int}
|
||||||
|
*/
|
||||||
|
private function coverageScope(PatientSession $session, string $entityType, int $entityId): array
|
||||||
|
{
|
||||||
|
$appointment = $session->getAppointment();
|
||||||
|
if ($appointment === null) {
|
||||||
|
return [$entityType, $entityId];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->insuranceScope->forContracts(
|
||||||
|
(int) $appointment->getDoctor()->getId(),
|
||||||
|
$appointment->getClinic()?->getId() !== null ? (int) $appointment->getClinic()->getId() : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** نهاییسازی، و اعلامش به مصرفکنندههای اثر جانبی (ساخت مطالبهٔ بیمه). */
|
/** نهاییسازی، و اعلامش به مصرفکنندههای اثر جانبی (ساخت مطالبهٔ بیمه). */
|
||||||
public function finalize(Invoice $invoice): void
|
public function finalize(Invoice $invoice): void
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ class BlogRepository extends ServiceEntityRepository
|
|||||||
* The admin review queue: posts awaiting a doctor's decision, newest first.
|
* The admin review queue: posts awaiting a doctor's decision, newest first.
|
||||||
* @return Blog[]
|
* @return Blog[]
|
||||||
*/
|
*/
|
||||||
|
/*
|
||||||
|
* هر چهار فهرست روی `id` هم مرتب میشوند، نه فقط `createdAt`.
|
||||||
|
*
|
||||||
|
* `created_at` ثانیهای است و دهها مقاله میتوانند دقیقاً یک مقدار داشته باشند؛
|
||||||
|
* ترتیب بین ردیفهای برابر در MySQL تضمینشده نیست. بدون این tiebreaker، دو صفحهٔ
|
||||||
|
* پیاپیِ همان فهرست میتوانند یک مقاله را دوبار بدهند و مقالهٔ دیگری را هرگز.
|
||||||
|
*/
|
||||||
public function findByReviewStatus(string $reviewStatus, int $page = 1, int $limit = 20): array
|
public function findByReviewStatus(string $reviewStatus, int $page = 1, int $limit = 20): array
|
||||||
{
|
{
|
||||||
return $this->createQueryBuilder('b')
|
return $this->createQueryBuilder('b')
|
||||||
@@ -61,6 +68,7 @@ class BlogRepository extends ServiceEntityRepository
|
|||||||
->where('b.reviewStatus = :rs')
|
->where('b.reviewStatus = :rs')
|
||||||
->setParameter('rs', $reviewStatus)
|
->setParameter('rs', $reviewStatus)
|
||||||
->orderBy('b.createdAt', 'DESC')
|
->orderBy('b.createdAt', 'DESC')
|
||||||
|
->addOrderBy('b.id', 'DESC')
|
||||||
->setFirstResult(($page - 1) * $limit)
|
->setFirstResult(($page - 1) * $limit)
|
||||||
->setMaxResults($limit)
|
->setMaxResults($limit)
|
||||||
->getQuery()->getResult();
|
->getQuery()->getResult();
|
||||||
@@ -86,6 +94,7 @@ class BlogRepository extends ServiceEntityRepository
|
|||||||
$qb = $this->adminQb($status, $search)
|
$qb = $this->adminQb($status, $search)
|
||||||
->leftJoin('b.city', 'c')->addSelect('c')
|
->leftJoin('b.city', 'c')->addSelect('c')
|
||||||
->orderBy('b.createdAt', 'DESC')
|
->orderBy('b.createdAt', 'DESC')
|
||||||
|
->addOrderBy('b.id', 'DESC')
|
||||||
->setFirstResult(($page - 1) * $limit)
|
->setFirstResult(($page - 1) * $limit)
|
||||||
->setMaxResults($limit);
|
->setMaxResults($limit);
|
||||||
return $qb->getQuery()->getResult();
|
return $qb->getQuery()->getResult();
|
||||||
@@ -120,6 +129,7 @@ class BlogRepository extends ServiceEntityRepository
|
|||||||
->where('b.representation = :rep')
|
->where('b.representation = :rep')
|
||||||
->setParameter('rep', $rep)
|
->setParameter('rep', $rep)
|
||||||
->orderBy('b.createdAt', 'DESC')
|
->orderBy('b.createdAt', 'DESC')
|
||||||
|
->addOrderBy('b.id', 'DESC')
|
||||||
->setFirstResult(($page - 1) * $limit)
|
->setFirstResult(($page - 1) * $limit)
|
||||||
->setMaxResults($limit);
|
->setMaxResults($limit);
|
||||||
if ($status !== null && $status !== '') {
|
if ($status !== null && $status !== '') {
|
||||||
@@ -166,6 +176,7 @@ class BlogRepository extends ServiceEntityRepository
|
|||||||
->where('b.status = :status')
|
->where('b.status = :status')
|
||||||
->setParameter('status', Blog::STATUS_PUBLISHED)
|
->setParameter('status', Blog::STATUS_PUBLISHED)
|
||||||
->orderBy('b.createdAt', 'DESC')
|
->orderBy('b.createdAt', 'DESC')
|
||||||
|
->addOrderBy('b.id', 'DESC')
|
||||||
->setFirstResult(($page - 1) * $limit)
|
->setFirstResult(($page - 1) * $limit)
|
||||||
->setMaxResults($limit);
|
->setMaxResults($limit);
|
||||||
|
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class ClinicController extends BaseController
|
|||||||
$this->userRepo->save($user);
|
$this->userRepo->save($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success(['data' => $clinic->toDetailArray()], 201);
|
return $this->success(['data' => $clinic->toDetailArray(withContact: true)], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[OA\Get(
|
#[OA\Get(
|
||||||
@@ -177,7 +177,7 @@ class ClinicController extends BaseController
|
|||||||
);
|
);
|
||||||
|
|
||||||
return $this->success(['data' => array_merge(
|
return $this->success(['data' => array_merge(
|
||||||
$clinic->toDetailArray($stateData, $cityData, $map, $street, $telephone),
|
$clinic->toDetailArray($stateData, $cityData, $map, $street, $telephone, $canEdit),
|
||||||
['can_edit' => $canEdit],
|
['can_edit' => $canEdit],
|
||||||
)]);
|
)]);
|
||||||
}
|
}
|
||||||
@@ -277,7 +277,7 @@ class ClinicController extends BaseController
|
|||||||
|
|
||||||
[$stateData, $cityData, $map, $street, $telephone] = $this->loadLocationData($clinic);
|
[$stateData, $cityData, $map, $street, $telephone] = $this->loadLocationData($clinic);
|
||||||
|
|
||||||
return $this->success(['data' => $clinic->toDetailArray($stateData, $cityData, $map, $street, $telephone)]);
|
return $this->success(['data' => $clinic->toDetailArray($stateData, $cityData, $map, $street, $telephone, true)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[OA\Get(
|
#[OA\Get(
|
||||||
@@ -795,7 +795,7 @@ class ClinicController extends BaseController
|
|||||||
$this->editLogger->logEdit($user, 'clinic', $clinic->getUuid(), $data);
|
$this->editLogger->logEdit($user, 'clinic', $clinic->getUuid(), $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success(['data' => $address->toArray()], 201);
|
return $this->success(['data' => $address->toArray(null, true)], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/api/v1/clinic/{clinicUuid}/address/{addressUuid}', methods: ['PATCH'])]
|
#[Route('/api/v1/clinic/{clinicUuid}/address/{addressUuid}', methods: ['PATCH'])]
|
||||||
@@ -824,7 +824,7 @@ class ClinicController extends BaseController
|
|||||||
$this->editLogger->logEdit($user, 'clinic', $clinic->getUuid(), $data);
|
$this->editLogger->logEdit($user, 'clinic', $clinic->getUuid(), $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success(['data' => $address->toArray()]);
|
return $this->success(['data' => $address->toArray(null, true)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/api/v1/clinic/{clinicUuid}/address/{addressUuid}', methods: ['DELETE'])]
|
#[Route('/api/v1/clinic/{clinicUuid}/address/{addressUuid}', methods: ['DELETE'])]
|
||||||
|
|||||||
@@ -209,9 +209,11 @@ class Clinic
|
|||||||
array $cityData = [],
|
array $cityData = [],
|
||||||
?array $map = null,
|
?array $map = null,
|
||||||
?string $street = null,
|
?string $street = null,
|
||||||
?string $telephone = null
|
?string $telephone = null,
|
||||||
|
bool $withContact = false
|
||||||
): array {
|
): array {
|
||||||
$phone = $this->firstFilled($telephone, $this->telephone);
|
// شمارهٔ کلینیک هم مثل شمارهٔ مطب عمومی نیست. {@see DoctorAddress::toArray()}
|
||||||
|
$phone = $withContact ? $this->firstFilled($telephone, $this->telephone) : null;
|
||||||
$address = $this->firstFilled($street, $this->address);
|
$address = $this->firstFilled($street, $this->address);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -267,9 +269,9 @@ class Clinic
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function toListArray(?string $city = null, ?string $state = null, ?string $telephone = null): array
|
public function toListArray(?string $city = null, ?string $state = null, ?string $telephone = null, bool $withContact = false): array
|
||||||
{
|
{
|
||||||
$phone = $this->firstFilled($telephone, $this->telephone);
|
$phone = $withContact ? $this->firstFilled($telephone, $this->telephone) : null;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (string) $this->id,
|
'id' => (string) $this->id,
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ class AddressController extends BaseController
|
|||||||
$this->clinicDoctorAccess->denyUnlessGranted($user, 'addresses', 'view');
|
$this->clinicDoctorAccess->denyUnlessGranted($user, 'addresses', 'view');
|
||||||
|
|
||||||
return $this->success(array_map(
|
return $this->success(array_map(
|
||||||
static fn (DoctorAddress $a): array => $a->toArray(),
|
// فهرست محیطِ خودِ کاربر است، پس شمارهٔ تماس هم برمیگردد.
|
||||||
|
static fn (DoctorAddress $a): array => $a->toArray(null, true),
|
||||||
$this->addresses->listForContext($user),
|
$this->addresses->listForContext($user),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ class DoctorController extends BaseController
|
|||||||
$this->userRepo->save($user);
|
$this->userRepo->save($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success(['data' => $doctor->toDetailArray($this->scheduleRepo->findAllByDoctor($doctor))], 201);
|
return $this->success(['data' => $doctor->toDetailArray($this->scheduleRepo->findAllByDoctor($doctor), true)], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[OA\Get(
|
#[OA\Get(
|
||||||
@@ -159,12 +159,14 @@ class DoctorController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$clinics = $this->clinicRepo->findByDoctor($doctor);
|
$clinics = $this->clinicRepo->findByDoctor($doctor);
|
||||||
|
// شمارهٔ کلینیک بعد از محاسبهٔ can_edit اضافه میشود؛ اینجا هنوز معلوم نیست
|
||||||
|
// درخواستدهنده صاحب پروفایل است یا یک بازدیدکنندهٔ عمومی.
|
||||||
$clinicData = array_map(fn(Clinic $c) => [
|
$clinicData = array_map(fn(Clinic $c) => [
|
||||||
'id' => (string) $c->getId(),
|
'id' => (string) $c->getId(),
|
||||||
'uuid' => $c->getUuid(),
|
'uuid' => $c->getUuid(),
|
||||||
'name' => $c->getName(),
|
'name' => $c->getName(),
|
||||||
'address' => $c->getAddress(),
|
'address' => $c->getAddress(),
|
||||||
'telephone' => $c->getTelephone(),
|
'telephone' => null,
|
||||||
'city_id' => $c->getCityId(),
|
'city_id' => $c->getCityId(),
|
||||||
'province_id' => $c->getProvinceId(),
|
'province_id' => $c->getProvinceId(),
|
||||||
'map' => [
|
'map' => [
|
||||||
@@ -187,8 +189,14 @@ class DoctorController extends BaseController
|
|||||||
|| $this->editPolicy->ownsDoctor($user, $doctor)
|
|| $this->editPolicy->ownsDoctor($user, $doctor)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ($canEdit) {
|
||||||
|
foreach ($clinics as $index => $clinic) {
|
||||||
|
$clinicData[$index]['telephone'] = $clinic->getTelephone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$schedules = $this->scheduleRepo->findAllByDoctor($doctor);
|
$schedules = $this->scheduleRepo->findAllByDoctor($doctor);
|
||||||
return $this->success(['data' => array_merge($doctor->toDetailArray($schedules), [
|
return $this->success(['data' => array_merge($doctor->toDetailArray($schedules, $canEdit), [
|
||||||
'clinics' => $clinicData,
|
'clinics' => $clinicData,
|
||||||
'representation' => $representation,
|
'representation' => $representation,
|
||||||
'can_edit' => $canEdit,
|
'can_edit' => $canEdit,
|
||||||
@@ -214,7 +222,7 @@ class DoctorController extends BaseController
|
|||||||
}
|
}
|
||||||
|
|
||||||
$schedules = $this->scheduleRepo->findAllByDoctor($doctor);
|
$schedules = $this->scheduleRepo->findAllByDoctor($doctor);
|
||||||
return $this->success(['data' => array_merge($doctor->toDetailArray($schedules), ['clinics' => [[
|
return $this->success(['data' => array_merge($doctor->toDetailArray($schedules, true), ['clinics' => [[
|
||||||
'id' => (string) $clinic->getId(),
|
'id' => (string) $clinic->getId(),
|
||||||
'uuid' => $clinic->getUuid(),
|
'uuid' => $clinic->getUuid(),
|
||||||
'name' => $clinic->getName(),
|
'name' => $clinic->getName(),
|
||||||
@@ -393,7 +401,7 @@ class DoctorController extends BaseController
|
|||||||
$this->editLogger->logEdit($user, 'doctor', $doctor->getUuid(), $data);
|
$this->editLogger->logEdit($user, 'doctor', $doctor->getUuid(), $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success(['data' => $doctor->toDetailArray($this->scheduleRepo->findAllByDoctor($doctor))]);
|
return $this->success(['data' => $doctor->toDetailArray($this->scheduleRepo->findAllByDoctor($doctor), true)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[OA\Delete(
|
#[OA\Delete(
|
||||||
@@ -613,7 +621,7 @@ class DoctorController extends BaseController
|
|||||||
$this->editLogger->logEdit($user, 'doctor', $doctor->getUuid(), $data);
|
$this->editLogger->logEdit($user, 'doctor', $doctor->getUuid(), $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success(['data' => $address->toArray()], 201);
|
return $this->success(['data' => $address->toArray(null, true)], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[OA\Get(
|
#[OA\Get(
|
||||||
@@ -651,7 +659,7 @@ class DoctorController extends BaseController
|
|||||||
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
return $this->error(ErrorCodes::ERR_AUTH_006, 'دسترسی ممنوع', 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success(['data' => $address->toArray()]);
|
return $this->success(['data' => $address->toArray(null, true)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[OA\Patch(
|
#[OA\Patch(
|
||||||
@@ -715,7 +723,7 @@ class DoctorController extends BaseController
|
|||||||
$this->editLogger->logEdit($user, 'doctor', $doctor->getUuid(), $data);
|
$this->editLogger->logEdit($user, 'doctor', $doctor->getUuid(), $data);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->success(['data' => $address->toArray()]);
|
return $this->success(['data' => $address->toArray(null, true)]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[OA\Delete(
|
#[OA\Delete(
|
||||||
|
|||||||
@@ -601,7 +601,11 @@ class Doctor
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @param WeeklySchedule[] $schedules همهٔ برنامههای پزشک (شخصی + کلینیکها) */
|
/** @param WeeklySchedule[] $schedules همهٔ برنامههای پزشک (شخصی + کلینیکها) */
|
||||||
public function toDetailArray(array $schedules = []): array
|
/**
|
||||||
|
* `$withContact` فقط برای مخاطبی روشن میشود که حق دیدن شمارهٔ مطب را دارد —
|
||||||
|
* صاحب پروفایل یا ادمین. پاسخ عمومی بدون شماره است. {@see DoctorAddress::toArray()}
|
||||||
|
*/
|
||||||
|
public function toDetailArray(array $schedules = [], bool $withContact = false): array
|
||||||
{
|
{
|
||||||
$sf = $this->computeScheduleFields($schedules);
|
$sf = $this->computeScheduleFields($schedules);
|
||||||
return [
|
return [
|
||||||
@@ -638,7 +642,7 @@ class Doctor
|
|||||||
'owner_status' => $this->ownerStatus,
|
'owner_status' => $this->ownerStatus,
|
||||||
'free_turn' => $sf['free_turn'],
|
'free_turn' => $sf['free_turn'],
|
||||||
'hours_of_work' => $sf['hours_of_work'],
|
'hours_of_work' => $sf['hours_of_work'],
|
||||||
'address' => array_map(fn(DoctorAddress $a) => $a->toArray(), $this->addresses->toArray()),
|
'address' => array_map(fn(DoctorAddress $a) => $a->toArray(null, $withContact), $this->addresses->toArray()),
|
||||||
'average_rate' => ['total_rates' => null],
|
'average_rate' => ['total_rates' => null],
|
||||||
'state' => array_map(fn(Province $p) => [
|
'state' => array_map(fn(Province $p) => [
|
||||||
'uuid' => $p->getUuid(),
|
'uuid' => $p->getUuid(),
|
||||||
|
|||||||
@@ -151,7 +151,16 @@ class DoctorAddress
|
|||||||
|
|
||||||
private function touch(): void { $this->updatedAt = time(); }
|
private function touch(): void { $this->updatedAt = time(); }
|
||||||
|
|
||||||
public function toArray(?string $clinicName = null): array
|
/**
|
||||||
|
* شمارهٔ تماسِ محل، دادهٔ عمومی نیست.
|
||||||
|
*
|
||||||
|
* صفحهٔ پزشک شمارهٔ مطب را کنار آدرس نشان میداد و همان شماره در پاسخ عمومیِ API
|
||||||
|
* هم میآمد — یعنی برداشت انبوهِ شمارهٔ همهٔ مطبها فقط یک درخواست فاصله داشت.
|
||||||
|
* حالا پیشفرض حذف است و فقط جایی که مخاطب حق دیدنش را دارد — صاحب همان محل، یا
|
||||||
|
* بیماری که نوبتش آنجاست — با `$withContact` برمیگردد. موقعیت مکانی و آدرس
|
||||||
|
* عمومی میمانند؛ آنها همان چیزیاند که بیمار برای پیدا کردن مطب لازم دارد.
|
||||||
|
*/
|
||||||
|
public function toArray(?string $clinicName = null, bool $withContact = false): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'id' => (string) $this->id,
|
'id' => (string) $this->id,
|
||||||
@@ -165,7 +174,7 @@ class DoctorAddress
|
|||||||
'longitude' => $this->longitude !== null ? (string) $this->longitude : null,
|
'longitude' => $this->longitude !== null ? (string) $this->longitude : null,
|
||||||
],
|
],
|
||||||
'address' => $this->address,
|
'address' => $this->address,
|
||||||
'telephone' => $this->telephone,
|
'telephone' => $withContact ? $this->telephone : null,
|
||||||
'active' => $this->active,
|
'active' => $this->active,
|
||||||
'timezone' => $this->timezone,
|
'timezone' => $this->timezone,
|
||||||
'city' => $this->city !== null ? [
|
'city' => $this->city !== null ? [
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class InsuranceController extends BaseController
|
|||||||
private readonly DoctorInsuranceRepository $doctorInsuranceRepo,
|
private readonly DoctorInsuranceRepository $doctorInsuranceRepo,
|
||||||
private readonly DoctorRepository $doctorRepo,
|
private readonly DoctorRepository $doctorRepo,
|
||||||
private readonly ClinicRepository $clinicRepo,
|
private readonly ClinicRepository $clinicRepo,
|
||||||
|
private readonly \App\Insurance\Service\InsuranceScopeResolver $insuranceScope,
|
||||||
private readonly EntityInsurancePricingRepository $pricingRepo,
|
private readonly EntityInsurancePricingRepository $pricingRepo,
|
||||||
private readonly TenantInsuranceRepository $tenantInsuranceRepo,
|
private readonly TenantInsuranceRepository $tenantInsuranceRepo,
|
||||||
private readonly TenantServiceCoverageRepository $serviceCoverageRepo,
|
private readonly TenantServiceCoverageRepository $serviceCoverageRepo,
|
||||||
@@ -317,9 +318,53 @@ class InsuranceController extends BaseController
|
|||||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// `inherit=1` را فقط مصرفکنندههایی میفرستند که میخواهند بدانند «برای نوبتِ
|
||||||
|
// این پزشک واقعاً چه چیزی اعمال میشود» — مثل مودال قطعیکردن نوبت. صفحهٔ
|
||||||
|
// تنظیمات آن را نمیفرستد، چون آنجا باید ردیفِ خودِ پزشک ویرایش شود، نه
|
||||||
|
// ردیفِ بهارثرسیده از کلینیک.
|
||||||
|
if ($request->query->getBoolean('inherit')) {
|
||||||
|
[$entityType, $entityId] = $this->inheritedScope($entityType, $entityId, $user, 'visit_price');
|
||||||
|
}
|
||||||
|
|
||||||
return $this->success($this->pricingPayload($entityType, $entityId));
|
return $this->success($this->pricingPayload($entityType, $entityId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* محیطی که تنظیم واقعاً از آن خوانده میشود: اول خودِ پزشک، در نبودِ تنظیمِ او
|
||||||
|
* کلینیکی که عضوش است. همان قاعدهای که موتور نوبت بهکار میبرد، پس پنل و
|
||||||
|
* محاسبهٔ نهایی یک چیز نشان میدهند. {@see InsuranceScopeResolver}
|
||||||
|
*
|
||||||
|
* @return array{0: string, 1: int}
|
||||||
|
*/
|
||||||
|
private function inheritedScope(string $entityType, int $entityId, User $user, string $kind): array
|
||||||
|
{
|
||||||
|
if ($entityType !== 'doctor') {
|
||||||
|
return [$entityType, $entityId];
|
||||||
|
}
|
||||||
|
|
||||||
|
$doctor = $this->doctorRepo->find($entityId);
|
||||||
|
if ($doctor === null) {
|
||||||
|
return [$entityType, $entityId];
|
||||||
|
}
|
||||||
|
|
||||||
|
// فقط کلینیکی که کاربر واقعاً در آن ایستاده — نه «هر کلینیکی که پزشک عضوش
|
||||||
|
// است»: پزشکِ چندکلینیکه وگرنه تنظیم کلینیکِ نامربوط را میدید.
|
||||||
|
[$callerType, $callerId] = $this->resolveEntity($user);
|
||||||
|
if ($callerType !== 'clinic' || $callerId === null) {
|
||||||
|
return [$entityType, $entityId];
|
||||||
|
}
|
||||||
|
|
||||||
|
$clinic = $this->clinicRepo->find((int) $callerId);
|
||||||
|
if ($clinic === null || !$clinic->getDoctors()->contains($doctor)) {
|
||||||
|
return [$entityType, $entityId];
|
||||||
|
}
|
||||||
|
|
||||||
|
return match ($kind) {
|
||||||
|
'visit_price' => $this->insuranceScope->forVisitPrice($entityId, (int) $callerId),
|
||||||
|
default => $this->insuranceScope->forContracts($entityId, (int) $callerId),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private function pricingPayload(string $entityType, int $entityId): array
|
private function pricingPayload(string $entityType, int $entityId): array
|
||||||
{
|
{
|
||||||
$rows = $this->pricingRepo->findByEntity($entityType, $entityId);
|
$rows = $this->pricingRepo->findByEntity($entityType, $entityId);
|
||||||
@@ -453,6 +498,10 @@ class InsuranceController extends BaseController
|
|||||||
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
return $this->error(ErrorCodes::ERR_FORBIDDEN_001, 'پروفایل یافت نشد', 403);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($request->query->getBoolean('inherit')) {
|
||||||
|
[$entityType, $entityId] = $this->inheritedScope($entityType, $entityId, $user, 'contracts');
|
||||||
|
}
|
||||||
|
|
||||||
$contracts = $this->tenantInsuranceRepo->findLatestByTenant($entityType, $entityId);
|
$contracts = $this->tenantInsuranceRepo->findLatestByTenant($entityType, $entityId);
|
||||||
|
|
||||||
$byId = [];
|
$byId = [];
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user