Files
hamed 021d0eb6b2 feat: implement cancellation policy, no-show tracking, and waitlist management
- 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.
2026-07-30 11:43:58 +03:30

146 lines
6.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# معماری — تسک ۰۸
## ساختار فایل
```
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
- «کپی از لیست قیمت قبلی» — بدون آن، کلینیک با ۲۰۰ سرویس هرگز لیست جدید نمی‌سازد