- Add implementation notes for cancellation and waitlist features. - Create task documentation outlining goals, current status, and acceptance criteria for cancellation policy and resource utilization reporting. - Establish architecture for domain events and outbox pattern to ensure reliable event publishing. - Define database schema for domain events and necessary queries for resource utilization and plan accuracy reports. - Implement detailed implementation notes covering edge cases, testing strategies, and documentation requirements.
6.7 KiB
معماری — تسک ۰۸
ساختار فایل
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 — هفت مرحلهٔ مستند
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 — فاکتور منجمد
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 اضافه کن، وگرنه اولین
کسی که هر دو را میبیند یکی را حذف میکند.
سیاست بیعانه
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 - «کپی از لیست قیمت قبلی» — بدون آن، کلینیک با ۲۰۰ سرویس هرگز لیست جدید نمیسازد