# معماری — تسک ۰۸ ## ساختار فایل ``` src/Pricing/ ├── Entity/ │ ├── PriceList.php │ ├── PriceListItem.php │ ├── PriceSnapshot.php │ ├── PriceSnapshotLine.php │ └── DepositPolicy.php ├── Service/ │ ├── PricingEngine.php # ارکستراتور هفت‌مرحله‌ای │ ├── PriceResolver.php # قیمت پایه: لیست → تعرفه → سرویس │ ├── SnapshotWriter.php │ └── DepositCalculator.php ├── Dto/{PriceQuote, PriceLine}.php ├── Controller/{PriceListController, PricingController}.php └── Repository/… ``` ## `PricingEngine` — هفت مرحلهٔ مستند ```php public function quote(QuoteRequest $req): PriceQuote { $lines = []; // ۱ قیمت پایهٔ سرویس (از لیست قیمت معتبر در تاریخ رزرو) $lines[] = PriceLine::base($this->resolver->servicePrice($req->service, $req->branch, $req->at)); // ۲ جمع قیمت آیتم‌های انتخابی foreach ($req->options as $option) { $lines[] = PriceLine::option($option, $this->resolver->optionPrice($option, $req->branch, $req->at)); } // ۳ قوانین قیمت به ترتیب اولویت ← DiscountEngine موجود، بعداً تسک ۰۹ $lines = $this->discounts->apply($lines, $req); // ۴ کسر از اعتبار پکیج ← قلاب تسک ۱۱ (فعلاً no-op) $lines = $this->packages->consume($lines, $req); // ۵ مالیات و سهم بیمه ← AppointmentInsuranceService موجود $lines = $this->insurance->apply($lines, $req); $lines = $this->tax->apply($lines, $req); // ۶ بیعانه $deposit = $this->depositCalculator->forQuote($lines, $req); // ۷ خروجی تفکیک‌شده (ذخیره فقط در confirm انجام می‌شود) return new PriceQuote($lines, $deposit); } ``` هر مرحله یک سرویس مستقل با اینترفیس خودش. مرحلهٔ ۳ و ۴ از روز اول در زنجیره هستند حتی وقتی خالی‌اند — همان دلیل تسک ۰۵: امضای عمومی بعداً عوض نشود. ## `PriceResolver` — ترتیب اولویت ``` ۱. ServiceBranchOverride.price_rials (تسک ۰۴ — اختصاصی‌ترین) ۲. PriceListItem از PriceList فعالی که تاریخ رزرو را می‌پوشاند و شعبه‌اش مطابق است ۳. PriceListItem از PriceList فعال محیط (بدون شعبه) ۴. Tariff::findForServiceYear(سال شمسی تاریخ رزرو) ← موجود، دست‌نخورده ۵. ServiceItem.price_rials ← آخرین fallback ``` هیچ‌وقت خطا یا صفر برنمی‌گرداند. سطر ۴ و ۵ تضمین می‌کنند همهٔ داده‌های موجود بدون هیچ لیست قیمتی درست کار کنند. **تاریخ مبنا:** تاریخ **رزرو** (`slot_start`)، نه تاریخ ثبت. مستند بند ۱۲: «از لیست قیمت معتبر در تاریخ رزرو». اگر بیمار امروز برای سه ماه بعد نوبت بگیرد، قیمت آن روز اعمال می‌شود. این تصمیم را در `docs/api/pricing.md` صریح بنویس — دو تفسیر دارد و پشتیبانی از هر دو غیرممکن است. ## `PriceSnapshot` — فاکتور منجمد ```php class PriceSnapshot { use TenantOwnedTrait; private Appointment $appointment; private int $baseRials; private int $optionsRials; private int $discountRials; private int $insuranceBaseRials; private int $insuranceSupplementaryRials; private int $taxRials; private int $finalRials; private int $depositRials; private array $appliedPolicyIds = []; // قانون‌های اعمال‌شده — قانون پنجم مستند private int $createdAt; private Collection $lines; // PriceSnapshotLine } ``` `appliedPolicyIds` از روز اول: مستند بند ۸ می‌گوید «هر نوبت فهرست قانون‌هایی که رویش اعمال شده را ذخیره می‌کند». تسک ۰۹ نسخهٔ قانون‌ها را هم اضافه می‌کند؛ فعلاً شناسهٔ `DiscountRule` ها ثبت می‌شود. `PriceSnapshotLine` ردیف‌های تفکیک‌شده: نوع (`base`|`option`|`discount`|`insurance`|`tax`)، نام، مبلغ، ارجاع اختیاری به منبع (سرویس/آیتم/قانون). ## رابطه با `Invoice` موجود `Invoice`/`InvoiceItem` (دامنهٔ `Billing`) **باقی می‌مانند** و کارشان صورتحساب مراجعهٔ انجام‌شده است. `PriceSnapshot` کار متفاوتی می‌کند: قیمت **لحظهٔ رزرو**. | | `PriceSnapshot` | `Invoice` | |---|---|---| | کِی ساخته می‌شود | `confirm` نوبت | پایان مراجعه | | چه چیزی را ثبت می‌کند | آن‌چه قرار بود پرداخت شود | آن‌چه واقعاً انجام و صورتحساب شد | | تغییر می‌کند | هرگز | تا تسویه | اگر بیمار سر نوبت خدمت اضافه بگیرد، `Invoice` فرق می‌کند و `PriceSnapshot` نه — و همین تفاوت، منبع گزارش «اختلاف پیش‌بینی و واقعیت» است. این جدول را در `docs/architecture/insurance-billing-system.md` اضافه کن، وگرنه اولین کسی که هر دو را می‌بیند یکی را حذف می‌کند. ## سیاست بیعانه ```php class DepositPolicy { use TenantOwnedTrait; private ?ServiceItem $service = null; // null = پیش‌فرض محیط private string $mode; // none | fixed | percent private int $value = 0; private ?int $minRials = null; private ?int $maxRials = null; } ``` `DepositCalculator` اختصاصی‌ترین سیاست را می‌گیرد (سرویس بر محیط) و مقدار را به `Appointment.deposit_required/deposit_amount_rials` موجود می‌نویسد — ستون‌های جدید لازم نیست. ## پنل ادمین - `PriceListsPage.tsx` — لیست با بازهٔ شمسی و وضعیت (پیش‌نویس/فعال/منقضی) - `PriceListFormPage.tsx` — بازهٔ تاریخ با `PersianDatePicker`، شعبه با `SearchableSelect`، جدول سرویس‌ها با `PriceInput` - در `AppointmentDetailPage.tsx` یک کارت «فاکتور» با ردیف‌های snapshot - «کپی از لیست قیمت قبلی» — بدون آن، کلینیک با ۲۰۰ سرویس هرگز لیست جدید نمی‌سازد