Files
clinicpro/tests/Treatment/SessionExecutionTest.php
T
hamedandClaude Opus 5 a182b05e1f feat(treatment): skip reasons and reopening for session areas
Skipping an area recorded only that it was skipped. Why it was skipped is
clinical history — the next session needs to read it — so `skip` now takes an
optional note, the same way completing an area already did, and the panel asks
for it inline instead of firing on the first click.

An operator finds out mid-laser that they closed the wrong area, and until now
had to carry that mistake to the end of the session. `reopen` puts a settled
area — completed or skipped — back to in_progress and clears finished_at,
keeping the recorded parameters and note so they can be seen and overwritten.
It stops at the same boundary everything else in this domain stops at: once the
session is finished the record is history, and reopening it is 409.

Also drops /admin/my-services. The staff role has one job — today's sessions —
and the dashboard already lists the services they may perform, so the page was
a second place to read the same list. Route, page, sidebar entry and the two
links to it are gone; the services stat card is no longer a link because it no
longer has a destination.

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

539 lines
25 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 testSkippingAnAreaStoresTheReason(): 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'],
['note' => 'پوست این ناحیه تحریک بود'],
);
self::assertSame(200, $this->responseCode());
self::assertSame('پوست این ناحیه تحریک بود', $body['data']['note']);
}
/** اپراتور وسط لیزر می‌فهمد اشتباه زده؛ نباید تا پایان جلسه با آن بماند. */
public function testASettledAreaCanBeReopenedWhileTheSessionIsOpen(): 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']);
$body = $this->authJson('POST', '/api/v1/dashboard/staff/session-area/' . $record->getUuid() . '/reopen', $s['staffUser']);
self::assertSame(200, $this->responseCode());
self::assertNotSame(SessionAreaRecord::STATUS_SKIPPED, $body['data']['status']);
self::assertNull($body['data']['finished_at']);
// و بعد از باز شدن، دوباره قابل ثبت است.
$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(200, $this->responseCode());
}
/** بعد از بستن جلسه، رکورد سابقه است و ویرایشش یعنی بازنویسی سند. */
public function testAnAreaCannotBeReopenedAfterTheSessionIsFinished(): 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/treatment-session/' . $s['session']->getUuid() . '/finish', $s['staffUser']);
$this->authJson('POST', '/api/v1/dashboard/staff/session-area/' . $record->getUuid() . '/reopen', $s['staffUser']);
self::assertSame(409, $this->responseCode());
}
/** ناحیهٔ باز چیزی برای برگرداندن ندارد. */
public function testAnOpenAreaCannotBeReopened(): 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() . '/reopen', $s['staffUser']);
self::assertSame(422, $this->responseCode());
}
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'));
}
}