Confirming an appointment was supposed to create the patient's record and its session, and PatientService already knew how. Only two of the five paths that confirm an appointment ever called it, and the one that mattered most did not: a booking paid for online was confirmed inside the payment callback, which never ran the side-effects. Every Nobat724 booking therefore went unfiled — 7 confirmed appointments in dev had no session at all. The side-effects now run through AppointmentConfirmationService, which every path calls: the payment callback, both PATCH endpoints, and panel/admin bookings. Creating the record can no longer roll back a confirmation or a payment; a failure is logged and can be repaired with the new app:appointment:backfill-sessions command. Two related defects fixed along the way: - A doctor working at a clinic got two records for one appointment, one under the doctor and one under the clinic, so a single visit's revenue was counted twice. The booking context now decides, and it decides once. - That context was inferred from address_id, falling back to "the doctor's only clinic" — a guess that files an appointment under the wrong practice now that schedules are per-context. It is stored as appointments.clinic_id instead. Panel and admin bookings were left pending forever: nothing confirmed them and no payment was expected. They are created confirmed. Repeat confirmations no longer duplicate the session; an archived one still counts as filed, so archiving a mistaken visit does not resurrect it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
151 lines
5.3 KiB
PHP
151 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Patient;
|
|
|
|
use App\Appointment\Entity\Appointment;
|
|
use App\Appointment\Service\AppointmentConfirmationService;
|
|
use App\Clinic\Entity\Clinic;
|
|
use App\Doctor\Entity\Doctor;
|
|
use App\Patient\Entity\PatientRecord;
|
|
use App\Patient\Entity\PatientSession;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* Confirming an appointment files exactly one case file, in the practice where
|
|
* the booking actually happened.
|
|
*
|
|
* Two records for one appointment means a single visit's revenue is counted
|
|
* twice, and inferring the practice from the address (rather than the booking
|
|
* context) files it under the wrong one.
|
|
*/
|
|
class AutoCreateSessionOnConfirmTest extends ApiTestCase
|
|
{
|
|
private function confirmation(): AppointmentConfirmationService
|
|
{
|
|
return static::getContainer()->get(AppointmentConfirmationService::class);
|
|
}
|
|
|
|
private function makeDoctor(): Doctor
|
|
{
|
|
$doctor = new Doctor($this->createUser(['ROLE_DOCTOR']), 'دکتر آزمون');
|
|
$this->em->persist($doctor);
|
|
$this->em->flush();
|
|
|
|
return $doctor;
|
|
}
|
|
|
|
private function makeClinic(Doctor $doctor): Clinic
|
|
{
|
|
$clinic = new Clinic($this->createUser(['ROLE_CLINIC']));
|
|
$clinic->setName('کلینیک آزمون');
|
|
$clinic->getDoctors()->add($doctor);
|
|
$this->em->persist($clinic);
|
|
$this->em->flush();
|
|
|
|
return $clinic;
|
|
}
|
|
|
|
private function makeAppointment(Doctor $doctor, ?Clinic $clinic = null): Appointment
|
|
{
|
|
$appointment = new Appointment($doctor, $this->createUser(['ROLE_USER']), 1_790_000_000, 1_790_001_800);
|
|
$appointment->setClinic($clinic);
|
|
$appointment->transitionTo(Appointment::STATUS_CONFIRMED);
|
|
$this->em->persist($appointment);
|
|
$this->em->flush();
|
|
|
|
return $appointment;
|
|
}
|
|
|
|
/** @return PatientRecord[] */
|
|
private function recordsFor(Appointment $appointment): array
|
|
{
|
|
return $this->em->getRepository(PatientRecord::class)
|
|
->findBy(['user' => $appointment->getUser()]);
|
|
}
|
|
|
|
/** @return PatientSession[] */
|
|
private function sessionsFor(Appointment $appointment): array
|
|
{
|
|
return $this->em->getRepository(PatientSession::class)
|
|
->findBy(['appointment' => $appointment]);
|
|
}
|
|
|
|
public function testClinicBookingFilesOnlyTheClinicRecord(): void
|
|
{
|
|
$doctor = $this->makeDoctor();
|
|
$clinic = $this->makeClinic($doctor);
|
|
$appointment = $this->makeAppointment($doctor, $clinic);
|
|
|
|
$this->confirmation()->onConfirmed($appointment);
|
|
$this->em->flush();
|
|
|
|
$records = $this->recordsFor($appointment);
|
|
self::assertCount(1, $records, 'یک نوبت باید دقیقاً یک پرونده بسازد');
|
|
self::assertSame('clinic', $records[0]->getEntityType());
|
|
self::assertSame($clinic->getId(), $records[0]->getEntityId());
|
|
self::assertCount(1, $this->sessionsFor($appointment));
|
|
}
|
|
|
|
public function testPersonalBookingFilesOnlyTheDoctorRecord(): void
|
|
{
|
|
$doctor = $this->makeDoctor();
|
|
$this->makeClinic($doctor); // عضویت کلینیک نباید نوبت شخصی را بدزدد
|
|
$appointment = $this->makeAppointment($doctor, null);
|
|
|
|
$this->confirmation()->onConfirmed($appointment);
|
|
$this->em->flush();
|
|
|
|
$records = $this->recordsFor($appointment);
|
|
self::assertCount(1, $records);
|
|
self::assertSame('doctor', $records[0]->getEntityType());
|
|
self::assertSame($doctor->getId(), $records[0]->getEntityId());
|
|
}
|
|
|
|
public function testExistingRecordGetsAnotherSessionInsteadOfAnotherRecord(): void
|
|
{
|
|
$doctor = $this->makeDoctor();
|
|
$first = $this->makeAppointment($doctor, null);
|
|
$patient = $first->getUser();
|
|
|
|
$this->confirmation()->onConfirmed($first);
|
|
$this->em->flush();
|
|
|
|
$second = new Appointment($doctor, $patient, 1_790_100_000, 1_790_101_800);
|
|
$second->transitionTo(Appointment::STATUS_CONFIRMED);
|
|
$this->em->persist($second);
|
|
$this->em->flush();
|
|
|
|
$this->confirmation()->onConfirmed($second);
|
|
$this->em->flush();
|
|
|
|
self::assertCount(1, $this->recordsFor($first), 'بیمار قبلاً پرونده دارد؛ پروندهٔ دوم ساخته نشود');
|
|
self::assertCount(1, $this->sessionsFor($second), 'ولی مراجعهٔ جدید باید ثبت شود');
|
|
}
|
|
|
|
public function testConfirmingTwiceDoesNotDuplicateTheSession(): void
|
|
{
|
|
$doctor = $this->makeDoctor();
|
|
$appointment = $this->makeAppointment($doctor, null);
|
|
|
|
$this->confirmation()->onConfirmed($appointment);
|
|
$this->em->flush();
|
|
$this->confirmation()->onConfirmed($appointment);
|
|
$this->em->flush();
|
|
|
|
self::assertCount(1, $this->sessionsFor($appointment));
|
|
}
|
|
|
|
public function testDayLevelReserveIsNotFiled(): void
|
|
{
|
|
$doctor = $this->makeDoctor();
|
|
$appointment = $this->makeAppointment($doctor, null);
|
|
$appointment->rescheduleTo(1_790_000_000, 1_790_001_800, true);
|
|
$this->em->flush();
|
|
|
|
$this->confirmation()->onConfirmed($appointment);
|
|
$this->em->flush();
|
|
|
|
self::assertCount(0, $this->sessionsFor($appointment), 'نوبت رزروِ روز-محور ساعت ندارد؛ مراجعه نمیسازد');
|
|
}
|
|
}
|