feat: implement per-doctor insurance settings in multi-doctor clinics

- Updated InsuranceModal to include doctorUuid in the payload for insurance contracts.
- Enhanced TenantInsuranceContracts to allow selection of doctors and pass doctorUuid in API requests.
- Modified InsuranceController to handle doctorUuid for tenant insurance endpoints, ensuring contracts are stored per doctor.
- Updated API documentation to reflect the new optional doctor_uuid parameter for tenant insurance endpoints.
- Added tests to verify the functionality of per-doctor insurance contracts and ensure isolation of contracts between doctors.
This commit is contained in:
hamed
2026-07-21 19:21:55 +03:30
parent 7e847b62c4
commit 5507b42fd8
6 changed files with 478 additions and 25 deletions
+13 -2
View File
@@ -371,9 +371,13 @@ entity جاری از `#[CurrentUser]` resolve می‌شود: نقش `ROLE_DOCTOR
tenant از `#[CurrentUser]` با `App\Patient\Security\PatientRecordScopeResolver` resolve می‌شود — همان رزولور پرونده‌ها و صورتحساب‌ها، تا قرارداد بیمه و صورتحسابی که از آن ساخته می‌شود هرگز به دو محیط متفاوت نیفتند. محیط فعال (`UserActiveContext`) تعیین‌کننده است، نه صرفاً ترتیب نقش‌ها؛ مالک کلینیکی که خودش پزشک هم هست، قراردادهای **کلینیک** خود را می‌بیند.
**تنظیمات 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`
لیست قراردادهای tenant جاری — **آخرین نسخهٔ هر بیمه، فعال یا غیرفعال** (برای toggle فعال/غیرفعال در UI مدیریت بیمه). `insurance_kind` = `kind` قرارداد در صورت تعیین، وگرنه نوع بیمه از کاتالوگ.
**Query:** `doctor_uuid` (اختیاری) — قراردادهای همان پزشک را برمی‌گرداند (نگاه کنید به «تنظیمات per-doctor» بالا).
**Permission:** `AUTH` (doctor/clinic)
```json
@@ -413,6 +417,7 @@ tenant از `#[CurrentUser]` با `App\Patient\Security\PatientRecordScopeResolv
| `kind` | string \| null | نوع بیمه قرارداد (`basic`/`supplementary`); خالی → پیش‌فرض نوع کاتالوگ |
| `effective_from` | int \| null | تاریخ شروع قرارداد (Unix)؛ null → اکنون |
| `effective_to` | int \| null | تاریخ پایان قرارداد (Unix)؛ null → نامحدود |
| `doctor_uuid` | string (UUID) \| null | اختیاری — قرارداد را به‌ازای پزشک هدف ذخیره می‌کند (نگاه کنید به «تنظیمات per-doctor» بالا) |
پاسخ `201`: `{ success, data: { …contract } }`.
خطاها: `404 ERR_NOT_FOUND_001` بیمه یافت نشد · `422 ERR_VALIDATION_001` insurance_id الزامی · `403 ERR_FORBIDDEN_001` پروفایل یافت نشد.
@@ -420,13 +425,16 @@ tenant از `#[CurrentUser]` با `App\Patient\Security\PatientRecordScopeResolv
### PATCH `/api/v1/billing/tenant-insurances/{uuid}`
ویرایش فیلدهای قرارداد (همه اختیاری، فقط کلیدهای موجود اعمال می‌شوند). فقط قرارداد متعلق به tenant جاری.
**Body:** `coverage_percent` · `franchise_rials` · `annual_ceiling_rials` · `kind` · `effective_from` · `effective_to` · `is_active`.
**Body:** `coverage_percent` · `franchise_rials` · `annual_ceiling_rials` · `kind` · `effective_from` · `effective_to` · `is_active` · `doctor_uuid` (اختیاری، برای هدف‌گیری پزشک — نگاه کنید به «تنظیمات per-doctor» بالا).
- `is_active` (bool): toggle فعال/غیرفعال. برخلاف `DELETE`، مقدار `effective_to`ِ تعیین‌شدهٔ کاربر را دست‌نخورده نگه می‌دارد (برای reactivate).
- قرارداد باید به همان موجودیتِ resolve‌شده (پزشک هدف یا tenant کاربر) تعلق داشته باشد، وگرنه `404`.
### DELETE `/api/v1/billing/tenant-insurances/{uuid}`
غیرفعال‌سازی نرم (soft) — `is_active=false` و `effective_to=now`. داده حذف نمی‌شود.
**Query:** `doctor_uuid` (اختیاری) — برای غیرفعال‌سازی قرارداد یک پزشک خاص.
```json
{ "success": true, "data": { "message": "قرارداد بیمه غیرفعال شد" } }
```
@@ -442,6 +450,8 @@ override پوشش یک خدمت خاص تحت قرارداد یک بیمه. فی
### GET `/api/v1/billing/tenant-insurances/{uuid}/service-coverage`
لیست overrideهای پوشش خدمات یک قرارداد.
**Query:** `doctor_uuid` (اختیاری) — برای قراردادِ متعلق به پزشک هدف در کلینیک چندپزشکه.
**Permission:** `AUTH` (مالک قرارداد)
```json
@@ -476,8 +486,9 @@ override پوشش یک خدمت خاص تحت قرارداد یک بیمه. فی
| `coverage_percent` | float \| null | null = ارث از قرارداد |
| `franchise_rials` | int \| null | null = ارث از قرارداد |
| `ceiling_rials` | int \| null | null = ارث از قرارداد |
| `doctor_uuid` | string (UUID) \| null | اختیاری — قراردادِ متعلق به پزشک هدف (نگاه کنید به «تنظیمات per-doctor» بالا) |
سرویس باید متعلق به همان مطب/کلینیکِ قرارداد باشد (`ServiceItem→section→entity_type/entity_id`).
سرویس باید متعلق به همان مطب/کلینیکِ قرارداد باشد (`ServiceItem→section→entity_type/entity_id`). با `doctor_uuid`، موجودیت هدف پزشک است، پس سرویس هم باید متعلق به همان پزشک باشد.
**اثر جانبی — همگام‌سازی `ServiceItem.insurance_covered`:** پس از ذخیره‌ی ردیف پوشش، پرچم
`insurance_covered` همان خدمت بازمحاسبه می‌شود: اگر زیر **هر** قرارداد بیمه‌ای دست‌کم یک ردیف با