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>
212 lines
8.4 KiB
PHP
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();
|
|
}
|
|
}
|