Starting a session created area records with no device, and the panel only ever read the device it never set — so every "اتمام این ناحیه" came back 422 with "دستگاه این ناحیه مشخص نیست". The backend tests passed because they sent resource_uuid explicitly; from the UI the flow was unusable end to end. The device now inherits from the appointment's resource, which the secretary already chose at booking; asking the operator again is taking one decision twice. The session screen offers a picker per area on top of that, because one session really does run bikini on an alexandrite and underarms on a diode. Treating without a device is allowed: botox is an injection, and requiring a device would make clinics invent a fake resource per injection. Sending readings with no device is still rejected — there would be no schema to validate against. A protocol whose service has no ResourceServiceOffering rows now says so in the tab where the manager is standing. It does not block booking: "no offering means any resource" is a deliberate, tested rule. But silence meant the gap surfaced only when the operator was already in front of a patient. Also adds the live timer the spec asked for, and wires slot-suggestions into the unbooked queue — the endpoint existed and tested green but no screen called it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
471 lines
22 KiB
PHP
471 lines
22 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Treatment;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\Auth\Entity\User;
|
|
use App\Auth\Repository\UserActiveContextRepository;
|
|
use App\Shared\Context\EntityContext;
|
|
use App\Clinic\Entity\Clinic;
|
|
use App\ClinicService\Entity\CatalogCategory;
|
|
use App\ClinicService\Entity\CatalogCategoryInclude;
|
|
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\SessionAreaRecord;
|
|
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 SessionExecutionTest extends ApiTestCase
|
|
{
|
|
private const LASER_SCHEMA = [
|
|
['key' => 'energy', 'label' => 'انرژی', 'type' => 'select', 'required' => true, 'sort_order' => 0, 'options' => [7, 8, 18]],
|
|
['key' => 'pulse', 'label' => 'پالس', 'type' => 'select', 'required' => true, 'sort_order' => 1, 'options' => [3, 5]],
|
|
['key' => 'shots', 'label' => 'شات', 'type' => 'number', 'required' => true, 'sort_order' => 2],
|
|
];
|
|
|
|
/**
|
|
* @return array{staffUser: User, staff: ClinicStaff, session: TreatmentSession,
|
|
* resource: ClinicResource, case: TreatmentCase, appointment: Appointment}
|
|
*/
|
|
private function scenario(int $areaCount = 2, bool $withResource = true): array
|
|
{
|
|
$owner = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
|
|
$clinic = new Clinic($owner);
|
|
$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)), 'دستگاه لیزر');
|
|
$type->setFieldSchema(self::LASER_SCHEMA);
|
|
$this->em->persist($type);
|
|
$this->em->flush();
|
|
|
|
$resource = new ClinicResource($address, $type, 'Diode Laser 808nm');
|
|
$resource->setSupervisor($doctor);
|
|
$this->em->persist($resource);
|
|
|
|
$section = new ServiceSection('clinic', (int) $clinic->getId(), 'لیزر');
|
|
$this->em->persist($section);
|
|
|
|
$parent = new CatalogCategory('clinic', (int) $clinic->getId(), 'توتال');
|
|
$this->em->persist($parent);
|
|
|
|
$areas = [];
|
|
foreach (array_slice(['بیکینی', 'زیر بغل', 'دست'], 0, $areaCount) as $name) {
|
|
$child = new CatalogCategory('clinic', (int) $clinic->getId(), $name);
|
|
$this->em->persist($child);
|
|
$this->em->flush();
|
|
$this->em->persist(new CatalogCategoryInclude($parent, $child));
|
|
$areas[] = $child;
|
|
}
|
|
|
|
$service = new ServiceItem($section, 'لیزر توتال', 10_000_000);
|
|
$service->setCatalogCategory($parent)->setDurationMinutes(30);
|
|
$this->em->persist($service);
|
|
|
|
$staffUser = $this->createUser(['ROLE_USER', 'ROLE_STAFF']);
|
|
$staff = new ClinicStaff('clinic', (int) $clinic->getId(), 'اپراتور یک');
|
|
$staff->setUser($staffUser);
|
|
$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);
|
|
foreach ($areas as $i => $category) {
|
|
$case->addArea(new TreatmentCaseArea($case, $category, $i));
|
|
}
|
|
|
|
$appointment = $this->newAppointment($doctor, $record->getUser(), time() + 3600, time() + 5400, $clinic);
|
|
if ($withResource) {
|
|
$appointment->setResource($resource);
|
|
}
|
|
$appointment->setStaff($staff);
|
|
$appointment->transitionTo(Appointment::STATUS_CONFIRMED);
|
|
$this->em->persist($appointment);
|
|
|
|
$session = new TreatmentSession($case, 1);
|
|
$session->attachAppointment($appointment);
|
|
$case->addSession($session);
|
|
$case->addSession(new TreatmentSession($case, 2));
|
|
$this->em->persist($case);
|
|
$this->em->flush();
|
|
|
|
// نقش بهتنهایی محیط نمیسازد؛ پرسنل باید محیط فعالش ست شده باشد.
|
|
static::getContainer()->get(UserActiveContextRepository::class)
|
|
->upsert($staffUser, $clinic->getUuid(), EntityContext::TYPE_CLINIC);
|
|
|
|
return [
|
|
'clinic' => $clinic,
|
|
'address' => $address,
|
|
'doctor' => $doctor,
|
|
'staffUser' => $staffUser,
|
|
'staff' => $staff,
|
|
'session' => $session,
|
|
'resource' => $resource,
|
|
'case' => $case,
|
|
'appointment' => $appointment,
|
|
];
|
|
}
|
|
|
|
/** دستگاه دومِ همان محیط — برای سنجیدن تعویض دستگاه در سطح ناحیه. */
|
|
private function otherDevice(array $s): ClinicResource
|
|
{
|
|
$type = new ResourceType('clinic', (int) $s['clinic']->getId(), 'laser_' . bin2hex(random_bytes(3)), 'لیزر دوم');
|
|
$type->setFieldSchema(self::LASER_SCHEMA);
|
|
$this->em->persist($type);
|
|
$this->em->flush();
|
|
|
|
// درخواستهای کرنل نمونههای محلی را جدا میکنند؛ دوباره از همین EM خوانده میشوند.
|
|
$address = $this->em->getRepository(DoctorAddress::class)->find($s['address']->getId());
|
|
$doctor = $this->em->getRepository(Doctor::class)->find($s['doctor']->getId());
|
|
|
|
$resource = new ClinicResource($address, $type, 'Alexandrite Laser');
|
|
$resource->setSupervisor($doctor);
|
|
$this->em->persist($resource);
|
|
$this->em->flush();
|
|
|
|
return $resource;
|
|
}
|
|
|
|
private function areaRecords(TreatmentSession $session): array
|
|
{
|
|
$records = $this->em->getRepository(SessionAreaRecord::class)->findBy(['session' => $session]);
|
|
usort($records, static fn (SessionAreaRecord $a, SessionAreaRecord $b): int
|
|
=> $a->getCaseArea()->getSortOrder() <=> $b->getCaseArea()->getSortOrder());
|
|
|
|
return $records;
|
|
}
|
|
|
|
public function testStartingASessionCreatesOneRecordPerArea(): void
|
|
{
|
|
$s = $this->scenario();
|
|
|
|
$body = $this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
|
|
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
|
self::assertSame(TreatmentSession::STATUS_IN_PROGRESS, $body['data']['status']);
|
|
self::assertNotNull($body['data']['started_at']);
|
|
self::assertCount(2, $body['data']['areas']);
|
|
self::assertSame($s['staff']->getUuid(), $body['data']['performed_by']['uuid']);
|
|
}
|
|
|
|
/** شروع دوباره نباید ناحیهٔ تکراری بسازد. */
|
|
public function testStartingTwiceIsIdempotent(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$uri = '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start';
|
|
|
|
$this->authJson('POST', $uri, $s['staffUser']);
|
|
$body = $this->authJson('POST', $uri, $s['staffUser']);
|
|
|
|
self::assertSame(200, $this->responseCode());
|
|
self::assertCount(2, $body['data']['areas']);
|
|
}
|
|
|
|
/** شروع جلسه، نوبت را به «سالن» میبرد. */
|
|
public function testStartingASessionMovesTheAppointmentToSalon(): void
|
|
{
|
|
$s = $this->scenario();
|
|
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
|
|
$this->em->clear();
|
|
$appointment = $this->em->getRepository(Appointment::class)->find($s['appointment']->getId());
|
|
self::assertSame(Appointment::STATUS_SALON, $appointment->getStatus());
|
|
}
|
|
|
|
/** دستگاه از نوبت به ارث میرسد؛ اپراتور نباید همان تصمیم را دوباره بگیرد. */
|
|
public function testAreaRecordsInheritTheAppointmentDevice(): void
|
|
{
|
|
$s = $this->scenario();
|
|
|
|
$body = $this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
|
|
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
|
foreach ($body['data']['areas'] as $area) {
|
|
self::assertSame($s['resource']->getUuid(), $area['resource']['uuid']);
|
|
}
|
|
}
|
|
|
|
/** با دستگاه ارثرسیده، اپراتور بدون فرستادن resource_uuid هم میتواند ببندد. */
|
|
public function testAnAreaCanBeCompletedWithoutResendingTheDevice(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
$record = $this->areaRecords($s['session'])[0];
|
|
|
|
$body = $this->authJson('POST', '/api/v1/dashboard/staff/session-area/' . $record->getUuid() . '/complete', $s['staffUser'], [
|
|
'parameters' => ['energy' => 18, 'pulse' => 3, 'shots' => 100],
|
|
]);
|
|
|
|
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
|
self::assertSame($s['resource']->getUuid(), $body['data']['resource']['uuid']);
|
|
}
|
|
|
|
/** اپراتور میتواند دستگاه یک ناحیه را عوض کند — بیکینی با یک دستگاه، زیر بغل با دیگری. */
|
|
public function testTheOperatorCanOverrideTheDevicePerArea(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
|
|
// دستگاه پیش از خواندن صفحه ساخته میشود، وگرنه در فهرست همان درخواست نیست.
|
|
$second = $this->otherDevice($s);
|
|
$body = $this->authJson('GET', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid(), $s['staffUser']);
|
|
|
|
self::assertContains($second->getUuid(), array_column($body['data']['devices'], 'uuid'));
|
|
|
|
$record = $this->areaRecords($s['session'])[1];
|
|
$body = $this->authJson('POST', '/api/v1/dashboard/staff/session-area/' . $record->getUuid() . '/complete', $s['staffUser'], [
|
|
'resource_uuid' => $second->getUuid(),
|
|
'parameters' => ['energy' => 8, 'pulse' => 5, 'shots' => 50],
|
|
]);
|
|
|
|
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
|
self::assertSame($second->getUuid(), $body['data']['resource']['uuid']);
|
|
}
|
|
|
|
/**
|
|
* بوتاکس تزریق است، نه دستگاه — نوبتِ بیمنبع باید بتواند ناحیهاش را ببندد.
|
|
*
|
|
* اجبارِ دستگاه یعنی کلینیک برای هر تزریق یک منبع ساختگی بسازد.
|
|
*/
|
|
public function testAnAreaWithoutAnyDeviceCanStillBeCompleted(): void
|
|
{
|
|
$s = $this->scenario(withResource: false);
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
$record = $this->areaRecords($s['session'])[0];
|
|
|
|
$body = $this->authJson('POST', '/api/v1/dashboard/staff/session-area/' . $record->getUuid() . '/complete', $s['staffUser'], [
|
|
'note' => 'تزریق انجام شد',
|
|
]);
|
|
|
|
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
|
self::assertNull($body['data']['resource']);
|
|
self::assertNull($body['data']['parameters']);
|
|
self::assertSame('تزریق انجام شد', $body['data']['note']);
|
|
}
|
|
|
|
/** ولی مقدار بدون دستگاه پذیرفته نمیشود — با چه چیزی سنجیده شود؟ */
|
|
public function testReadingsWithoutADeviceAreRejected(): void
|
|
{
|
|
$s = $this->scenario(withResource: false);
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
$record = $this->areaRecords($s['session'])[0];
|
|
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/session-area/' . $record->getUuid() . '/complete', $s['staffUser'], [
|
|
'parameters' => ['energy' => 18],
|
|
]);
|
|
|
|
self::assertSame(422, $this->responseCode());
|
|
}
|
|
|
|
public function testCompletingAnAreaStoresTheDeviceReadings(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
|
|
$record = $this->areaRecords($s['session'])[0];
|
|
|
|
$body = $this->authJson('POST', '/api/v1/dashboard/staff/session-area/' . $record->getUuid() . '/complete', $s['staffUser'], [
|
|
'resource_uuid' => $s['resource']->getUuid(),
|
|
'parameters' => ['energy' => 18, 'pulse' => 3, 'shots' => 212],
|
|
'note' => 'بدون عارضه',
|
|
]);
|
|
|
|
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
|
self::assertSame(SessionAreaRecord::STATUS_COMPLETED, $body['data']['status']);
|
|
self::assertSame(['energy' => 18, 'pulse' => 3, 'shots' => 212], $body['data']['parameters']);
|
|
self::assertSame('بدون عارضه', $body['data']['note']);
|
|
self::assertSame($s['resource']->getUuid(), $body['data']['resource']['uuid']);
|
|
self::assertNotNull($body['data']['finished_at']);
|
|
}
|
|
|
|
/** مقدار خارج از گزینههای دستگاه رد میشود. */
|
|
public function testAnOutOfRangeReadingIsRejected(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
$record = $this->areaRecords($s['session'])[0];
|
|
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/session-area/' . $record->getUuid() . '/complete', $s['staffUser'], [
|
|
'resource_uuid' => $s['resource']->getUuid(),
|
|
'parameters' => ['energy' => 99, 'pulse' => 3, 'shots' => 10],
|
|
]);
|
|
|
|
self::assertSame(422, $this->responseCode());
|
|
}
|
|
|
|
/** فیلد الزامیِ نیامده باید ۴۲۲ بدهد، نه ذخیرهٔ ناقص. */
|
|
public function testAMissingRequiredReadingIsRejected(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
$record = $this->areaRecords($s['session'])[0];
|
|
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/session-area/' . $record->getUuid() . '/complete', $s['staffUser'], [
|
|
'resource_uuid' => $s['resource']->getUuid(),
|
|
'parameters' => ['energy' => 18],
|
|
]);
|
|
|
|
self::assertSame(422, $this->responseCode());
|
|
}
|
|
|
|
public function testAnAreaCanBeSkipped(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
$record = $this->areaRecords($s['session'])[1];
|
|
|
|
$body = $this->authJson('POST', '/api/v1/dashboard/staff/session-area/' . $record->getUuid() . '/skip', $s['staffUser']);
|
|
|
|
self::assertSame(200, $this->responseCode());
|
|
self::assertSame(SessionAreaRecord::STATUS_SKIPPED, $body['data']['status']);
|
|
}
|
|
|
|
public function testAnAlreadySettledAreaCannotBeCompletedAgain(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
$record = $this->areaRecords($s['session'])[0];
|
|
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/session-area/' . $record->getUuid() . '/skip', $s['staffUser']);
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/session-area/' . $record->getUuid() . '/complete', $s['staffUser'], [
|
|
'resource_uuid' => $s['resource']->getUuid(),
|
|
'parameters' => ['energy' => 18, 'pulse' => 3, 'shots' => 10],
|
|
]);
|
|
|
|
self::assertSame(422, $this->responseCode());
|
|
}
|
|
|
|
/** اپراتور جلوی بیمار ایستاده — ناحیهٔ ناتمام نباید بستن جلسه را قفل کند. */
|
|
public function testASessionCanBeFinishedWithUnsettledAreasButReportsThem(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
|
|
$body = $this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/finish', $s['staffUser'], [
|
|
'note' => 'جلسه ناقص ماند',
|
|
]);
|
|
|
|
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
|
self::assertSame(TreatmentSession::STATUS_DONE, $body['data']['status']);
|
|
self::assertSame(2, $body['data']['unsettled_areas']);
|
|
self::assertSame('جلسه ناقص ماند', $body['data']['note']);
|
|
}
|
|
|
|
/** بستن جلسه سررسید جلسهٔ بعد را از تاریخ واقعی همین جلسه حساب میکند. */
|
|
public function testFinishingSchedulesTheNextSession(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/finish', $s['staffUser']);
|
|
|
|
$this->em->clear();
|
|
$case = $this->em->getRepository(TreatmentCase::class)->find($s['case']->getId());
|
|
$sessions = $case->getSessions()->toArray();
|
|
usort($sessions, static fn (TreatmentSession $a, TreatmentSession $b): int
|
|
=> $a->getSessionNumber() <=> $b->getSessionNumber());
|
|
|
|
self::assertSame(TreatmentSession::STATUS_DONE, $sessions[0]->getStatus());
|
|
self::assertNotNull($sessions[1]->getDueAt());
|
|
self::assertSame(
|
|
$sessions[0]->getFinishedAt() + 30 * 86400,
|
|
$sessions[1]->getDueAt(),
|
|
);
|
|
}
|
|
|
|
/** اتمام جلسه، نوبت را «انجام شد» میکند و زمانش را دست نمیزند. */
|
|
public function testFinishingCompletesTheAppointmentWithoutRewritingItsTimes(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$start = $s['appointment']->getSlotStart();
|
|
$end = $s['appointment']->getSlotEnd();
|
|
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/finish', $s['staffUser']);
|
|
|
|
$this->em->clear();
|
|
$appointment = $this->em->getRepository(Appointment::class)->find($s['appointment']->getId());
|
|
|
|
self::assertSame(Appointment::STATUS_COMPLETED, $appointment->getStatus());
|
|
self::assertSame($start, $appointment->getSlotStart());
|
|
self::assertSame($end, $appointment->getSlotEnd());
|
|
}
|
|
|
|
public function testAFinishedSessionCannotBeFinishedTwice(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$uri = '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/finish';
|
|
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $s['staffUser']);
|
|
$this->authJson('POST', $uri, $s['staffUser']);
|
|
$this->authJson('POST', $uri, $s['staffUser']);
|
|
|
|
self::assertSame(422, $this->responseCode());
|
|
}
|
|
|
|
/** جلسهٔ محیط دیگر برای این پرسنل وجود ندارد. */
|
|
public function testASessionFromAnotherTenantIsNotFound(): void
|
|
{
|
|
$mine = $this->scenario();
|
|
$other = $this->scenario();
|
|
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $other['session']->getUuid() . '/start', $mine['staffUser']);
|
|
|
|
self::assertSame(404, $this->responseCode());
|
|
}
|
|
|
|
/** نقش staff بدون ردیف فعالِ پرسنل کافی نیست. */
|
|
public function testAUserWithoutAStaffRowIsForbidden(): void
|
|
{
|
|
$s = $this->scenario();
|
|
$orphan = $this->createUser(['ROLE_USER', 'ROLE_STAFF']);
|
|
|
|
$this->authJson('POST', '/api/v1/dashboard/staff/treatment-session/' . $s['session']->getUuid() . '/start', $orphan);
|
|
|
|
self::assertContains($this->responseCode(), [403, 404]);
|
|
}
|
|
|
|
public function testTodayListsThisStaffsSessions(): void
|
|
{
|
|
$s = $this->scenario();
|
|
|
|
$body = $this->authJson('GET', '/api/v1/dashboard/staff/treatment-sessions', $s['staffUser']);
|
|
|
|
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
|
self::assertContains($s['session']->getUuid(), array_column($body['data'], 'uuid'));
|
|
}
|
|
}
|