feat(treatment): select treatment behaviour by practice domain, not by if-branch
Everything that differs between specialties as data is already stored as data. What is left is behaviour — when a case opens, what happens once a session ends — so it becomes a TreatmentWorkflow resolved through a tagged-service registry. The booking path calls one collaborator and never names a specialty; adding dentistry is a new class, not an edit to confirmation. A clinic that has chosen no practice domain still gets working multi-session courses: DefaultTreatmentWorkflow answers for null and for any code without a dedicated implementation, keeping "unset means behave as today, not error". LaserTreatmentWorkflow is deliberately empty beyond claiming `beauty` — it is the seam where laser-specific behaviour will land without disturbing anyone else. Session due dates are anchored to the previous session's actual finish, so a patient who comes twenty days late shifts the rest of their course instead of getting the next session while it can still do nothing. Only the next session is recomputed; later ones keep their estimate because they are anchored to nothing yet. Attachment targets the first session without an appointment rather than the first open one: a patient booking again mid-course was otherwise matched to the session that already had a booking, and the second appointment went nowhere. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace App\Treatment\Workflow;
|
||||
|
||||
use App\Appointment\Entity\Appointment;
|
||||
use App\ClinicService\Entity\ServiceItem;
|
||||
use App\Patient\Entity\PatientRecord;
|
||||
use App\Treatment\Entity\TreatmentCase;
|
||||
use App\Treatment\Entity\TreatmentProtocol;
|
||||
use App\Treatment\Entity\TreatmentSession;
|
||||
use App\Treatment\Repository\TreatmentCaseRepository;
|
||||
use App\Treatment\Repository\TreatmentSessionRepository;
|
||||
use App\Treatment\Service\TreatmentCaseOpener;
|
||||
use App\Treatment\Service\TreatmentScheduler;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
|
||||
/**
|
||||
* رفتار دورهٔ درمان وقتی حوزهٔ فعالیت پیادهسازی اختصاصی ندارد.
|
||||
*
|
||||
* عمداً پایینترین اولویت را دارد و به هر کدی جواب مثبت میدهد، پس محیطی که هنوز
|
||||
* حوزهاش را انتخاب نکرده هم دورهٔ چندجلسهایاش کار میکند — تصمیمِ «نال یعنی رفتار
|
||||
* امروز، نه خطا».
|
||||
*/
|
||||
class DefaultTreatmentWorkflow implements TreatmentWorkflow
|
||||
{
|
||||
public function __construct(
|
||||
protected readonly TreatmentCaseRepository $cases,
|
||||
protected readonly TreatmentSessionRepository $sessions,
|
||||
protected readonly TreatmentCaseOpener $opener,
|
||||
protected readonly TreatmentScheduler $scheduler,
|
||||
protected readonly EntityManagerInterface $em,
|
||||
) {}
|
||||
|
||||
public function supports(?string $practiceDomainCode): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function openCase(
|
||||
Appointment $appointment,
|
||||
PatientRecord $record,
|
||||
ServiceItem $service,
|
||||
TreatmentProtocol $protocol,
|
||||
): TreatmentCase {
|
||||
// بیماری که وسط دورهاش نوبت دیگری از همان سرویس میگیرد، جلسهٔ همان دوره را
|
||||
// میگیرد نه یک دورهٔ موازی.
|
||||
$case = $this->cases->findOpenFor($record, $service)
|
||||
?? $this->opener->open(
|
||||
$record->getEntityType(),
|
||||
$record->getEntityId(),
|
||||
$record,
|
||||
$service,
|
||||
$protocol,
|
||||
);
|
||||
|
||||
$this->attachToNextOpenSession($case, $appointment);
|
||||
|
||||
return $case;
|
||||
}
|
||||
|
||||
public function onSessionFinished(TreatmentSession $session): void
|
||||
{
|
||||
$next = $this->scheduler->scheduleNextAfter($session);
|
||||
|
||||
if ($next === null) {
|
||||
$this->closeIfEverySessionIsSettled($session->getTreatmentCase());
|
||||
}
|
||||
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
/** نوبت به اولین جلسهٔ بدونِ نوبت وصل میشود، نه لزوماً به جلسهٔ اول. */
|
||||
protected function attachToNextOpenSession(TreatmentCase $case, Appointment $appointment): void
|
||||
{
|
||||
$session = $this->sessions->findNextUnbooked($case);
|
||||
|
||||
if ($session === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$session->attachAppointment($appointment);
|
||||
// سررسیدِ جلسهای که نوبت گرفته دیگر حدس نیست — همان ساعت نوبت است.
|
||||
$this->scheduler->seedFirstSession($session, $appointment->getSlotStart());
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
protected function closeIfEverySessionIsSettled(TreatmentCase $case): void
|
||||
{
|
||||
foreach ($case->getSessions() as $session) {
|
||||
if (!in_array($session->getStatus(), [TreatmentSession::STATUS_DONE, TreatmentSession::STATUS_CANCELLED], true)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$case->close(TreatmentCase::STATUS_COMPLETED);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user