Base insurance is a percentage-only rule: patient share is now total minus the base share, and the contract franchise no longer inflates it (franchise stays meaningful for supplementary contracts only). Coverage percentages are managed centrally by admin per service category (outpatient/inpatient, extensible via the ServiceCategory enum). A tenant contract may override a category, otherwise it follows the admin default live — changing the central value immediately applies to every contract that did not override it. - add ServiceCategory enum + GET /api/v1/service-categories as the single source of the category list for every client - add insurance_coverage_defaults (+ GET/PUT admin coverage-defaults endpoints) and expose coverage_defaults on the insurance list and insurance-pricing - add tenant_insurance_category_coverage; tenant-insurances accepts optional category_coverages (needs insurances.update) and returns the effective percentages with their source - add service_items.service_category; visits always resolve as outpatient - drop the reverse-engineered percent from patient_share_rials in MyPatientsPage and align the client-side BillingCalculator mirror in CreateStep Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
25 KiB
اصلاح محاسبه بیمه پایه بر پایه «درصد پوشش» + تنظیمات مرکزی درصدها در پنل ادمین
پروژه
clinicpro (بکاند Symfony + پنل React ادمین/پزشک). دامنهٔ اصلی: src/Insurance/، src/Billing/، src/Patient/، assets/admin/.
سایت عمومی nobat724_front هیچجا insurance-pricing یا patient_share_rials را مصرف نمیکند (بررسی شد) → پرامپت همتا لازم نیست.
زمینه
محاسبهٔ سهم بیمهٔ پایه در سیستم بر پایهٔ ترکیبی از «فرانشیز ریالی» و «سهم بیمار ثابت» انجام میشود و در نتیجه:
BillingCalculatorفرانشیز ریالی قرارداد بیمهٔ پایه را به سهم بیمار اضافه میکند؛ در حالیکه قاعدهٔ درست بیمهٔ پایه فقط درصدی است.- در
MyPatientsPageدرصد پوشش از روی «سهم بیمار ثابت» (EntityInsurancePricing.patient_share_rials) معکوسسازی میشود (shareToDiscountPercent) — یعنی درصد واقعی قرارداد نادیده گرفته میشود. - هیچ منبع مرکزی برای درصد پوشش بیمههای پایه وجود ندارد؛ هر پزشک/کلینیک درصد را دستی وارد میکند و بین tenantها اختلاف ایجاد میشود.
- درصد پوشش، تکمقداری است؛ تفکیک «خدمات سرپایی / بستری» (و انواع آیندهٔ خدمت) وجود ندارد.
هدف
الف) قاعدهٔ محاسبه در کل سیستم:
سهم بیمهٔ پایه = round(مبلغ کل × درصد پوشش پایه ÷ 100)
سهم بیمار = مبلغ کل − سهم بیمهٔ پایه (فرانشیز در بیمهٔ پایه دخالت ندارد)
مثال مرجع کاربر (مبالغ نمایش تومان، ذخیره ریال — ۱ تومان = ۱۰ ریال):
| مورد | تومان | ریال |
|---|---|---|
| هزینه ویزیت | 595,200 | 5,952,000 |
| درصد پوشش پایه (بستری) | ۳۰٪ | — |
| سهم بیمهٔ پایه | 178,560 | 1,785,600 |
| سهم بیمار | 416,640 | 4,166,400 |
ب) درصدهای پوشش هر بیمهٔ پایه بهصورت مرکزی در پنل ادمین اصلی (نقش ROLE_ADMIN) تعریف شوند: حداقل «سرپایی» و «بستری»، با ساختار توسعهپذیر برای انواع بعدی.
ج) هنگام ایجاد/ویرایش قرارداد بیمه توسط پزشک یا کلینیک، این درصدها پیشفرض بارگذاری شوند؛ پزشک در حالت عادی چیزی وارد نکند و فقط در صورت داشتن مجوز بتواند برای همان قرارداد override کند.
فایلهای مرتبط
| فایل | نقش |
|---|---|
src/Billing/Service/BillingCalculator.php |
محاسبهٔ سهم پایه/تکمیلی/بیمار — نقطهٔ مرکزی باگ فرانشیز |
src/Insurance/ValueObject/CoverageRule.php |
VO قاعدهٔ پوشش (coveragePercent, franchiseRials, ceilingRials) |
src/Insurance/Service/TenantInsuranceService.php |
ساخت CoverageRule از قرارداد tenant + override خدمت |
src/Insurance/Entity/Insurance.php |
کاتالوگ بیمه (ادمین) — محل اتصال درصدهای پیشفرض |
src/Insurance/Entity/TenantInsurance.php |
قرارداد بیمهٔ پزشک/کلینیک (coverage_percent, franchise_rials) |
src/Insurance/Entity/TenantServiceCoverage.php |
override پوشش در سطح خدمت |
src/Insurance/Entity/EntityInsurancePricing.php |
«سهم بیمار ثابت» هر بیمه (مدل قدیمی) |
src/Insurance/Controller/InsuranceController.php |
همهٔ اندپوینتهای بیمه (ادمین + tenant + pricing) |
src/Insurance/Enum/InsuranceType.php |
basic / supplementary |
src/ClinicService/Entity/ServiceItem.php |
خدمت — فاقد نوع خدمت (سرپایی/بستری) |
src/Patient/Service/PatientService.php |
calculateFinalPrice() + snapshot درصدها روی مراجعه |
src/Billing/Service/InvoiceService.php |
صدور فاکتور از مراجعه با همان CoverageRule |
assets/admin/components/InsuranceModal.tsx |
فرم افزودن/ویرایش قرارداد بیمه (پنل پزشک) |
assets/admin/components/TenantInsuranceContracts.tsx |
لیست/کارت قراردادهای بیمهٔ tenant |
assets/admin/components/ServiceInsuranceModal.tsx |
override پوشش یک خدمت |
assets/admin/components/session/CreateStep.tsx |
آینهٔ سمتکلاینت BillingCalculator (patientShareOf) |
assets/admin/pages/MyPatientsPage.tsx |
shareToDiscountPercent — منبع محاسبهٔ اشتباه |
assets/admin/pages/CategoriesPage.tsx |
تب «بیمهها» در پنل ادمین اصلی (CRUD کاتالوگ بیمه) |
docs/api/insurance.md, docs/api/billing.md, docs/api/patient.md |
مستندات API |
وضعیت فعلی
۱) فرانشیز به سهم بیمار اضافه میشود — src/Billing/Service/BillingCalculator.php:15-52
if ($base !== null && $base->covered) {
$baseShare = $total->percent($base->coveragePercent);
if ($base->ceilingRials !== null) {
$baseShare = $baseShare->min(new Money($base->ceilingRials));
}
$remaining = $total->sub($baseShare);
}
// ...
// فرانشیز سهم بیمار است؛ از سهم بیمه کم نمیکند ولی سهم بیمار از کل بیشتر نمیشود.
$franchise = new Money(
($base?->franchiseRials ?? 0) + ($supplementary?->franchiseRials ?? 0)
);
$patient = $remaining->add($franchise)->min($total);
۲) درصد پوشش از «سهم بیمار ثابت» معکوس میشود — assets/admin/pages/MyPatientsPage.tsx:397-421
// درصد تخفیف معادلِ سهم بیمار بر اساس قیمت آزاد. share=null یعنی پوشش ندارد (۰٪).
const shareToDiscountPercent = (insuranceId: string): number => {
if (!insuranceId || freeVisitPrice <= 0) return 0;
const ins = pricing?.insurances.find((i) => String(i.insurance_id) === insuranceId);
if (!ins || ins.patient_share_rials == null) return 0;
const covered = Math.max(0, freeVisitPrice - ins.patient_share_rials);
return Math.round((covered / freeVisitPrice) * 1000) / 10;
};
const applyBaseInsurance = (insuranceId: string) => {
// ...
form.setValue("base_insurance_discount_percent", shareToDiscountPercent(insuranceId));
};
۳) قرارداد فقط یک درصد دارد و پزشک باید دستی وارد کند — assets/admin/components/InsuranceModal.tsx:169-182
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 12 }}>
<div style={field}>
<label style={label}>درصد پوشش</label>
<input ... value={form.coverage} onChange={(e) => set({ coverage: digitsOnly(e.target.value, 3) })} />
</div>
<div style={field}>
<label style={label}>فرانشیز (تومان)</label>
<input ... value={form.franchise} onChange={(e) => set({ franchise: digitsOnly(e.target.value) })} />
</div>
<div style={field}>
<label style={label}>سقف تعهد (تومان)</label>
...
</div>
</div>
۴) قاعدهٔ پوشش، بیخبر از نوع خدمت — src/Insurance/Service/TenantInsuranceService.php:111-138
return new CoverageRule(
coveragePercent: $override?->getCoveragePercent() ?? $contract->getCoveragePercent(),
franchiseRials: $override?->getFranchiseRials() ?? $contract->getFranchiseRials(),
ceilingRials: $override?->getCeilingRials() ?? $contract->getAnnualCeilingRials(),
covered: true,
);
ServiceItem هیچ فیلدی برای «سرپایی/بستری» ندارد (فیلدها: name, price_rials, active, insurance_covered, insurance_price_rials, duration_minutes, bookable, inventory_package_id).
وظایف
ترتیب پیشنهادی: ۱ → ۹ (بکاند اول، سپس فرانت، سپس مستندات/تست). هر گام مستقل قابل تست باشد.
۱. Enum نوع خدمت (توسعهپذیر)
فایل جدید src/Insurance/Enum/ServiceCategory.php:
namespace App\Insurance\Enum;
enum ServiceCategory: string
{
case Outpatient = 'outpatient'; // سرپایی
case Inpatient = 'inpatient'; // بستری
public function label(): string
{
return match ($this) {
self::Outpatient => 'خدمات سرپایی',
self::Inpatient => 'خدمات بستری',
};
}
/** @return list<string> */
public static function values(): array
{
return array_map(static fn(self $c) => $c->value, self::cases());
}
}
افزودن نوع جدید در آینده = فقط یک case تازه؛ هیچ جای دیگری نباید لیست ثابت hardcode شود (نه در Entity، نه در Controller، نه در فرانت — فرانت لیست را از API میگیرد).
۲. جدول درصدهای پیشفرض ادمین
Entity جدید src/Insurance/Entity/InsuranceCoverageDefault.php + src/Insurance/Repository/InsuranceCoverageDefaultRepository.php:
#[ORM\Entity(repositoryClass: InsuranceCoverageDefaultRepository::class)]
#[ORM\Table(name: 'insurance_coverage_defaults')]
#[ORM\UniqueConstraint(name: 'uniq_insurance_service_category', columns: ['insurance_id', 'service_category'])]
class InsuranceCoverageDefault
{
#[ORM\Id] #[ORM\GeneratedValue] #[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(name: 'insurance_id', type: 'integer')]
private int $insuranceId;
#[ORM\Column(name: 'service_category', type: 'string', length: 30, enumType: ServiceCategory::class)]
private ServiceCategory $serviceCategory;
#[ORM\Column(name: 'coverage_percent', type: 'decimal', precision: 5, scale: 2)]
private string $coveragePercent = '0.00';
#[ORM\Column(name: 'updated_at', type: 'integer')]
private int $updatedAt;
// getters/setters + toArray() طبق الگوی سایر Entityهای Insurance
}
متد ریپازیتوری لازم:
/** @return array<string, float> service_category => percent */
public function percentMapFor(int $insuranceId): array;
/** @return array<int, array<string, float>> insurance_id => (category => percent) — برای پرکردن لیستها بدون N+1 */
public function percentMapForMany(array $insuranceIds): array;
سپس:
ddev exec php bin/console doctrine:migrations:diff --no-interaction
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
در همان migration، backfill: برای هر بیمهٔ basic یک ردیف outpatient و یک inpatient با مقدار ۰ ساخته شود تا پنل ادمین همیشه ردیف کامل نشان دهد (خالیبودن = «تعریفنشده» نه «صفرِ عمدی»).
۳. اندپوینتهای ادمین برای درصدهای پیشفرض
در src/Insurance/Controller/InsuranceController.php (کنار /api/v1/admin/insurance/...، همان #[IsGranted('ROLE_ADMIN')] که بقیهٔ اکشنهای ادمین دارند):
#[Route('/api/v1/admin/insurance/{id}/coverage-defaults', methods: ['GET'])]
#[IsGranted('ROLE_ADMIN')]
public function getCoverageDefaults(int $id): JsonResponse
{
// { categories: [ { key, label, coverage_percent } ] } — همیشه همهٔ caseهای ServiceCategory
}
#[Route('/api/v1/admin/insurance/{id}/coverage-defaults', methods: ['PUT'])]
#[IsGranted('ROLE_ADMIN')]
public function saveCoverageDefaults(int $id, Request $request): JsonResponse
{
// body: { categories: [ { key: 'outpatient', coverage_percent: 30 }, ... ] }
// اعتبارسنجی: key ∈ ServiceCategory::values() و 0 ≤ percent ≤ 100 → در غیر این صورت
// $this->error(ErrorCodes::ERR_VALIDATION_001, ..., 422, 'coverage_percent')
}
- در پاسخ
GET /api/v1/admin/insurances(لیست ادمین) همcoverage_defaultsهر بیمه ضمیمه شود (باpercentMapForMany، بدون N+1). - در
GET /api/v1/insurancesو درpricingPayload()(خروجیGET /api/v1/insurance-pricing) هم برای هر بیمه کلیدcoverage_defaults: { outpatient: 30, inpatient: 0 }اضافه شود — پنل پزشک از همین برای پیشفرض استفاده میکند.
۴. درصدهای قرارداد در سطح نوع خدمت
Entity جدید src/Insurance/Entity/TenantInsuranceCategoryCoverage.php (tenant_insurance_category_coverage): tenant_insurance_id, service_category, coverage_percent, updated_at با UniqueConstraint روی دو ستون اول.
- در
POST/PATCH /api/v1/billing/tenant-insurancesبدنه یک آرایهٔ اختیاری بگیرد:{ "insurance_id": 3, "kind": "basic", "category_coverages": [ { "key": "outpatient", "coverage_percent": 70 }, { "key": "inpatient", "coverage_percent": 30 } ] } - اگر
category_coveragesارسال نشد → هیچ ردیفی ساخته نشود؛ محاسبه به پیشفرض ادمین برمیگردد (fallback زنده، نه کپی). این مهم است: با تغییر قوانین بیمه در پنل ادمین، قراردادهایی که override نکردهاند خودبهخود بهروز میشوند. GET /api/v1/billing/tenant-insurancesبرای هر قرارداد برگرداند:تا فرانت بتواند نشان دهد کدام مقدار از تنظیمات مرکزی آمده است.{ "coverage_percent": 70, "category_coverages": { "outpatient": 70, "inpatient": 30 }, "category_coverage_source": { "outpatient": "override", "inpatient": "admin_default" } }- مجوز override: همان
insurances.updateکه در کنترلر باsecretaryAccess/clinicDoctorAccessچک میشود؛ اگر کاربر مجوز ندارد وcategory_coveragesفرستاده،ERR_FORBIDDEN_001برگردد. TenantInsuranceCleanupService(حذف قرارداد) باید ردیفهای این جدول را هم پاک کند.
۵. نوع خدمت روی ServiceItem
- ستون جدید
service_categoryرویsrc/ClinicService/Entity/ServiceItem.phpباenumType: ServiceCategory::classو پیشفرضoutpatient؛ درtoArray()هم برگردد. - ویزیت (که آیتم سرویس نیست) همیشه
outpatientاست؛ این را بهصورت ثابت درPatientService::calculateFinalPrice()وInvoiceService::createFromSession()اعمال کن، نه با مقدار جادویی پراکنده. - در
ServiceItemFormModal.tsxیکSearchableSelect(طبق قاعدهٔ پروژه: هرگز<select>بومی) برای «نوع خدمت» با گزینههای گرفتهشده از API اضافه شود. - migration + backfill: همهٔ ردیفهای موجود =
outpatient.
۶. اصلاح CoverageRule و BillingCalculator
CoverageRule نوع خدمت را نمیشناسد و نباید بشناسد؛ فقط منطق فرانشیز اصلاح میشود:
final readonly class CoverageRule
{
public function __construct(
public float $coveragePercent,
public int $franchiseRials, // فقط برای بیمهٔ تکمیلی معنا دارد
public ?int $ceilingRials,
public bool $covered = true,
) {}
}
در BillingCalculator::calculateItem() فرانشیزِ پایه حذف شود:
// بیمهٔ پایه صرفاً درصدی است: سهم بیمار = کل − سهم پایه. فرانشیز فقط در بیمهٔ تکمیلی معنا دارد.
$franchise = new Money($supplementary?->franchiseRials ?? 0);
$patient = $remaining->add($franchise)->min($total);
و TenantInsuranceService::coverageRule*() هنگام ساخت قاعدهٔ پایه franchiseRials: 0 بدهد (ستون DB برای سازگاری باقی میماند ولی در محاسبهٔ پایه دخالت نمیکند).
سناریوی مرجع باید دقیقاً برقرار شود: calculateItem(new Money(5_952_000), new CoverageRule(30.0, 0, null), null) →
base_insurance_rials = 1_785_600 و patient_rials = 4_166_400.
۷. زنجیرهٔ resolve درصد پوشش
در TenantInsuranceService یک متد خصوصی اضافه شود و هر دو متد coverageRule() / coverageRuleForService() از آن استفاده کنند:
/**
* درصد پوشش مؤثر، به ترتیب اولویت:
* ۱) override همان خدمت (TenantServiceCoverage.coverage_percent)
* ۲) override قرارداد برای نوع خدمت (TenantInsuranceCategoryCoverage)
* ۳) پیشفرض مرکزی ادمین (InsuranceCoverageDefault)
* ۴) coverage_percent قرارداد (سازگاری با ردیفهای قدیمی)
*/
private function resolvePercent(
TenantInsurance $contract,
ServiceCategory $category,
?TenantServiceCoverage $override,
): float
coverageRule() (ویزیت) با ServiceCategory::Outpatient صدا زده شود؛ coverageRuleForService() با service_category همان ServiceItem.
PatientService::contractPercent() (snapshot روی base_insurance_discount_percent مراجعه) هم از همین زنجیره تغذیه شود — این فیلد فقط snapshot نمایشی است و نباید ورودی محاسبه باشد (همانطور که در docblock فعلی نوشته شده).
۸. پنل ادمین اصلی — بخش تنظیمات درصدهای هر بیمه
در assets/admin/pages/CategoriesPage.tsx (تب «بیمهها»، اطراف خط ۸۷۰ به بعد):
- در هر ردیف بیمه یک اکشن «تنظیمات پوشش» اضافه شود که مودالی باز کند (از
components/ui/Modalموجود؛ طراحی جدید نساز — همان تم/کامپوننتهای موجود). - مودال از
GET /api/v1/admin/insurance/{id}/coverage-defaultsمیخواند و یک ردیف ورودی درصد بهازای هر category برمیگرداند (لیست از API، نه hardcode) و باPUTذخیره میکند؛ سپسqueryKeyلیست بیمهها invalidate شود. - اعتبارسنجی کلاینت: عدد ۰ تا ۱۰۰، با
digitsOnly(value, 3)مثلInsuranceModal. - برای بیمهٔ تکمیلی هم همین بخش نمایش داده شود (ساختار یکسان)، ولی متن راهنما شفاف کند که سهم تکمیلی روی باقیماندهٔ پس از پایه اعمال میشود.
۹. پنل پزشک/کلینیک — پیشفرضگیری از تنظیمات مرکزی
assets/admin/components/InsuranceModal.tsx:
InsuranceFormValuesبهجایcoverage: stringتکمقداری،categoryPercents: Record<string, string>بگیرد.- با انتخاب بیمه در حالت «افزودن»، مقادیر از
coverage_defaultsهمان بیمه (ازpricingQuery) پر شوند؛ در حالت «ویرایش»، ازcategory_coveragesقرارداد. - ورودیهای درصد فقط وقتی قابل ویرایش باشند که
canUpdate(ازusePermissions) درست باشد؛ در غیر این صورتreadOnlyبا متن راهنما «مقدار پیشفرض تنظیمات مرکزی». - زیر هر ورودی که هنوز override نشده، برچسب کوچک «پیشفرض ادمین» نمایش داده شود (
category_coverage_source). - فیلد «فرانشیز (تومان)» برای قراردادهای
basicحذف شود (درbuildInsurancePayloadبرای basic همیشهfranchise_rials: 0)؛ برایsupplementaryباقی بماند.
assets/admin/components/TenantInsuranceContracts.tsx:
contractSummary()بهجای «پوشش X٪» درصدها را به تفکیک نشان دهد:سرپایی ۷۰٪ · بستری ۳۰٪ · سقف پوشش …؛ فرانشیز فقط در قراردادهای تکمیلی.- در
ContractDetailsبهازای هر category یکDetailCellرندر شود.
assets/admin/pages/MyPatientsPage.tsx:
shareToDiscountPercentحذف شود.applyBaseInsurance/applySuppInsuranceدرصد را از قرارداد فعال همان بیمه (/api/v1/billing/tenant-insurances) بگیرند، با category پیشفرضoutpatientبرای ویزیت.patient_share_rialsدیگر ورودی محاسبه نیست؛ نقشEntityInsurancePricingفقط «قیمت ویزیت آزاد» وrequire_visit_priceباقی میماند.
assets/admin/components/session/CreateStep.tsx:
patientShareOf(آینهٔ سرور) با قاعدهٔ جدید همتراز شود: فرانشیز پایه حذف، وruleFor()درصد را بر اساسservice_categoryخدمت انتخاب کند.coverageOf(id)برای ویزیت درصدoutpatientرا برگرداند.
۱۰. مستندات و تست
docs/api/insurance.md: دو اندپوینت جدید coverage-defaults، فیلدهای جدید درGET /api/v1/insurances،GET /api/v1/insurance-pricing، وGET/POST/PATCH /api/v1/billing/tenant-insurances.docs/api/billing.mdوdocs/api/patient.md: فرمول جدید سهم پایه/بیمار و بیاثرشدن فرانشیز در بیمهٔ پایه.docs/api/clinic-services.md: فیلدservice_categoryروی خدمت.- PHPUnit:
BillingCalculator: سناریوی مرجع ۵,۹۵۲,۰۰۰ ریال با ۳۰٪؛ پایه+تکمیلی پشتسرهم؛ سقف تعهد؛ درصد ۰ و ۱۰۰؛notCovered.TenantInsuranceService::resolvePercent: هر چهار سطح زنجیره + رفتار fallback پس از تغییر پیشفرض ادمین.- Controller:
PUT coverage-defaultsبا درصد ۱۰۱ → ۴۲۲؛ کاربر غیر ادمین → ۴۰۳.
- Vitest:
InsuranceModal(پیشفرضگیری ازcoverage_defaults، readOnly بدون مجوز)،contractSummary،patientShareOfدرCreateStep. - اجرای واقعی:
ddev exec php bin/phpunit،ddev exec php vendor/bin/phpstan analyse،yarn test،npx tsc --noEmit --project tsconfig.json.
نکات مهم
- واحد پول: ذخیرهسازی و API ریال است؛ ورودی/نمایش پنل تومان (
tomanToRial/rialToToman/formatRial). سناریوی کاربر با تومان نوشته شده — در تستها ریال بگذار. - گرد کردن: یک قاعده در کل سیستم —
Money::percent()سمت سرور وMath.round(total * percent / 100)سمت کلاینت. سهم بیمار همیشهtotal − baseShareاست، نه یک محاسبهٔ درصدیِ مستقل؛ در غیر این صورت مجموع دو سهم با کل برابر نمیشود. - منبع واحد محاسبه: فقط
BillingCalculator.PatientService::calculateFinalPrice()وInvoiceService::createFromSession()هر دو از آن عبور میکنند و نباید محاسبهٔ موازی اضافه شود. کد فرانت آینه است و حق ندارد به تنهایی مبنای ثبت باشد. - سازگاری با دادهٔ موجود: ستونهای
tenant_insurances.coverage_percentوfranchise_rialsحذف نشوند (سطح ۴ زنجیره + قراردادهای تکمیلی). فقط از مسیر محاسبهٔ بیمهٔ پایه خارج میشوند. - fallback زنده، نه کپی: قرارداد بدون override نباید درصد را در جدول خودش snapshot کند؛ در غیر این صورت هدف «مدیریت مرکزی» نقض میشود.
- کالاهای مصرفی (
SessionConsumable) بدون پوشش بیمهاند — این رفتار تغییر نکند. ServiceItem.insurance_coveredهمچنان gate نهایی است: خدمتی که این پرچم را ندارد، هرچقدر هم درصد تعریف شده باشد،notCoveredمیماند.- الگوهای پروژه: پاسخها با
$this->success()/$this->paginated()/$this->error()؛ لیستهای ادمین باgetArrayResult()؛ تاریخها Unix timestamp؛ رشتههای UI فارسی؛ در فرانت بهجای<select>ازSearchableSelect؛ صفحه/مودال جدید با همان تم و کامپوننتهای موجود، بدون طراحی تازه. - پاسخهای تودرتو:
tenant-insurancesدر فرانت با(data as any)?.data?.dataخوانده میشود — اگر شکل پاسخ را تغییر دادی، هر سه مصرفکننده (TenantInsuranceContracts,CreateStep,MyPatientsPage) را همزمان اصلاح کن. - بعد از اتمام:
graphify update .(پس از commit).