Files
clinicpro/tests/Treatment/TreatmentCaseEndpointsTest.php
hamedandClaude Opus 5 fc50ac4b3b feat(treatment): endpoint for a course's calendar and its recorded work
GET /api/v1/treatment-case/{uuid}/plan returns every session with a date and an
is_estimate flag, plus each session's area records — the device readings a staff
member actually logged. Until now nothing exposed either: due_at existed only
for the next session, and TreatmentCase::toArray() serialised sessions without
their areas, so 'what was done' was unreachable outside the staff panel.

Kept separate from GET /treatment-case/{uuid}; that response feeds the edit
modal, which needs neither the calendar nor the areas.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 16:12:55 +03:30

241 lines
9.4 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());
}
/** تقویم دوره: همهٔ جلسات با تاریخ، و جزئیات نواحیِ جلسهٔ انجام‌شده. */
public function testPlanReturnsEverySessionWithADate(): void
{
$s = $this->scenario();
$body = $this->authJson('GET', '/api/v1/treatment-case/' . $s['case']->getUuid() . '/plan', $s['user']);
self::assertSame(200, $this->responseCode());
self::assertSame(
$s['case']->getTotalSessions(),
count($body['data']['sessions']),
'تقویم باید همهٔ جلسات را بدهد، نه فقط آن‌هایی که سررسید نوشته دارند',
);
foreach ($body['data']['sessions'] as $row) {
self::assertArrayHasKey('planned_at', $row);
self::assertArrayHasKey('is_estimate', $row);
// نواحی باید بیایند — «چه کاری انجام شد» همین است.
self::assertArrayHasKey('areas', $row);
}
}
/** حالت مرزی: پروندهٔ محیط دیگر — تقویم هم مثل بقیه ۴۰۴ می‌گیرد نه ۴۰۳. */
public function testPlanOfAnotherTenantIs404(): void
{
$mine = $this->scenario();
$other = $this->scenario();
$this->authJson('GET', '/api/v1/treatment-case/' . $other['case']->getUuid() . '/plan', $mine['user']);
self::assertSame(404, $this->responseCode());
}
}