- 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.
9.3 KiB
نکات پیادهسازی — تسک ۰۹
۱. چرا DiscountRule مهاجرت نمیکند
وسوسهاش زیاد است: DiscountRule عملاً یک Policy دستهٔ pricing است. ولی:
DiscountEngineروی پرونده (PatientRecord) و مراجعه هم اجرا میشود، نه فقط نوبتDiscountsPage.tsxوdocs/api/discount.mdو تستهای موجود روی همان قراردادند- شش نوع تخفیفش (
patient_tag,occasion,visit_count, …) دقیقاً همان شرطهایFieldRegistryنیستند و نگاشت یکبهیک ندارند
هزینهٔ مهاجرت بالا و سودش صفر است. PricingPolicyEngine هر دو را میخواند و
Combiner نتیجهشان را یکجا ترکیب میکند. ولی یک قاعده لازم است:
تخفیف جدید در
DiscountRuleساخته میشود اگر روی مراجعه هم کار میکند؛ درPolicyاگر فقط قیمت نوبت را عوض میکند. این را درdocs/api/policy.mdبنویس.
۲. FieldRegistry تنها منبع حقیقت
final class FieldRegistry
{
private const FIELDS = [
'patient.age' => ['type' => 'int', 'ops' => ['eq','gt','gte','lt','lte','between']],
'patient.tags'=> ['type' => 'array', 'ops' => ['in','not_in','contains']],
// …
];
public function schema(): array; // برای GET /policy-schema
public function extract(string $field, PolicyContext $ctx): mixed; // برای ارزیابی
public function assertValid(string $field, string $op): void; // برای ذخیره
}
سه مسئولیت روی یک آرایه. اگر schema و extract جدا باشند، فیلدی در فرم ظاهر میشود که ارزیابی نمیشود — و قانونی که همیشه false است، بدترین باگ این سیستم است چون خطا نمیدهد.
۳. قانون خاموش هرگز نباید بیصدا false باشد
// ❌
$value = $ctx->get($field) ?? null;
if ($value === null) return false; // قانون بیصدا رد میشود
// ✅
if (!$this->registry->has($field)) {
throw new \LogicException("فیلد ناشناخته در قانون: {$field}"); // نباید ممکن باشد؛ ذخیره جلویش را گرفته
}
$value = $this->registry->extract($field, $ctx);
if ($value === self::UNAVAILABLE) {
$this->logger->warning('policy_field_unavailable', ['policy' => $id, 'field' => $field]);
return false; // با لاگ، نه سکوت
}
مثال واقعی: قانون روی patient.last_session_at برای بیمار جدید. مقدار وجود ندارد و
قانون باید رد شود — ولی با لاگ، تا اگر کلینیک گفت «قانونم کار نمیکند» جواب داشته باشیم.
۴. spacing — کوئری، نه حلقه
بدترین اشتباه ممکن در این تسک:
// ❌ فاجعهٔ کارایی — ۹۰ روز × دهها اسلات × یک کوئری
foreach ($slots as $slot) {
$last = $this->appointmentRepo->findLastSession($patient, $service);
if ($slot['start'] - $last < $minDays * 86400) continue;
}
// ✅ یک کوئری، بعد بازهٔ ممنوعه
$last = $this->appointmentRepo->findLastCompletedAt($patient, $service); // ۱ کوئری
if ($last !== null) {
$forbidden[] = ['start' => $last, 'end' => $last + $minDays * 86400];
}
// بازه به CandidateGenerator داده میشود → آن نقطهها ساخته نمیشوند
AvailabilityPerformanceTest تسک ۰۶ باید با قوانین فعال هم سبز بماند. اگر بعد از این
تسک قرمز شد، دلیلش همین است.
۵. ترتیب اعمال تخفیف — پشتسرهم
$remaining = $subtotal;
foreach ($sortedPolicies as $policy) { // به ترتیب اولویت
$amount = intdiv($remaining * $policy->percent(), 100);
$remaining -= $amount;
$lines[] = PriceLine::discount($policy->getName(), -$amount);
}
نه جمع درصدها. ۴۰٪ سپس ۱۰٪ = ۴۶٪ کل، نه ۵۰٪. مستند بند ۸ صریح: «به ترتیب اولویت پشت سر هم».
۶. combinable و short-circuit
foreach ($sorted as $policy) {
if (!$this->conditions->matches($policy, $ctx)) continue;
$applied[] = $policy;
if (!$policy->isCombinable()) break; // ← اولین غیرترکیبشدنی، پایان
}
قانون غیرترکیبشدنی با اولویت بالا، بقیه را میبلعد. این همان رفتار DiscountRule موجود
است و باید یکسان بماند، وگرنه دو دستهٔ تخفیف دو رفتار متفاوت میگیرند.
اثر deny استثناست: همیشه short-circuit، مستقل از combinable.
۷. نسخهبندی — ویرایش ممنوع
// PolicyController: PATCH وجود ندارد. فقط:
POST /policy/{uuid}/version
PATCH روی محتوای قانون عمداً نیست. تنها چیزهایی که بدون نسخهٔ جدید تغییر میکنند:
active، name. شرط و اثر و اولویت → نسخهٔ جدید.
اگر کاربر گفت «فقط میخواهم غلط املایی نام را درست کنم» — name مجاز است. هر چیزی که
روی محاسبه اثر دارد، نه.
۸. edge case ها
| حالت | رفتار درست |
|---|---|
| هیچ قانونی وجود ندارد | همهچیز مثل قبل — تست سازگاری اجباری |
قانون فعال با valid_from آینده |
اعمال نمیشود |
دو قانون deny |
یکی کافی است؛ پیام اولی (بالاترین اولویت) نمایش داده میشود |
قانون add_requirement که هیچ منبع واجد شرایطی ندارد |
NoEligibleResourceException با پیام شامل نام قانون: «قانون X جراح میخواهد ولی جراحی در این شعبه نیست» |
قانون min_duration کمتر از مدت فعلی |
بیاثر (max) |
قانون spacing برای بیمار مهمان بدون سابقه |
رد نمیکند، اعمال نمیشود |
قانون روی booking.channel = online و ثبت از پنل |
اعمال نمیشود |
conditions خالی ({}) |
همیشه true — مجاز، ولی در UI هشدار «این قانون روی همهٔ نوبتها اعمال میشود» |
قانون دستهٔ resource با اثر discount_percent |
422 هنگام ذخیره |
نسخهٔ جدید با valid_from گذشته |
422 — نسخه گذشته را عوض نمیکند |
حذف قانونی که در applied_policy_ids نوبتهاست |
مجاز — name کپی شده و فاکتور سالم است |
سطر ماقبل آخر مهم است: valid_from گذشته یعنی بازنویسی تاریخ، که قانون پنجم مستند را
نقض میکند.
۹. تست
tests/Policy/ConditionEvaluatorTest.php ← واحد، بدون DB
- همهٔ عملگرها روی همهٔ نوعها
- all/any
- فیلد ناموجود → false با لاگ
tests/Policy/CombinerTest.php ← واحد
- min_duration: max برنده
- add_duration: جمع
- add_requirement: union بدون تکرار
- restrict_requirement: اشتراک
- discount: پشتسرهم (۴۰ سپس ۱۰ → ۴۶ کل)
- deny: یکی کافی
tests/Policy/PolicyResolverTest.php
- اولویت > اختصاصیبودن > قدمت (سه سناریوی جدا)
- combinable=false short-circuit
tests/Policy/SpacingPolicyEngineTest.php
- بازهٔ ممنوعه درست
- بیمار بدون سابقه → بیاثر
- تعداد کوئری ثابت (نه per slot)
tests/Policy/PolicyVersioningTest.php ← ⭐ قانون پنجم
- نوبت با نسخهٔ ۱ ثبت شد → نسخهٔ ۲ ساخته شد → فاکتور نوبت تغییر نکرد
- policy_version_log snapshot کامل دارد
- valid_from گذشته → 422
tests/Policy/PolicyIntegrationTest.php
- resource: نیازمندی اضافه در preview
- timing: مدت افزایش مییابد
- eligibility: confirm رد میشود با پیام فارسی
- pricing: ردیف تخفیف در snapshot
tests/Policy/PolicySchemaTest.php
- هر فیلد schema قابل extract است (نه فیلد نمایشی بیارزیابی)
- اثر خارج از دسته → 422
tests/Policy/NoPolicyRegressionTest.php ← ⭐
- بدون هیچ قانون: خروجی preview/availability/quote بیتبهبیت مثل تسک ۰۸
tests/Appointment/AvailabilityPerformanceTest.php ← باید با قوانین فعال هم سبز باشد
۱۰. مستندات
docs/api/policy.md— endpoint ها + فهرست کامل فیلد/عملگر/اثر + قاعدهٔ «DiscountRuleیاPolicy؟»docs/architecture/policy-engine.md— جدول حل تناقض، جدول ترکیب، دلیل ممنوعیت کد دلخواه، و دلیل عدم مهاجرتDiscountRule