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.
This commit is contained in:
hamed
2026-07-30 11:43:58 +03:30
parent 1d338503c8
commit 021d0eb6b2
62 changed files with 8098 additions and 0 deletions
@@ -0,0 +1,145 @@
# معماری — تسک ۰۸
## ساختار فایل
```
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
- «کپی از لیست قیمت قبلی» — بدون آن، کلینیک با ۲۰۰ سرویس هرگز لیست جدید نمی‌سازد