Files
clinicpro/tests/Treatment/StaffSessionQueueTest.php
T
hamedandClaude Opus 5 38ea477429 fix(treatment): make the staff panel a queue instead of an assignment list
findTodayForStaff filtered on appointments.staff, and no booking path ever sets
that column. The result was a staff panel that was empty for every operator, in
every tenant, no matter how much work the day held.

Today's sessions now reach an operator three ways: the session they already
claimed (performedBy), the appointment a secretary pre-assigned to them, or
unclaimed work whose protocol names them. A protocol with no staff list means
everyone may perform it — the same "no rows is not a restriction" rule
ResourceServiceOffering already uses.

The query is also tenant-scoped, which the old one was not: it relied on
appointments.staff being a same-tenant row rather than saying so.

Verified against the dev database: the operator behind 09128726723 now gets
both of today's sessions on "لیزر توتال", which the old query returned none of.

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

212 lines
8.4 KiB
PHP

<?php
namespace App\Tests\Treatment;
use App\Appointment\Entity\Appointment;
use App\Appointment\Service\AppointmentConfirmationService;
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\Staff\Entity\ClinicStaff;
use App\Tests\ApiTestCase;
use App\Treatment\Entity\TreatmentProtocol;
use App\Treatment\Entity\TreatmentProtocolStaff;
use App\Treatment\Entity\TreatmentProtocolStep;
use App\Treatment\Entity\TreatmentSession;
use App\Treatment\Repository\TreatmentSessionRepository;
/**
* «جلسات امروزِ من» یک صف است، نه فهرستِ اختصاص‌یافته.
*
* هیچ مسیرِ نوبت‌دهی اپراتور را ست نمی‌کند، پس فیلتر کردن صرفاً روی
* `appointment.staff` یعنی پنل پرسنل همیشه خالی است. اپراتورِ مجاز باید کارِ
* بی‌صاحبِ امروز را ببیند و برش دارد؛ کارِ برداشته‌شده یا از پیش اختصاص‌یافته فقط
* مالِ صاحبش است.
*/
class StaffSessionQueueTest extends ApiTestCase
{
private function repo(): TreatmentSessionRepository
{
return static::getContainer()->get(TreatmentSessionRepository::class);
}
/**
* @return array{Clinic, ServiceItem, Doctor, TreatmentProtocol}
*/
private function clinicWithProtocol(bool $namesStaff, ?ClinicStaff $allowed = null): array
{
$doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر صف');
$this->em->persist($doctor);
$clinic = new Clinic($this->createUser(['ROLE_CLINIC']));
$clinic->setName('کلینیک صف ' . uniqid());
$clinic->getDoctors()->add($doctor);
$this->em->persist($clinic);
$this->em->flush();
$section = new ServiceSection('clinic', (int) $clinic->getId(), 'لیزر');
$this->em->persist($section);
// بدون دستهٔ کاتالوگ، نواحیِ پرونده جایی برای snapshot ندارند و باز کردن
// پرونده خطا می‌دهد — همان چیزی که TreatmentCaseOpener انتظار دارد.
$category = new CatalogCategory('clinic', (int) $clinic->getId(), 'صورت');
$this->em->persist($category);
$service = new ServiceItem($section, 'لیزر صورت', 3_000_000);
$service->setCatalogCategory($category);
$this->em->persist($service);
$this->em->flush();
$protocol = new TreatmentProtocol($service);
$this->em->persist($protocol);
$protocol->replaceSteps([
new TreatmentProtocolStep($protocol, 1, 0),
new TreatmentProtocolStep($protocol, 2, 15),
]);
if ($namesStaff && $allowed !== null) {
$protocol->replaceAllowedStaff([new TreatmentProtocolStaff($protocol, $allowed)]);
}
$this->em->flush();
return [$clinic, $service, $doctor, $protocol];
}
private function staffIn(Clinic $clinic, string $name): ClinicStaff
{
$staff = new ClinicStaff('clinic', (int) $clinic->getId(), $name);
$this->em->persist($staff);
$this->em->flush();
return $staff;
}
/** نوبتِ امروز روی سرویسِ پروتکل‌دار، قطعی‌شده تا پرونده و جلسه ساخته شود. */
private function bookToday(Clinic $clinic, Doctor $doctor, ServiceItem $service, ?ClinicStaff $staff = null): Appointment
{
$start = strtotime('today midnight') + 10 * 3600;
$appointment = $this->newAppointment(
$doctor,
$this->createUser(['ROLE_USER']),
$start,
$start + 1800,
$clinic,
);
$appointment->addServiceItem($service);
if ($staff !== null) {
$appointment->setStaff($staff);
}
$appointment->transitionTo(Appointment::STATUS_CONFIRMED);
$this->em->persist($appointment);
$this->em->flush();
static::getContainer()->get(AppointmentConfirmationService::class)->onConfirmed($appointment);
return $appointment;
}
private function today(ClinicStaff $staff): array
{
return $this->repo()->findTodayForStaff(
$staff,
strtotime('today midnight'),
strtotime('tomorrow midnight') - 1,
);
}
public function testAnAllowedOperatorSeesUnclaimedWork(): void
{
[$clinic, $service, $doctor] = $this->clinicWithProtocol(namesStaff: false);
$operator = $this->staffIn($clinic, 'اپراتور مجاز');
$this->clinicWithProtocolNamesStaff($service, $operator);
$appointment = $this->bookToday($clinic, $doctor, $service);
$sessions = $this->today($operator);
self::assertCount(1, $sessions);
self::assertSame($appointment->getId(), $sessions[0]->getAppointment()?->getId());
}
/** پرسنلی که پروتکل نامش را نبرده، کارِ بی‌صاحب را هم نباید ببیند. */
public function testAnOperatorOutsideTheProtocolSeesNothing(): void
{
[$clinic, $service, $doctor] = $this->clinicWithProtocol(namesStaff: false);
$allowed = $this->staffIn($clinic, 'اپراتور مجاز');
$other = $this->staffIn($clinic, 'اپراتور دیگر');
$this->clinicWithProtocolNamesStaff($service, $allowed);
$this->bookToday($clinic, $doctor, $service);
self::assertCount(1, $this->today($allowed));
self::assertSame([], $this->today($other));
}
/** پروتکلِ بدون فهرست پرسنل یعنی همه مجازند، نه هیچ‌کس. */
public function testAProtocolWithNoStaffListIsOpenToEveryone(): void
{
[$clinic, $service, $doctor] = $this->clinicWithProtocol(namesStaff: false);
$anyone = $this->staffIn($clinic, 'هر اپراتوری');
$this->bookToday($clinic, $doctor, $service);
self::assertCount(1, $this->today($anyone));
}
/** اختصاص از قبل توسط منشی: جلسه فقط مالِ همان نفر است. */
public function testPreAssignedWorkIsHiddenFromOthers(): void
{
[$clinic, $service, $doctor] = $this->clinicWithProtocol(namesStaff: false);
$mine = $this->staffIn($clinic, 'اپراتور من');
$theirs = $this->staffIn($clinic, 'اپراتور آن‌ها');
$this->bookToday($clinic, $doctor, $service, staff: $mine);
self::assertCount(1, $this->today($mine));
self::assertSame([], $this->today($theirs));
}
/** جلسه‌ای که یکی برداشته از صفِ بقیه بیرون می‌رود. */
public function testClaimedWorkLeavesTheSharedQueue(): void
{
[$clinic, $service, $doctor] = $this->clinicWithProtocol(namesStaff: false);
$first = $this->staffIn($clinic, 'اپراتور اول');
$second = $this->staffIn($clinic, 'اپراتور دوم');
$this->bookToday($clinic, $doctor, $service);
self::assertCount(1, $this->today($first));
self::assertCount(1, $this->today($second));
/** @var TreatmentSession $session */
$session = $this->today($first)[0];
$session->setPerformedBy($first);
$this->em->flush();
self::assertCount(1, $this->today($first));
self::assertSame([], $this->today($second));
}
/** پرسنلِ کلینیک دیگر هیچ‌وقت این صف را نمی‌بیند. */
public function testTheQueueIsScopedToTheTenant(): void
{
[$clinic, $service, $doctor] = $this->clinicWithProtocol(namesStaff: false);
$this->bookToday($clinic, $doctor, $service);
[$otherClinic] = $this->clinicWithProtocol(namesStaff: false);
$outsider = $this->staffIn($otherClinic, 'پرسنل کلینیک دیگر');
self::assertSame([], $this->today($outsider));
}
/** فهرست پرسنلِ پروتکل بعد از ساختِ سرویس ست می‌شود؛ همان مسیر تنظیمات. */
private function clinicWithProtocolNamesStaff(ServiceItem $service, ClinicStaff $staff): void
{
$protocol = $this->em->getRepository(TreatmentProtocol::class)->findOneBy(['serviceItem' => $service]);
self::assertNotNull($protocol);
$protocol->replaceAllowedStaff([new TreatmentProtocolStaff($protocol, $staff)]);
$this->em->flush();
}
}