Booking the next session stays a decision, not an automation: the system offers free slots and the secretary picks one with the patient in front of them. Booking automatically would fill the worst slot in the calendar — the one nobody wanted — and produce a no-show. A session whose due date has passed with nobody booking it surfaces in an explicit queue instead of waiting silently for the patient to call. Suggestions default to the resource the previous session ran on, since continuing a course on the same device is both clinically steadier and one less choice to make; with no previous booking the caller must name a resource rather than get an empty list. Slot maths is reused from ResourceBookingSlotService; this only decides which resource, from which day, and how far ahead to look. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
208 lines
8.0 KiB
PHP
208 lines
8.0 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Treatment;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\Clinic\Entity\Clinic;
|
|
use App\ClinicService\Entity\CatalogCategory;
|
|
use App\ClinicService\Entity\ServiceItem;
|
|
use App\ClinicService\Entity\ServiceSection;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Doctor\Entity\DoctorAddress;
|
|
use App\Patient\Entity\PatientRecord;
|
|
use App\Resource\Entity\ClinicResource;
|
|
use App\Resource\Entity\ResourceType;
|
|
use App\Staff\Entity\ClinicStaff;
|
|
use App\Tests\ApiTestCase;
|
|
use App\Treatment\Entity\TreatmentCase;
|
|
use App\Treatment\Entity\TreatmentCaseArea;
|
|
use App\Treatment\Entity\TreatmentProtocol;
|
|
use App\Treatment\Entity\TreatmentProtocolStaff;
|
|
use App\Treatment\Entity\TreatmentProtocolStep;
|
|
use App\Treatment\Entity\TreatmentSession;
|
|
|
|
class TreatmentCaseEndpointsTest extends ApiTestCase
|
|
{
|
|
/** @return array{user: \App\Auth\Entity\User, clinic: Clinic, case: TreatmentCase, resource: ClinicResource} */
|
|
private function scenario(): array
|
|
{
|
|
$user = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
|
|
$clinic = new Clinic($user);
|
|
$clinic->setName('کلینیک پرونده');
|
|
$this->em->persist($clinic);
|
|
$this->em->flush();
|
|
|
|
$doctor = new Doctor($this->createUser(['ROLE_USER', 'ROLE_DOCTOR']), 'دکتر ناظر');
|
|
$this->em->persist($doctor);
|
|
$clinic->getDoctors()->add($doctor);
|
|
|
|
$address = DoctorAddress::forClinic($clinic->getId());
|
|
$address->setName('شعبهٔ مرکزی');
|
|
$this->em->persist($address);
|
|
|
|
$type = new ResourceType('clinic', (int) $clinic->getId(), 'laser_' . bin2hex(random_bytes(3)), 'لیزر');
|
|
$this->em->persist($type);
|
|
$this->em->flush();
|
|
|
|
$resource = new ClinicResource($address, $type, 'دستگاه لیزر');
|
|
$resource->setSupervisor($doctor);
|
|
$this->em->persist($resource);
|
|
|
|
$section = new ServiceSection('clinic', (int) $clinic->getId(), 'لیزر');
|
|
$this->em->persist($section);
|
|
$category = new CatalogCategory('clinic', (int) $clinic->getId(), 'دست');
|
|
$this->em->persist($category);
|
|
|
|
$service = new ServiceItem($section, 'لیزر دست', 5_000_000);
|
|
$service->setCatalogCategory($category)->setDurationMinutes(30);
|
|
$this->em->persist($service);
|
|
|
|
$staff = new ClinicStaff('clinic', (int) $clinic->getId(), 'اپراتور');
|
|
$this->em->persist($staff);
|
|
|
|
$protocol = new TreatmentProtocol($service);
|
|
$this->em->persist($protocol);
|
|
$protocol->replaceSteps([
|
|
new TreatmentProtocolStep($protocol, 1, 0),
|
|
new TreatmentProtocolStep($protocol, 2, 30),
|
|
]);
|
|
$protocol->replaceAllowedStaff([new TreatmentProtocolStaff($protocol, $staff)]);
|
|
|
|
$record = new PatientRecord('clinic', (int) $clinic->getId(), $this->createUser(), 'clinic', (int) $clinic->getId());
|
|
$this->em->persist($record);
|
|
$this->em->flush();
|
|
|
|
$case = new TreatmentCase('clinic', (int) $clinic->getId(), $record, $service, $protocol);
|
|
$case->addArea(new TreatmentCaseArea($case, $category, 0));
|
|
$first = new TreatmentSession($case, 1);
|
|
$second = new TreatmentSession($case, 2);
|
|
$case->addSession($first);
|
|
$case->addSession($second);
|
|
$this->em->persist($case);
|
|
$this->em->flush();
|
|
|
|
return ['user' => $user, 'clinic' => $clinic, 'case' => $case, 'resource' => $resource, 'doctor' => $doctor];
|
|
}
|
|
|
|
public function testListReturnsCasesOfTheCurrentTenantOnly(): void
|
|
{
|
|
$mine = $this->scenario();
|
|
$other = $this->scenario();
|
|
|
|
$body = $this->authJson('GET', '/api/v1/treatment-cases', $mine['user']);
|
|
|
|
self::assertSame(200, $this->responseCode());
|
|
$uuids = array_column($body['data'], 'uuid');
|
|
self::assertContains($mine['case']->getUuid(), $uuids);
|
|
self::assertNotContains($other['case']->getUuid(), $uuids);
|
|
}
|
|
|
|
public function testShowReturnsSessionsAndAreas(): void
|
|
{
|
|
$s = $this->scenario();
|
|
|
|
$body = $this->authJson('GET', '/api/v1/treatment-case/' . $s['case']->getUuid(), $s['user']);
|
|
|
|
self::assertSame(200, $this->responseCode());
|
|
self::assertSame(2, $body['data']['total_sessions']);
|
|
self::assertSame(0, $body['data']['completed_sessions']);
|
|
self::assertCount(2, $body['data']['sessions']);
|
|
self::assertSame(['دست'], array_column($body['data']['areas'], 'name'));
|
|
}
|
|
|
|
public function testACaseFromAnotherTenantIsNotFound(): void
|
|
{
|
|
$mine = $this->scenario();
|
|
$other = $this->scenario();
|
|
|
|
$this->authJson('GET', '/api/v1/treatment-case/' . $other['case']->getUuid(), $mine['user']);
|
|
|
|
self::assertSame(404, $this->responseCode());
|
|
}
|
|
|
|
/** جلسهای که سررسیدش رسیده و رزرو نشده باید در صف دیده شود. */
|
|
public function testUnbookedQueueListsDueSessions(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$sessions = $s['case']->getSessions()->toArray();
|
|
$sessions[0]->setDueAt(time() - 3600);
|
|
$this->em->flush();
|
|
|
|
$body = $this->authJson('GET', '/api/v1/treatment-sessions/unbooked', $s['user']);
|
|
|
|
self::assertSame(200, $this->responseCode());
|
|
$uuids = array_column($body['data'], 'uuid');
|
|
self::assertContains($sessions[0]->getUuid(), $uuids);
|
|
// جلسهٔ دوم هنوز سررسید ندارد — لنگرش جلسهٔ اولِ انجامنشده است.
|
|
self::assertNotContains($sessions[1]->getUuid(), $uuids);
|
|
}
|
|
|
|
public function testUnbookedQueueSkipsSessionsThatAlreadyHaveAnAppointment(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$sessions = $s['case']->getSessions()->toArray();
|
|
|
|
$appointment = $this->newAppointment(
|
|
$s['doctor'],
|
|
$this->createUser(['ROLE_USER']),
|
|
time() + 3600,
|
|
time() + 5400,
|
|
$s['clinic'],
|
|
);
|
|
$this->em->persist($appointment);
|
|
$this->em->flush();
|
|
|
|
$sessions[0]->setDueAt(time() - 3600);
|
|
$sessions[0]->attachAppointment($appointment);
|
|
$this->em->flush();
|
|
|
|
$body = $this->authJson('GET', '/api/v1/treatment-sessions/unbooked', $s['user']);
|
|
|
|
self::assertNotContains($sessions[0]->getUuid(), array_column($body['data'], 'uuid'));
|
|
}
|
|
|
|
/** بدون منبع پیشفرض و بدون resource_uuid، باید صریح خطا بدهد نه فهرست خالی. */
|
|
public function testSlotSuggestionsWithoutAResourceIsRejected(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$session = $s['case']->getSessions()->first();
|
|
|
|
$body = $this->authJson('GET', '/api/v1/treatment-session/' . $session->getUuid() . '/slot-suggestions', $s['user']);
|
|
|
|
self::assertSame(422, $this->responseCode());
|
|
self::assertSame('resource_uuid', $body['errors'][0]['field']);
|
|
}
|
|
|
|
public function testSlotSuggestionsAcceptAnExplicitResource(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$session = $s['case']->getSessions()->first();
|
|
|
|
$body = $this->authJson(
|
|
'GET',
|
|
'/api/v1/treatment-session/' . $session->getUuid() . '/slot-suggestions?resource_uuid=' . $s['resource']->getUuid(),
|
|
$s['user'],
|
|
);
|
|
|
|
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
|
self::assertSame($s['resource']->getUuid(), $body['data']['resource_uuid']);
|
|
self::assertArrayHasKey('days', $body['data']);
|
|
self::assertSame($session->getUuid(), $body['data']['session']['uuid']);
|
|
}
|
|
|
|
public function testAResourceFromAnotherTenantIsNotFound(): void
|
|
{
|
|
$mine = $this->scenario();
|
|
$other = $this->scenario();
|
|
$session = $mine['case']->getSessions()->first();
|
|
|
|
$this->authJson(
|
|
'GET',
|
|
'/api/v1/treatment-session/' . $session->getUuid() . '/slot-suggestions?resource_uuid=' . $other['resource']->getUuid(),
|
|
$mine['user'],
|
|
);
|
|
|
|
self::assertSame(404, $this->responseCode());
|
|
}
|
|
}
|