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:
@@ -0,0 +1,136 @@
|
||||
# نکات پیادهسازی — تسک ۰۸
|
||||
|
||||
## ۱. تاریخ مبنا: تاریخ رزرو، نه تاریخ ثبت
|
||||
|
||||
```php
|
||||
$at = $appointment->getSlotStart(); // ✅
|
||||
// نه: time()
|
||||
```
|
||||
|
||||
دو تفسیر ممکن است و باید یکی انتخاب شود. مستند بند ۱۲ صریح میگوید «لیست قیمت معتبر در
|
||||
تاریخ رزرو». پیامدش: بیمار که امروز برای مهر نوبت میگیرد، قیمت مهر را میپردازد.
|
||||
|
||||
این را در `docs/api/pricing.md` و در UI («قیمت بر اساس تاریخ نوبت محاسبه شده است») بنویس.
|
||||
|
||||
## ۲. عدد صحیح ریال، همهجا
|
||||
|
||||
```php
|
||||
// درصد تخفیف روی مبلغ صحیح
|
||||
$discount = intdiv($amount * $percent, 100); // ✅ گردکردن به پایین، قطعی
|
||||
// نه: (int) round($amount * $percent / 100) // ❌ float در مسیر پول
|
||||
```
|
||||
|
||||
`intdiv` قطعی است و در همهٔ پلتفرمها یکسان. یک ریال اختلاف در جمع فاکتور، ساعتها
|
||||
دیباگ حسابداری میآورد.
|
||||
|
||||
## ۳. سقف تخفیف
|
||||
|
||||
مستند بند ۸: «تخفیف درصدی: به ترتیب اولویت پشت سر هم، با یک سقف قابل تنظیم».
|
||||
|
||||
```php
|
||||
// SiteConfig یا تنظیمات محیط
|
||||
$maxPercent = $this->config->maxTotalDiscountPercent($ctx) ?? 100;
|
||||
$cap = intdiv($subtotal * $maxPercent, 100);
|
||||
$discountTotal = min($discountTotal, $cap);
|
||||
```
|
||||
|
||||
بدون سقف، سه قانون ۴۰٪ پشتسرهم مبلغ را به ۲۱٪ میرسانند و کلینیک صبح روز بعد
|
||||
متوجه میشود.
|
||||
|
||||
**پشت سر هم، نه جمع:** ۴۰٪ سپس ۱۰٪ یعنی `0.9 × 0.6 = 0.54`، نه `1 - 0.5 = 0.5`.
|
||||
این تفاوت باید در تست باشد.
|
||||
|
||||
## ۴. مبلغ نهایی هرگز منفی نیست
|
||||
|
||||
```php
|
||||
$final = max(0, $subtotal - $discount - $insuranceBase - $insuranceSupplementary + $tax);
|
||||
```
|
||||
|
||||
و اگر `max(0, …)` فعال شد، یک ردیف `price_snapshot_lines` با `kind='adjustment'` و
|
||||
مبلغ اصلاحی ثبت شود — وگرنه جمع ردیفها با `final_rials` نمیخواند و اولین کسی که
|
||||
فاکتور را audit کند فکر میکند باگ محاسباتی است.
|
||||
|
||||
## ۵. جمع ردیفها باید با مبلغ نهایی بخواند
|
||||
|
||||
تست ثابت (invariant):
|
||||
|
||||
```php
|
||||
$sum = array_sum(array_map(fn($l) => $l->getAmountRials(), $snapshot->getLines()));
|
||||
self::assertSame($snapshot->getFinalRials(), $sum, 'جمع ردیفها باید با مبلغ نهایی برابر باشد');
|
||||
```
|
||||
|
||||
با علامتگذاری درست (تخفیف و سهم بیمه منفی) این همیشه برقرار است. اگر نبود، یکی از
|
||||
مراحل ردیف ننوشته — که یعنی فاکتور غیرقابلتوضیح.
|
||||
|
||||
## ۶. بیمه: از موجود استفاده کن، دوباره نساز
|
||||
|
||||
`AppointmentInsuranceService` و `TenantServiceCoverage` و `TenantInsuranceCategoryCoverage`
|
||||
از قبل هستند و منطق «تکمیلی روی باقیماندهٔ بعد از پایه» را دارند
|
||||
(`docs/architecture/insurance-billing-system.md`). `PricingEngine` مرحلهٔ ۵ فقط آن را صدا
|
||||
میزند و نتیجه را به ردیف تبدیل میکند.
|
||||
|
||||
قاعدهٔ پروژه: «API جدید فقط وقتی هیچ اندپوینت موجودی کافی نباشد». اینجا سرویس موجود
|
||||
کافی است — بازنویسیاش یعنی دو منبع حقیقت برای پوشش بیمه.
|
||||
|
||||
## ۷. تداخل بازهٔ لیست قیمت
|
||||
|
||||
```php
|
||||
// PriceListService::activate()
|
||||
$overlap = $this->repo->findActiveOverlapping($ctx, $branch, $validFrom, $validTo);
|
||||
if ($overlap !== []) {
|
||||
throw new AppException(ErrorCodes::ERR_VALIDATION_001, sprintf(
|
||||
'لیست قیمت «%s» بازهٔ مشترک دارد', $overlap[0]->getName()
|
||||
), 422);
|
||||
}
|
||||
```
|
||||
|
||||
نکتهٔ ظریف: لیست بدون شعبه (`branch_id = NULL`) با لیست شعبهدار تداخل **ندارد** —
|
||||
دومی اختصاصیتر است و اولویت دارد. فقط لیستهای همسطح با هم تداخل دارند.
|
||||
|
||||
## ۸. edge case ها
|
||||
|
||||
| حالت | رفتار درست |
|
||||
|---|---|
|
||||
| هیچ لیست قیمتی تاریخ را نمیپوشاند | fallback: تعرفهٔ سال → قیمت سرویس |
|
||||
| سرویس در لیست قیمت نیست | همان fallback per سرویس، نه رد کل quote |
|
||||
| `valid_to = null` و لیست جدید با `valid_from` وسط آن | `activate` باید لیست قبلی را با `valid_to = new.valid_from - 1` ببندد و پیام بدهد، نه `422` خشک |
|
||||
| نوبت حالت `slot` بدون سرویس | snapshot با `visit_price_rials` و یک ردیف `base` |
|
||||
| تخفیف بیشتر از مبلغ | `final = 0` + ردیف `adjustment` |
|
||||
| بیعانه درصدی وقتی مبلغ صفر است | بیعانه صفر، `deposit_required = false` |
|
||||
| `reschedule` | نوبت جدید، snapshot جدید با قیمت **تاریخ جدید** |
|
||||
| snapshot موجود و `confirm` دوباره (idempotent تسک ۰۷) | snapshot دستنخورده بماند، دوباره ساخته نشود |
|
||||
| مبلغ بزرگتر از `INT_MAX` ریال (۲.۱ میلیارد) | `BIGINT` لازم؟ — ۲۱۴ میلیون تومان. برای پکیجهای بزرگ ممکن است. **تصمیم: `BIGINT` برای `final_rials` و `amount_rials`** |
|
||||
|
||||
آخرین سطر را جدی بگیر: پکیج ۸ جلسه لیزر فولبادی میتواند از سقف `INT` عبور کند.
|
||||
`price_snapshots.final_rials` و `price_snapshot_lines.amount_rials` را `BIGINT` بگیر.
|
||||
(بقیهٔ ستونهای `price_rials` پروژه `INT` میمانند — قیمت واحد از سقف عبور نمیکند.)
|
||||
|
||||
## ۹. تست
|
||||
|
||||
```
|
||||
tests/Pricing/PriceResolverTest.php
|
||||
- ترتیب پنجگانه: override شعبه > لیست شعبه > لیست محیط > تعرفه > قیمت سرویس
|
||||
- تاریخ بدون لیست → fallback
|
||||
tests/Pricing/PricingEngineTest.php
|
||||
- تخفیف پشتسرهم: ۴۰٪ سپس ۱۰٪ → ۵۴٪ باقی، نه ۵۰٪
|
||||
- سقف تخفیف اعمال میشود
|
||||
- مبلغ منفی → صفر + ردیف adjustment
|
||||
- جمع ردیفها = مبلغ نهایی (invariant، در همهٔ سناریوها)
|
||||
tests/Pricing/PriceSnapshotImmutabilityTest.php ← ⭐ قانون پنجم
|
||||
- ثبت نوبت → تغییر قیمت سرویس → snapshot بدون تغییر
|
||||
- حذف قانون تخفیف → label و مبلغ ردیف سالم
|
||||
tests/Pricing/PriceListActivationTest.php
|
||||
- بازهٔ همپوشان همسطح → 422
|
||||
- لیست شعبه با لیست محیط → تداخل نیست
|
||||
tests/Pricing/DepositCalculatorTest.php
|
||||
- درصدی با min/max
|
||||
- سیاست سرویس بر سیاست محیط اولویت دارد
|
||||
tests/Pricing/QuoteTenantTest.php
|
||||
- سرویس محیط دیگر → 404
|
||||
```
|
||||
|
||||
## ۱۰. مستندات
|
||||
|
||||
`docs/api/pricing.md` بساز. `docs/architecture/insurance-billing-system.md` را با جدول
|
||||
`PriceSnapshot` vs `Invoice` بهروز کن — این تنها راه جلوگیری از حذف یکی از آنها در
|
||||
آیندهٔ نزدیک است.
|
||||
Reference in New Issue
Block a user