# سیستم مدیریت تخفیف عمومی (Discount Rules Engine) + اعمال در پرداخت پرونده ## پروژه `clinicpro` (backend Symfony + پنل ادمین React). تک-ریپو. > پیشنیاز منطقی: `session-autofill-on-appointment-confirm.md` (تخفیف روی `final_price_rials` اعمال میشود). > تست: پنل ادمین با `09390039833 / 09390039833`. اجرا داخل ddev. ## زمینه الان تخفیف فقط **تسویهی دستی** است: در صفحه پرداخت پرونده، اپراتور `percent` یا `fixed` با مقدار آزاد وارد میکند؛ روی `PatientSession.discount_type/discount_value/discount_rials` ذخیره میشود (`PATCH /api/v1/session/{uuid}` → `PatientService::applyDiscount()`). هیچ **قانون تخفیف** تعریفشدهای وجود ندارد، هیچ محاسبهی خودکاری بر اساس تگ/سرویس/مبلغ/… نیست، و هیچ ردی از «کدام قانون» اعمال شده ذخیره نمیشود (audit gap). هدف: یک سیستم **تخفیف عمومی (Generic Discount Rules)** که در `/admin/subscription` مدیریت شود و هنگام پرداخت پرونده، تخفیفهای قابلاعمال را خودکار محاسبه، به اپراتور پیشنهاد، و پس از انتخاب با ثبت منبعِ Rule اعمال کند. ## هدف / قابلیت 1. Entity + CRUD ادمین برای **DiscountRule** با انواع مختلف. 2. **Engine** که برای یک پرونده، قوانین قابلاعمال را ارزیابی و مبلغ تخفیف هرکدام را محاسبه کند. 3. اعمال در صفحه پرداخت با نمایش مبلغ قبل/تخفیف/نهایی + منبع Rule + امکان انتخاب/حذف. 4. **Audit**: ثبت اینکه کدام Rule اعمال شده، در پرونده و سوابق مالی، برای گزارشگیری. ## انواع قوانین (rule types) | نوع | `type` | هدف (`target_*`) | نمونه | |-----|--------|------------------|-------| | تگ بیمار | `patient_tag` | `target_tag_id` (TenantTag) | VIP، پرسنل، خانواده پزشک، خیریه | | مبلغ فاکتور | `invoice_amount` | `min_amount_rials` (آستانه) | بالای ۲م → ۱۰٪ | | بیمار خاص | `specific_patient` | `target_record_id` (PatientRecord) + بازهی زمانی اختیاری | بیمار A همیشه ۳۰٪ | | مناسبتی | `occasion` | `valid_from`/`valid_to` (+ زیرنوع تولد) | تولد بیمار، کمپین، بازه | | سرویس | `service` | `target_service_item_id` (ServiceItem) | لیزر ۲۰٪ | | تعداد مراجعات | `visit_count` | `min_visit_count` | بعد از مراجعه ۵م → ۱۰٪ | هر Rule مشترکاً دارد: `discount_type` (`percent`|`fixed`)، `value` (درصد یا ریال)، `priority` (int، بزرگتر = مهمتر)، `combinable` (bool)، `active` (bool)، `valid_from`/`valid_to` (nullable int unix — برای موقت/مناسبتی)، و مالکیت scope (`owner_type` doctor|clinic + `owner_id`) همسو با بقیهی دادههای per-tenant. ## اولویت / ترکیبپذیری - قوانین قابلاعمال بر اساس `priority` نزولی مرتب شوند. - پیشفرض: **فقط یک تخفیف** (بالاترین priority) اعمال میشود. - اگر Rule `combinable = true` باشد، میتواند با سایر combinableها جمع شود (جمع مبلغ ریالی، با سقفِ `final_price_rials`). - اپراتور میتواند بهجای پیشنهاد خودکار، دستی یکی را انتخاب یا حذف کند. ## فایلهای مرتبط | فایل | نقش | |------|-----| | `src/Discount/Entity/DiscountRule.php` (جدید) | Entity قانون تخفیف | | `src/Discount/Repository/DiscountRuleRepository.php` (جدید) | کوئریها (array hydration برای لیست ادمین) | | `src/Discount/Service/DiscountEngine.php` (جدید) | ارزیابی قوانین برای یک `PatientSession` → لیست پیشنهادها | | `src/Discount/Controller/DiscountController.php` (جدید) | CRUD ادمین + endpoint محاسبه برای یک session | | `src/Patient/Entity/PatientSession.php` | افزودن ستونهای audit `applied_discount_rule_id` (nullable) + `applied_discount_rule_label` (nullable) — `discount_type/value/rials` و `setDiscount()` (L196-203) موجودند | | `src/Patient/Service/PatientService.php` | `applyDiscount()` (L244-283) — گسترش برای پذیرش/ثبت `rule` | | `src/Patient/Controller/PatientController.php` | `updateSession()` (L1034-1073) — عبور `discount_rule_uuid` | | `src/Patient/Entity/PatientRecord.php` | `getTags()` (ManyToMany `patient_record_tags` → TenantTag) — برای `patient_tag` | | `src/Tag/Entity/TenantTag.php` | برچسب بیمار (per-tenant، `name`/`color`) | | `src/ClinicService/Entity/ServiceItem.php` | برای `service` — `getPriceRials()` | | `assets/admin/pages/AdminSubscriptionPage.tsx` | صفحهی تبدار (`.seg`) — افزودن تب «مدیریت تخفیفها» | | `assets/admin/components/session/PaymentStep.tsx` | UI پرداخت — نمایش/انتخاب تخفیفهای پیشنهادی | | `docs/api/*.md` | مستندات (فایل جدید `docs/api/discount.md` + بهروزرسانی `patient.md`) | ## وضعیت فعلی `src/Patient/Service/PatientService.php` — تخفیف دستی، بدون منبع Rule: ```php public function applyDiscount(PatientSession $session, ?string $type, int $value): void { $final = $session->getFinalPriceRials(); if ($type === 'percent') { if ($value > 100) throw new AppException(/* ... */ 'discount_value'); $rials = (int) round($final * $value / 100); } else { if ($value > $final) throw new AppException(/* ... */ 'discount_value'); $rials = $value; } if ($rials > $final - $session->getPaidTotalRials()) throw new AppException(/* ... */); $session->setDiscount($type, $value, $rials); // ← فقط type/value/rials؛ بدون rule } ``` `assets/admin/components/session/PaymentStep.tsx` — تخفیف فقط `percent`/`fixed` با مقدار آزاد (L98-140): ```tsx const applyDiscount = () => { if (!discountType || !discountValue) return; discountMut.mutate({ discount_type: discountType, discount_value: Number(discountValue) }); }; // discountMut → PATCH /api/v1/session/{sessionUuid} ``` `assets/admin/pages/AdminSubscriptionPage.tsx` — تبدار با `.seg` (L437-453): ```tsx const [tab, setTab] = useState<'plans' | 'report'>('plans'); //