- Removed package consumption flags and related properties from PriceQuote. - Eliminated unused domain event publishing for policies and waitlist in Schedule. - Cleaned up BookingEngineSeeder by removing package and policy related logic. - Updated SeedScenariosCommand to reflect removal of policies from output. - Dropped policy, package, treatment course, cancellation, waitlist, and domain event tables in migration. - Removed domain event assertions from tests related to resource blocking.
515 lines
22 KiB
PHP
515 lines
22 KiB
PHP
<?php
|
||
|
||
namespace App\Tests\Appointment;
|
||
|
||
use App\Appointment\Availability\Entity\ResourceOccupancy;
|
||
use App\Appointment\Booking\Entity\AppointmentHold;
|
||
use App\Appointment\Booking\Entity\OccupancyBucket;
|
||
use App\Auth\Entity\User;
|
||
use App\Clinic\Entity\Clinic;
|
||
use App\ClinicService\Entity\ServiceItem;
|
||
use App\ClinicService\Entity\ServiceSection;
|
||
use App\Doctor\Entity\Doctor;
|
||
use App\Doctor\Entity\DoctorAddress;
|
||
use App\Resource\Entity\ResourceType;
|
||
use App\Tests\ApiTestCase;
|
||
use Doctrine\DBAL\DriverManager;
|
||
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||
|
||
/**
|
||
* رزرو موقت و ثبت نهایی چندمنبعی — بند ۱۱ مستند.
|
||
*/
|
||
class HoldAndBookTest extends ApiTestCase
|
||
{
|
||
private const TEHRAN = 'Asia/Tehran';
|
||
|
||
private function nextSaturdayAt(int $hour): int
|
||
{
|
||
return (new \DateTimeImmutable('next saturday', new \DateTimeZone(self::TEHRAN)))
|
||
->setTime($hour, 0)
|
||
->getTimestamp();
|
||
}
|
||
|
||
/**
|
||
* اشغالهای یک منبع مشخص.
|
||
*
|
||
* `db_test` هرگز ریست نمیشود و تستهای دیگر هم روی همین ساعت ردیف میسازند، پس
|
||
* پرسوجو حتماً باید به منبعِ همین تست محدود شود — وگرنه تست، دادهٔ دیگران را
|
||
* میشمارد.
|
||
*
|
||
* @return list<ResourceOccupancy>
|
||
*/
|
||
private function occupancyOf(string $resourceUuid, ?int $startsAt = null): array
|
||
{
|
||
$qb = $this->em->createQueryBuilder()
|
||
->select('o')
|
||
->from(ResourceOccupancy::class, 'o')
|
||
->join('o.resource', 'r')
|
||
->where('r.uuid = :uuid')
|
||
->setParameter('uuid', $resourceUuid)
|
||
->orderBy('o.startsAt', 'ASC');
|
||
|
||
if ($startsAt !== null) {
|
||
$qb->andWhere('o.startsAt = :start')->setParameter('start', $startsAt);
|
||
}
|
||
|
||
return $qb->getQuery()->getResult();
|
||
}
|
||
|
||
/** @return array{user: User, doctor: Doctor, section: ServiceSection, address: DoctorAddress} */
|
||
private function clinic(): array
|
||
{
|
||
$user = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
|
||
$clinic = new Clinic($user);
|
||
$clinic->setName('کلینیک رزرو');
|
||
$this->em->persist($clinic);
|
||
$this->em->flush();
|
||
|
||
$doctorUser = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
|
||
$doctor = new Doctor($doctorUser, 'دکتر رزرو');
|
||
$this->em->persist($doctor);
|
||
|
||
$section = new ServiceSection('clinic', $clinic->getId(), 'لیزر');
|
||
$this->em->persist($section);
|
||
|
||
$address = DoctorAddress::forClinic($clinic->getId());
|
||
$address->setName('شعبهٔ مرکزی');
|
||
$this->em->persist($address);
|
||
$this->em->flush();
|
||
|
||
return ['user' => $user, 'doctor' => $doctor, 'section' => $section, 'address' => $address];
|
||
}
|
||
|
||
private function service(ServiceSection $section, string $name, int $solo): ServiceItem
|
||
{
|
||
$section = $this->em->getRepository(ServiceSection::class)->find($section->getId());
|
||
|
||
$item = new ServiceItem($section, $name);
|
||
$item->setSoloDurationMinutes($solo);
|
||
$this->em->persist($item);
|
||
$this->em->flush();
|
||
|
||
return $item;
|
||
}
|
||
|
||
private function type(DoctorAddress $address, string $code, string $name): ResourceType
|
||
{
|
||
$type = new ResourceType($address->tenantEntityType(), $address->tenantEntityId(), $code, $name);
|
||
$this->em->persist($type);
|
||
$this->em->flush();
|
||
|
||
return $type;
|
||
}
|
||
|
||
/** @param array<string, mixed> $extra */
|
||
private function resource(User $user, DoctorAddress $address, ResourceType $type, string $name, array $extra = []): array
|
||
{
|
||
$created = $this->authJson('POST', '/api/v1/resource', $user, $extra + [
|
||
'address_uuid' => $address->getUuid(),
|
||
'type_uuid' => $type->getUuid(),
|
||
'name' => $name,
|
||
]);
|
||
self::assertSame(201, $this->responseCode(), json_encode($created, JSON_UNESCAPED_UNICODE));
|
||
|
||
$this->authJson('PUT', "/api/v1/resource/{$created['data']['uuid']}/calendar", $user, [
|
||
'days' => array_fill_keys(range(0, 6), [['start_minute' => 0, 'end_minute' => 1440]]),
|
||
]);
|
||
|
||
return $created['data'];
|
||
}
|
||
|
||
/** @param list<array<string, mixed>> $segments */
|
||
private function segments(User $user, ServiceItem $service, array $segments): void
|
||
{
|
||
$this->authJson('PUT', "/api/v1/service-item/{$service->getUuid()}/segments", $user, ['segments' => $segments]);
|
||
self::assertSame(200, $this->responseCode());
|
||
}
|
||
|
||
/** @param array<string, list<string>> $assignment */
|
||
private function hold(User $user, ServiceItem $service, DoctorAddress $address, int $start, array $assignment): array
|
||
{
|
||
return $this->authJson('POST', '/api/v1/appointment-hold', $user, [
|
||
'service_uuid' => $service->getUuid(),
|
||
'branch_uuid' => $address->getUuid(),
|
||
'start' => $start,
|
||
'assignment' => $assignment,
|
||
]);
|
||
}
|
||
|
||
/** یک سرویس بیستدقیقهای که فقط یک اتاق میخواهد. */
|
||
private function simpleSetup(int $capacity = 1): array
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'ویزیت', 20);
|
||
$room = $this->type($c['address'], 'room', 'اتاق');
|
||
$created = $this->resource($c['user'], $c['address'], $room, 'اتاق ۱', ['capacity' => $capacity]);
|
||
|
||
$this->segments($c['user'], $service, [
|
||
['sequence' => 1, 'name' => 'ویزیت', 'duration_minutes' => 20, 'requirements' => [['type_uuid' => $room->getUuid()]]],
|
||
]);
|
||
|
||
return $c + ['service' => $service, 'room' => $created];
|
||
}
|
||
|
||
public function testHoldCreatesOccupancyRowsPerSegmentAndResource(): void
|
||
{
|
||
$s = $this->simpleSetup();
|
||
$start = $this->nextSaturdayAt(10);
|
||
|
||
$body = $this->hold($s['user'], $s['service'], $s['address'], $start, ['room' => [$s['room']['uuid']]]);
|
||
|
||
self::assertSame(201, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
||
self::assertNotEmpty($body['data']['hold_uuid']);
|
||
self::assertGreaterThan(time(), $body['data']['expires_at']);
|
||
|
||
$this->em->clear();
|
||
$rows = $this->occupancyOf($s['room']['uuid'], $start);
|
||
|
||
self::assertCount(1, $rows);
|
||
self::assertSame(ResourceOccupancy::STATUS_HOLD, $rows[0]->getStatus());
|
||
self::assertSame('ویزیت', $rows[0]->getSegmentName());
|
||
}
|
||
|
||
/** بعد از رزرو موقت، همان زمان دیگر پیشنهاد نمیشود. */
|
||
public function testHeldTimeDisappearsFromAvailability(): void
|
||
{
|
||
$s = $this->simpleSetup();
|
||
$start = $this->nextSaturdayAt(10);
|
||
$saturday = $this->nextSaturdayAt(0);
|
||
|
||
$before = $this->authJson('POST', '/api/v1/appointment-availability', $s['user'], [
|
||
'service_uuid' => $s['service']->getUuid(),
|
||
'branch_uuid' => $s['address']->getUuid(),
|
||
'from' => $saturday,
|
||
'to' => $saturday,
|
||
'step_minutes' => 20,
|
||
]);
|
||
self::assertContains($start, array_column($before['data']['slots'], 'start'));
|
||
|
||
$this->hold($s['user'], $s['service'], $s['address'], $start, ['room' => [$s['room']['uuid']]]);
|
||
self::assertSame(201, $this->responseCode());
|
||
|
||
$after = $this->authJson('POST', '/api/v1/appointment-availability', $s['user'], [
|
||
'service_uuid' => $s['service']->getUuid(),
|
||
'branch_uuid' => $s['address']->getUuid(),
|
||
'from' => $saturday,
|
||
'to' => $saturday,
|
||
'step_minutes' => 20,
|
||
]);
|
||
|
||
self::assertNotContains($start, array_column($after['data']['slots'], 'start'));
|
||
}
|
||
|
||
/**
|
||
* ⭐ تست همزمانی — اصلیترین تست این تسک.
|
||
*
|
||
* دو رزرو روی همان منبع و همان بازه: دقیقاً یکی ۲۰۱ و دیگری ۴۰۹. تضمین از قید
|
||
* یکتای دیتابیس میآید نه از بررسی کد، و همین تست آن قید را مستقیم هم میسنجد.
|
||
*/
|
||
public function testSecondHoldOnTheSameResourceAndIntervalIsRejected(): void
|
||
{
|
||
$s = $this->simpleSetup();
|
||
$start = $this->nextSaturdayAt(11);
|
||
|
||
$first = $this->hold($s['user'], $s['service'], $s['address'], $start, ['room' => [$s['room']['uuid']]]);
|
||
self::assertSame(201, $this->responseCode(), json_encode($first, JSON_UNESCAPED_UNICODE));
|
||
|
||
$second = $this->hold($s['user'], $s['service'], $s['address'], $start, ['room' => [$s['room']['uuid']]]);
|
||
|
||
self::assertSame(409, $this->responseCode());
|
||
self::assertSame('ERR_SLOT_TAKEN', $second['errors'][0]['code']);
|
||
}
|
||
|
||
/**
|
||
* خودِ قید دیتابیس، مستقل از هر کدِ PHP: نوشتن مستقیم دو ردیف یکسان باید با
|
||
* نقض کلید یکتا رد شود. اگر این تست بشکند، یعنی تضمین فقط در کد بوده است.
|
||
*/
|
||
public function testDatabaseItselfRefusesADuplicateBucketSeat(): void
|
||
{
|
||
$s = $this->simpleSetup();
|
||
$start = $this->nextSaturdayAt(12);
|
||
|
||
$this->hold($s['user'], $s['service'], $s['address'], $start, ['room' => [$s['room']['uuid']]]);
|
||
self::assertSame(201, $this->responseCode());
|
||
|
||
$this->em->clear();
|
||
$occupancy = $this->occupancyOf($s['room']['uuid'], $start)[0];
|
||
$bucket = OccupancyBucket::bucketsFor($start, $start + 1200)[0];
|
||
|
||
// اتصال جدا، نه اتصال مشترکِ تست: هم به «درخواست دیگر» وفادارتر است و هم
|
||
// خطای عمدی، EntityManager مشترک را برای تستهای بعدی خراب نمیکند.
|
||
$connection = DriverManager::getConnection(
|
||
$this->em->getConnection()->getParams(),
|
||
);
|
||
|
||
$this->expectException(UniqueConstraintViolationException::class);
|
||
|
||
try {
|
||
$connection->insert('resource_occupancy_buckets', [
|
||
'resource_id' => $occupancy->getResource()->getId(),
|
||
'occupancy_id' => $occupancy->getId(),
|
||
'bucket_at' => $bucket,
|
||
'seat' => 0,
|
||
]);
|
||
} finally {
|
||
$connection->close();
|
||
}
|
||
}
|
||
|
||
/** ظرفیت ۳: سه رزرو همزمان میگذرند، چهارمی ۴۰۹. */
|
||
public function testCapacityThreeAllowsThreeConcurrentHoldsAndRefusesTheFourth(): void
|
||
{
|
||
$s = $this->simpleSetup(capacity: 3);
|
||
$start = $this->nextSaturdayAt(13);
|
||
|
||
foreach (range(1, 3) as $n) {
|
||
$this->hold($s['user'], $s['service'], $s['address'], $start, ['room' => [$s['room']['uuid']]]);
|
||
self::assertSame(201, $this->responseCode(), "رزرو $n باید بگذرد");
|
||
}
|
||
|
||
$fourth = $this->hold($s['user'], $s['service'], $s['address'], $start, ['room' => [$s['room']['uuid']]]);
|
||
|
||
self::assertSame(409, $this->responseCode());
|
||
self::assertSame('ERR_SLOT_TAKEN', $fourth['errors'][0]['code']);
|
||
}
|
||
|
||
public function testConfirmMarksEverythingBooked(): void
|
||
{
|
||
$s = $this->simpleSetup();
|
||
$start = $this->nextSaturdayAt(14);
|
||
|
||
$hold = $this->hold($s['user'], $s['service'], $s['address'], $start, ['room' => [$s['room']['uuid']]]);
|
||
self::assertSame(201, $this->responseCode());
|
||
|
||
$body = $this->authJson('POST', '/api/v1/appointment-confirm', $s['user'], [
|
||
'hold_uuid' => $hold['data']['hold_uuid'],
|
||
'doctor_uuid' => $s['doctor']->getUuid(),
|
||
]);
|
||
|
||
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
||
self::assertNotEmpty($body['data']['appointment_uuid']);
|
||
|
||
$this->em->clear();
|
||
$rows = $this->occupancyOf($s['room']['uuid'], $start);
|
||
|
||
self::assertSame(ResourceOccupancy::STATUS_BOOKED, $rows[0]->getStatus());
|
||
self::assertNotNull($rows[0]->getAppointmentId());
|
||
}
|
||
|
||
/** رزرو منقضی ثبت نمیشود و آن زمان دوباره آزاد است. */
|
||
public function testExpiredHoldCannotBeConfirmed(): void
|
||
{
|
||
$s = $this->simpleSetup();
|
||
$start = $this->nextSaturdayAt(15);
|
||
|
||
$hold = $this->hold($s['user'], $s['service'], $s['address'], $start, ['room' => [$s['room']['uuid']]]);
|
||
self::assertSame(201, $this->responseCode());
|
||
|
||
// مهلت را به عقب میبریم — همان کاری که گذر زمان میکند.
|
||
$this->em->clear();
|
||
$entity = $this->em->getRepository(AppointmentHold::class)->findOneBy(['uuid' => $hold['data']['hold_uuid']]);
|
||
$this->em->getConnection()->update(
|
||
'appointment_holds',
|
||
['expires_at' => time() - 60],
|
||
['id' => $entity->getId()],
|
||
);
|
||
|
||
$body = $this->authJson('POST', '/api/v1/appointment-confirm', $s['user'], [
|
||
'hold_uuid' => $hold['data']['hold_uuid'],
|
||
'doctor_uuid' => $s['doctor']->getUuid(),
|
||
]);
|
||
|
||
self::assertSame(409, $this->responseCode());
|
||
self::assertSame('ERR_HOLD_EXPIRED', $body['errors'][0]['code']);
|
||
}
|
||
|
||
/** آزادسازی زودهنگام: زمان دوباره در جستجو ظاهر میشود. */
|
||
public function testReleasingAHoldFreesTheTimeAgain(): void
|
||
{
|
||
$s = $this->simpleSetup();
|
||
$start = $this->nextSaturdayAt(16);
|
||
$saturday = $this->nextSaturdayAt(0);
|
||
|
||
$hold = $this->hold($s['user'], $s['service'], $s['address'], $start, ['room' => [$s['room']['uuid']]]);
|
||
self::assertSame(201, $this->responseCode());
|
||
|
||
$this->authJson('DELETE', "/api/v1/appointment-hold/{$hold['data']['hold_uuid']}", $s['user']);
|
||
self::assertSame(200, $this->responseCode());
|
||
|
||
$after = $this->authJson('POST', '/api/v1/appointment-availability', $s['user'], [
|
||
'service_uuid' => $s['service']->getUuid(),
|
||
'branch_uuid' => $s['address']->getUuid(),
|
||
'from' => $saturday,
|
||
'to' => $saturday,
|
||
'step_minutes' => 20,
|
||
]);
|
||
|
||
self::assertContains($start, array_column($after['data']['slots'], 'start'));
|
||
}
|
||
|
||
/** رزرو کاربر دیگر ۴۰۴ میگیرد، نه ۴۰۳ — وجودش نباید لو برود. */
|
||
public function testAnotherUsersHoldIsNotFound(): void
|
||
{
|
||
$s = $this->simpleSetup();
|
||
$start = $this->nextSaturdayAt(17);
|
||
|
||
$hold = $this->hold($s['user'], $s['service'], $s['address'], $start, ['room' => [$s['room']['uuid']]]);
|
||
self::assertSame(201, $this->responseCode());
|
||
|
||
// مزاحم باید محیط معتبر خودش را داشته باشد، وگرنه ۴۰۳ «محیط انتخاب نشده»
|
||
// میگیرد و تست چیزی را که ادعا میکند نمیسنجد.
|
||
$intruder = $this->clinic()['user'];
|
||
|
||
$this->authJson('POST', '/api/v1/appointment-confirm', $intruder, [
|
||
'hold_uuid' => $hold['data']['hold_uuid'],
|
||
'doctor_uuid' => $s['doctor']->getUuid(),
|
||
]);
|
||
|
||
self::assertSame(404, $this->responseCode());
|
||
}
|
||
|
||
/** نیازمندیای که در `assignment` منبع ندارد → ۴۲۲، پیش از هر رزروی. */
|
||
public function testAssignmentMissingARoleIsRejected(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'لیزر', 20);
|
||
$room = $this->type($c['address'], 'room', 'اتاق');
|
||
$operator = $this->type($c['address'], 'operator', 'اپراتور');
|
||
|
||
$roomRes = $this->resource($c['user'], $c['address'], $room, 'اتاق ۱');
|
||
$this->resource($c['user'], $c['address'], $operator, 'اپراتور ۱');
|
||
|
||
$this->segments($c['user'], $service, [
|
||
['sequence' => 1, 'name' => 'لیزر', 'duration_minutes' => 20, 'requirements' => [
|
||
['type_uuid' => $room->getUuid()], ['type_uuid' => $operator->getUuid()],
|
||
]],
|
||
]);
|
||
|
||
$body = $this->hold($c['user'], $service, $c['address'], $this->nextSaturdayAt(18), [
|
||
'room' => [$roomRes['uuid']], // اپراتور نیامده
|
||
]);
|
||
|
||
self::assertSame(422, $this->responseCode());
|
||
self::assertStringContainsString('اپراتور', $body['errors'][0]['message']);
|
||
}
|
||
|
||
/**
|
||
* ⭐ آزادسازی ظرفیت حفظ میشود: اپراتور در بخش «انتظار» ردیف اشغال **ندارد**.
|
||
*/
|
||
public function testOperatorHasNoOccupancyDuringTheWaitingSegment(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'لیزر', 20);
|
||
$room = $this->type($c['address'], 'room', 'اتاق');
|
||
$operator = $this->type($c['address'], 'operator', 'اپراتور');
|
||
|
||
$roomRes = $this->resource($c['user'], $c['address'], $room, 'اتاق ۱');
|
||
$opRes = $this->resource($c['user'], $c['address'], $operator, 'اپراتور ۱');
|
||
|
||
$this->segments($c['user'], $service, [
|
||
['sequence' => 1, 'name' => 'بیحسی', 'duration_minutes' => 5, 'requirements' => [['type_uuid' => $room->getUuid()], ['type_uuid' => $operator->getUuid()]]],
|
||
['sequence' => 2, 'name' => 'انتظار', 'duration_minutes' => 30, 'requirements' => [['type_uuid' => $room->getUuid()]]],
|
||
['sequence' => 3, 'name' => 'لیزر', 'duration_minutes' => 20, 'requirements' => [['type_uuid' => $room->getUuid()], ['type_uuid' => $operator->getUuid()]]],
|
||
]);
|
||
|
||
$start = $this->nextSaturdayAt(19);
|
||
|
||
$body = $this->hold($c['user'], $service, $c['address'], $start, [
|
||
'room' => [$roomRes['uuid']],
|
||
'operator' => [$opRes['uuid']],
|
||
]);
|
||
self::assertSame(201, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
||
|
||
$this->em->clear();
|
||
|
||
$operatorRows = $this->occupancyOf($opRes['uuid']);
|
||
|
||
self::assertCount(2, $operatorRows, 'دو بخش، نه یک بازهٔ پیوستهٔ ۵۵ دقیقهای');
|
||
self::assertSame($start, $operatorRows[0]->getStartsAt());
|
||
self::assertSame($start + 5 * 60, $operatorRows[0]->getEndsAt());
|
||
self::assertSame($start + 35 * 60, $operatorRows[1]->getStartsAt());
|
||
|
||
// اتاق برعکس: هر سه بخش را میگیرد.
|
||
self::assertCount(3, $this->occupancyOf($roomRes['uuid']));
|
||
}
|
||
|
||
/** رزرو نیمهکاره نمیماند: اگر منبع دوم جا نداشت، اولی هم آزاد میشود. */
|
||
public function testPartialHoldIsRolledBack(): void
|
||
{
|
||
$c = $this->clinic();
|
||
$service = $this->service($c['section'], 'لیزر', 20);
|
||
$room = $this->type($c['address'], 'room', 'اتاق');
|
||
$operator = $this->type($c['address'], 'operator', 'اپراتور');
|
||
|
||
$roomRes = $this->resource($c['user'], $c['address'], $room, 'اتاق ۱');
|
||
$opRes = $this->resource($c['user'], $c['address'], $operator, 'اپراتور ۱');
|
||
|
||
$this->segments($c['user'], $service, [
|
||
['sequence' => 1, 'name' => 'لیزر', 'duration_minutes' => 20, 'requirements' => [
|
||
['type_uuid' => $room->getUuid()], ['type_uuid' => $operator->getUuid()],
|
||
]],
|
||
]);
|
||
|
||
$start = $this->nextSaturdayAt(20);
|
||
|
||
// اپراتور را از پیش میگیریم تا رزرو دوم روی او شکست بخورد.
|
||
$onlyOperator = $this->service($c['section'], 'کار اپراتور', 20);
|
||
$this->segments($c['user'], $onlyOperator, [
|
||
['sequence' => 1, 'name' => 'کار', 'duration_minutes' => 20, 'requirements' => [['type_uuid' => $operator->getUuid()]]],
|
||
]);
|
||
$this->hold($c['user'], $onlyOperator, $c['address'], $start, ['operator' => [$opRes['uuid']]]);
|
||
self::assertSame(201, $this->responseCode());
|
||
|
||
$body = $this->hold($c['user'], $service, $c['address'], $start, [
|
||
'room' => [$roomRes['uuid']],
|
||
'operator' => [$opRes['uuid']],
|
||
]);
|
||
self::assertSame(409, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
||
|
||
// اتاق نباید قفل مانده باشد.
|
||
$this->em->clear();
|
||
$roomRows = $this->em->createQuery(
|
||
'SELECT o FROM App\Appointment\Availability\Entity\ResourceOccupancy o
|
||
JOIN o.resource r WHERE r.uuid = :uuid AND o.status IN (:blocking)'
|
||
)
|
||
->setParameter('uuid', $roomRes['uuid'])
|
||
->setParameter('blocking', ResourceOccupancy::BLOCKING_STATUSES)
|
||
->getResult();
|
||
|
||
self::assertSame([], $roomRows, 'اتاق نباید از رزروِ شکستخورده قفل بماند');
|
||
}
|
||
|
||
/**
|
||
* جابهجایی از بیرون شبیه «لغو + رزرو» است، ولی برای بیمار یک اتفاق است.
|
||
*
|
||
* رویداد `AppointmentRescheduled` همین را میگوید؛ بدون آن، مصرفکنندهای که فقط
|
||
* `AppointmentCancelled` را میشنود برای بیماری که هنوز نوبت دارد پیام لغو میفرستد.
|
||
*/
|
||
public function testRebookingMovesTheAppointmentAndRecordsTheEvent(): void
|
||
{
|
||
$s = $this->simpleSetup();
|
||
$start = $this->nextSaturdayAt(14);
|
||
|
||
$first = $this->hold($s['user'], $s['service'], $s['address'], $start, ['room' => [$s['room']['uuid']]]);
|
||
$book = $this->authJson('POST', '/api/v1/appointment-confirm', $s['user'], [
|
||
'hold_uuid' => $first['data']['hold_uuid'],
|
||
'doctor_uuid' => $s['doctor']->getUuid(),
|
||
]);
|
||
self::assertSame(200, $this->responseCode(), json_encode($book, JSON_UNESCAPED_UNICODE));
|
||
|
||
$newStart = $start + 2 * 3600;
|
||
$second = $this->hold($s['user'], $s['service'], $s['address'], $newStart, ['room' => [$s['room']['uuid']]]);
|
||
self::assertSame(201, $this->responseCode());
|
||
|
||
$moved = $this->authJson(
|
||
'POST',
|
||
"/api/v1/appointment/{$book['data']['appointment_uuid']}/rebook",
|
||
$s['user'],
|
||
['hold_uuid' => $second['data']['hold_uuid']],
|
||
);
|
||
|
||
self::assertSame(200, $this->responseCode(), json_encode($moved, JSON_UNESCAPED_UNICODE));
|
||
self::assertSame($newStart, $moved['data']['starts_at']);
|
||
}
|
||
}
|