Files
clinicpro/docs/new_feture/taskes/task-12-treatment-course/architecture.md
T
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

214 lines
8.6 KiB
Markdown
Raw 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/Course/
├── Entity/
│ ├── CourseProtocol.php
│ ├── CourseProtocolStep.php # پارامتر هر جلسه
│ ├── TreatmentCourse.php
│ └── CourseSession.php
├── Service/
│ ├── CourseStarter.php # شروع دوره از پروتکل
│ ├── CourseScheduler.php # رزرو یکجا + پیشنهاد جلسهٔ بعدی
│ ├── CourseProgressCalculator.php
│ └── CourseSessionLinker.php # اتصال نوبت ↔ جلسهٔ دوره
├── Controller/{CourseProtocolController, TreatmentCourseController}.php
└── Repository/…
```
## `CourseProtocol` و `CourseProtocolStep`
```php
class CourseProtocol
{
use TenantOwnedTrait;
private ServiceItem $service;
private int $sessionCount; // ۸
private int $minDays; // ۲۱
private int $idealDays; // ۲۸
private int $maxDays; // ۴۵
private bool $preferSameResource = true;
private Collection $steps; // CourseProtocolStep
}
class CourseProtocolStep
{
private int $sessionNumber; // ۱..۸
private array $params = []; // {"energy": 12} — اسکالر، فهرست آزاد
private ?int $overrideDurationMinutes = null; // جلسهٔ اول طولانی‌تر است
}
```
`params` آزاد است چون هر تخصص پارامتر خودش را دارد (سطح انرژی، ضخامت، دوز). ولی مثل
`ClinicResource.attributes` فقط اسکالر — و هیچ منطقی به مقدارش وابسته نیست، فقط نمایش و
ثبت می‌شود.
`minDays <= idealDays <= maxDays` قید اجباری.
## `TreatmentCourse` و `CourseSession`
```php
class TreatmentCourse
{
use TenantOwnedTrait;
public const STATUS_ACTIVE = 'active';
public const STATUS_COMPLETED = 'completed';
public const STATUS_ABANDONED = 'abandoned';
private PatientRecord $patient;
private ServiceItem $service;
private CourseProtocol $protocol;
// ── snapshot پروتکل در لحظهٔ شروع (قانون پنجم مستند) ──
private int $sessionCount;
private int $minDays;
private int $idealDays;
private int $maxDays;
private ?PatientPackage $package = null; // تسک ۱۱ — اختیاری
private ?ClinicResource $preferredResource = null; // منبع جلسهٔ اول
private string $status = self::STATUS_ACTIVE;
private int $startedAt;
}
class CourseSession
{
public const STATUS_PLANNED = 'planned';
public const STATUS_BOOKED = 'booked';
public const STATUS_COMPLETED = 'completed';
public const STATUS_SKIPPED = 'skipped';
private TreatmentCourse $course;
private int $sessionNumber;
private array $params = []; // snapshot از CourseProtocolStep
private ?Appointment $appointment = null;
private string $status = self::STATUS_PLANNED;
private ?int $completedAt = null;
}
```
چهار فیلد فاصله و `params` **کپی** می‌شوند نه FK: تغییر پروتکل فردا نباید دورهٔ در جریان
را عوض کند. این همان تصمیمی است که در `appointment_segments` و `patient_packages` گرفته شد.
## `CourseScheduler` — رزرو یکجا
```php
public function bookAll(TreatmentCourse $course): BookAllResult
{
return $this->em->wrapInTransaction(function () use ($course) {
$anchor = $this->lastCompletedAt($course) ?? time();
$planned = $course->plannedSessions(); // مرتب بر اساس sessionNumber
$holds = [];
foreach ($planned as $session) {
$target = $anchor + $course->getIdealDays() * 86400;
if ($target > time() + 90 * 86400) {
// بیرون از بازهٔ مجاز جستجو — این و بقیه planned می‌مانند
break;
}
$slot = $this->findNearestInRange(
$course, $session,
min: $anchor + $course->getMinDays() * 86400,
ideal: $target,
max: $anchor + $course->getMaxDays() * 86400,
);
if ($slot === null) {
throw new AppException(ErrorCodes::ERR_VALIDATION_001, sprintf(
'برای جلسهٔ %d هیچ وقت مناسبی در بازهٔ مجاز پیدا نشد', $session->getSessionNumber()
), 422);
}
$holds[] = $this->holdService->hold($this->holdRequestFor($course, $session, $slot));
$anchor = $slot->start; // ← لنگر جلسهٔ بعدی، همین جلسه
}
foreach ($holds as $hold) { $this->bookingService->confirm($hold->uuid, $course->owner()); }
return new BookAllResult(count($holds), count($planned) - count($holds));
});
}
```
سه نکتهٔ حیاتی:
1. **همه یا هیچ** — کل حلقه در یک تراکنش. استثنا در جلسهٔ ۵ یعنی rollback جلسات ۱ تا ۴.
رزرو نیمه‌کاره بدترین حالت است: بیمار فکر می‌کند دوره‌اش رزرو شده.
2. **لنگر متحرک** — فاصله از جلسهٔ **قبلی** حساب می‌شود، نه از شروع دوره. اگر جلسهٔ ۲
سه روز دیرتر افتاد، جلسهٔ ۳ هم جابه‌جا می‌شود.
3. **سقف ۹۰ روز** — محدودیت جستجوی تسک ۰۶. جلسات بیرون بازه `planned` می‌مانند و بیمار
بعداً رزرو می‌کند. پیام روشن اجباری است.
## `findNearestInRange` — نزدیک‌ترین به ایده‌آل
```php
$slots = $this->availability->search($req->withRange($min, $max));
if ($slots === []) return null;
usort($slots, fn($a, $b) => abs($a->start - $ideal) <=> abs($b->start - $ideal));
return $slots[0];
```
نزدیک‌ترین به ایده‌آل، نه اولین موجود. ۲۸ روز ایده‌آل است؛ روز ۲۱ (حداقل) از نظر
درمانی بدتر از روز ۲۷ است.
## `same_as_previous` — اتصال به تسک ۰۶
```php
// SameAsPreviousPicker (تسک ۰۶) به یک ورودی نیاز دارد که تا حالا نداشت
public function pick(array $freeIds, PlannedRequirement $req, OccupancyIndex $idx, AppointmentPlan $plan): int
{
$preferred = $plan->context()->preferredResourceIds ?? [];
foreach ($preferred as $id) {
if (in_array($id, $freeIds, true)) return $id;
}
return $this->fallback->pick($freeIds, $req, $idx, $plan); // least_gap
}
```
`preferredResourceIds` از `TreatmentCourse.preferredResource` می‌آید و در `PlanRequest`
حمل می‌شود. اگر منبع ترجیحی آزاد نبود، **رزرو رد نمی‌شود** — به `least_gap` برمی‌گردد.
اجبار به همان منبع یعنی بیمار دو هفته منتظر بماند.
## پیشنهاد جلسهٔ بعدی
```
GET /treatment-course/{uuid}/next-slot-suggestion
{
"session_number": 4,
"params": { "energy": 18 },
"ideal_date": "1405-06-01",
"range": { "min": "1405-05-25", "max": "1405-06-18" },
"suggested_slots": [ … سه وقت نزدیک به ایده‌آل … ],
"warning": null
}
```
`warning` وقتی پر می‌شود که `now > lastCompleted + maxDays`:
«از حداکثر فاصلهٔ مجاز (۴۵ روز) عبور شده است. برای ادامهٔ دوره با پزشک مشورت کنید.»
## اتصال به `spacing` تسک ۰۹
قانون `spacing` و پروتکل دوره هر دو فاصله را محدود می‌کنند. **قانون برنده است** اگر
سخت‌گیرانه‌تر باشد:
```php
$effectiveMin = max($course->getMinDays(), $policyMinDays ?? 0);
```
دلیل: پروتکل پیشنهاد بالینی است، قانون سیاست کلینیک. سیاست کلینیک نمی‌تواند شل‌تر شود.
این را در `docs/api/course.md` بنویس.
## پنل ادمین
- `CourseProtocolsPage.tsx` — پروتکل per سرویس + جدول پارامتر جلسات
- `TreatmentCoursePage.tsx` — نوار پیشرفت («۳ از ۸»)، جدول جلسات با وضعیت و تاریخ،
دکمهٔ «رزرو جلسهٔ بعدی» و «رزرو همهٔ جلسات»
- کارت دوره‌ها در `PatientDetailPage.tsx`
- نوار پیشرفت باید فاصلهٔ واقعی بین جلسات را هم نشان دهد (۲۸ · ۳۱ · ۲۶ روز) — کلینیک از
همان می‌فهمد بیمار منظم است یا نه