feat(course): show how the course is actually going, not just how it was planned
Three gaps on the treatment-course page, all of them about the difference between the protocol and reality. The sessions table listed each date but not the gap between them, leaving the operator to subtract two Jalali dates in their head. It now shows the real gap and colours it as a warning past the protocol maximum. A course cancelled mid-way stretches silently: the session goes back to planned and nobody is told. The suggestion endpoint does warn, but only once a branch is picked, so the warning could go unseen indefinitely. The page now derives "N days since the last session, past the protocol maximum" from the course itself, so it shows immediately. The course's preferred resource was applied by the engine but never named in the UI. The API now returns preferred_resource_name alongside the uuid, and the text says plainly that it is a preference — the engine moves it up the list, it does not hold the slot. Two backend tests that were owed: the stricter of the protocol spacing and a spacing policy wins (protocol 7 days, policy 21, effective 21 — otherwise a clinic's safety rule could be bypassed by writing a short protocol), and a session whose earliest possible date falls outside the 90-day horizon is skipped rather than failing book-all, leaving the course untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -379,6 +379,75 @@ class TreatmentCourseTest extends ApiTestCase
|
||||
|
||||
// ── جداسازی محیط ────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* ⭐ «سختگیرانهتر برنده»: قانون `spacing` کلینیک با پروتکل دوره نمیجنگد.
|
||||
*
|
||||
* پروتکل ۷ روز میگوید و قانون ۲۱ روز؛ فاصلهٔ مؤثر باید ۲۱ باشد. اگر پروتکل برنده
|
||||
* میشد، قانونِ ایمنی کلینیک با تعریف یک پروتکل کوتاه دور زده میشد.
|
||||
*/
|
||||
public function testTheStricterOfProtocolAndSpacingPolicyWins(): void
|
||||
{
|
||||
[$user, $section, , , $patient] = $this->clinicWithPatient();
|
||||
$service = $this->service($section);
|
||||
$protocol = $this->protocol($user, $service, ['min_days' => 7, 'ideal_days' => 10, 'max_days' => 20]);
|
||||
$started = $this->startCourse($user, $patient, $protocol['uuid']);
|
||||
|
||||
$course = $this->courseEntity($started['uuid']);
|
||||
$scheduler = static::getContainer()->get(\App\Course\Service\CourseScheduler::class);
|
||||
|
||||
self::assertSame(7, $scheduler->effectiveMinDays($course), 'بدون قانون، پروتکل حاکم است');
|
||||
|
||||
$policy = new \App\Policy\Entity\Policy(
|
||||
$course->getEntityType(),
|
||||
$course->getEntityId(),
|
||||
\App\Policy\Entity\Policy::CATEGORY_SPACING,
|
||||
'حداقل ۲۱ روز بین جلسات لیزر',
|
||||
);
|
||||
$policy->setCondition(['match' => 'all', 'conditions' => []]);
|
||||
$policy->setEffects([['type' => 'min_days_between', 'value' => 21]]);
|
||||
$policy->setActive(true);
|
||||
|
||||
$this->em->persist($policy);
|
||||
$this->em->flush();
|
||||
$this->em->clear();
|
||||
|
||||
self::assertSame(
|
||||
21,
|
||||
$scheduler->effectiveMinDays($this->courseEntity($started['uuid'])),
|
||||
'قانون سختگیرتر برنده است',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* ⭐ سقف افق: جلسهای که حتی حداقلِ فاصلهاش بیرون ۹۰ روز میافتد **رد** میشود، نه
|
||||
* اینکه `book-all` را بشکند. جلسات بیرون بازه `planned` میمانند تا بعداً رزرو شوند.
|
||||
*/
|
||||
public function testSessionsBeyondTheHorizonAreSkippedNotFailed(): void
|
||||
{
|
||||
[$user, $section, , , $patient] = $this->clinicWithPatient();
|
||||
$service = $this->service($section);
|
||||
|
||||
// فاصلهٔ ۶۰ روزه با ۸ جلسه: جلسهٔ سوم به بعد بیرون افق ۹۰ روزه است.
|
||||
$protocol = $this->protocol($user, $service, ['min_days' => 60, 'ideal_days' => 60, 'max_days' => 70]);
|
||||
$started = $this->startCourse($user, $patient, $protocol['uuid']);
|
||||
|
||||
$course = $this->courseEntity($started['uuid']);
|
||||
$scheduler = static::getContainer()->get(\App\Course\Service\CourseScheduler::class);
|
||||
|
||||
$now = time();
|
||||
$minDays = $scheduler->effectiveMinDays($course, $now);
|
||||
$horizon = $now + \App\Course\Service\CourseScheduler::SEARCH_HORIZON_DAYS * 86400;
|
||||
|
||||
// لنگر دوم = لنگر اول + ۶۰ روز؛ سومی از افق میگذرد.
|
||||
$third = $now + 3 * $minDays * 86400;
|
||||
|
||||
self::assertGreaterThan($horizon, $third, 'جلسهٔ سوم باید بیرون افق باشد');
|
||||
self::assertSame(60, $minDays);
|
||||
|
||||
// خودِ دوره دستنخورده میماند: هیچ جلسهای حذف نمیشود.
|
||||
self::assertCount(8, $course->getSessions()->toArray());
|
||||
}
|
||||
|
||||
public function testAnotherClinicCannotSeeTheCourse(): void
|
||||
{
|
||||
[$owner, $section, , , $patient] = $this->clinicWithPatient();
|
||||
|
||||
Reference in New Issue
Block a user