feat(branch): branch working hours and rooms on the existing address entity
Task 01 planned a new `branches` table with `doctor_addresses.branch_id` bridging to it. That plan was wrong: the branch already exists and is called `DoctorAddress`. It carries name, address, telephone, coordinates, city/province FKs and an owner (`forDoctor` / `forClinic` + `type`), and the whole system already consumes it with exactly that meaning — `WeeklySchedule.sessions[].location_id` points at `doctor_addresses.id`, `appointment-booking-locations` calls each row a booking location, and nine CRUD endpoints plus four admin pages manage them. A parallel table would mean two sources of truth for one physical place and a branch that `location_id` never references. So no `branches` table and no duplicate branch CRUD. Only the three genuinely missing pieces: - `doctor_addresses.active` / `.timezone`, both NOT NULL with a default so existing rows need no backfill and no current behaviour changes. `active` is stored only — applying it to slot calculation is task 03, since touching `SlotCalculatorService` is off limits in this phase. - `branch_working_hours`, keyed to `doctor_addresses.id`. Minutes from midnight rather than "09:00" strings so range intersection stays arithmetic. PUT replaces all seven days; validation of the whole week runs before any DELETE, so an invalid sixth day cannot wipe the five valid ones and then answer 422. - `rooms`, with `capacity` as concurrency (a three-bed injection room is one resource with capacity 3, not three resources) and a deletion-guard iterator so tasks 02 and 07 can add reasons without editing RoomService. `BranchWorkingHours` first registered as an aggregate child of `DoctorAddress`; TenantSchemaCoverageTest rejected it correctly, because that root is itself declared global. It now carries a real tenant pair instead, derived in the constructor from the address's `type` — a total mapping, and the address is only ever listed in its own context, so nothing is hidden wrongly. RoomController checks ownership explicitly rather than trusting TenantFilter: hard isolation only applies to a *chosen* context, so a doctor who had not selected one could PATCH another clinic's room. Caught by RoomCrudTest::testForeignRoomIsNotFound, which failed with 200 before the fix. 35 tests, 97 assertions. Slot-mode frozen contract still green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Branch;
|
||||
|
||||
use App\Doctor\Entity\DoctorAddress;
|
||||
|
||||
/**
|
||||
* دو ویژگی تازهٔ شعبه (`active` / `timezone`) و فهرست شعبههای محیط جاری.
|
||||
*/
|
||||
class BranchFieldsTest extends BranchTestCase
|
||||
{
|
||||
/** ردیفهای موجود بدون backfill درست میشوند؛ هیچ رفتار فعلی عوض نمیشود. */
|
||||
public function testExistingBranchGetsSafeDefaults(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
self::assertTrue($address->isActive());
|
||||
self::assertSame(DoctorAddress::DEFAULT_TIMEZONE, $address->getTimezone());
|
||||
|
||||
$body = $this->authJson('GET', '/api/v1/branches', $user);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertTrue($body['data'][0]['active']);
|
||||
self::assertSame('Asia/Tehran', $body['data'][0]['timezone']);
|
||||
}
|
||||
|
||||
public function testListReportsWorkingHoursAndRoomCounts(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$before = $this->authJson('GET', '/api/v1/branches', $user);
|
||||
self::assertFalse($before['data'][0]['working_hours_defined']);
|
||||
self::assertSame(0, $before['data'][0]['rooms_count']);
|
||||
|
||||
$this->authJson('PUT', "/api/v1/branch/{$address->getUuid()}/working-hours", $user, [
|
||||
'days' => [1 => [['start_minute' => 540, 'end_minute' => 780]]],
|
||||
]);
|
||||
$this->authJson('POST', '/api/v1/room', $user, [
|
||||
'address_uuid' => $address->getUuid(),
|
||||
'name' => 'اتاق ۱',
|
||||
]);
|
||||
|
||||
$after = $this->authJson('GET', '/api/v1/branches', $user);
|
||||
|
||||
self::assertTrue($after['data'][0]['working_hours_defined']);
|
||||
self::assertSame(1, $after['data'][0]['rooms_count']);
|
||||
}
|
||||
|
||||
/** فقط اتاق فعال شمرده میشود — اتاق غیرفعال ظرفیت واقعی شعبه نیست. */
|
||||
public function testInactiveRoomIsNotCounted(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
$room = $this->authJson('POST', '/api/v1/room', $user, [
|
||||
'address_uuid' => $address->getUuid(),
|
||||
'name' => 'اتاق بسته',
|
||||
]);
|
||||
$this->authJson('PATCH', "/api/v1/room/{$room['data']['uuid']}", $user, ['active' => false]);
|
||||
|
||||
$body = $this->authJson('GET', '/api/v1/branches', $user);
|
||||
|
||||
self::assertSame(0, $body['data'][0]['rooms_count']);
|
||||
}
|
||||
|
||||
public function testListShowsOnlyTheCurrentContextBranches(): void
|
||||
{
|
||||
[$doctorUser, , $doctorAddress] = $this->doctorWithAddress('مطب شخصی');
|
||||
$this->clinicWithAddress('شعبهٔ کلینیک بیگانه');
|
||||
|
||||
$body = $this->authJson('GET', '/api/v1/branches', $doctorUser);
|
||||
|
||||
self::assertCount(1, $body['data']);
|
||||
self::assertSame($doctorAddress->getUuid(), $body['data'][0]['uuid']);
|
||||
}
|
||||
|
||||
public function testBranchIsDeactivated(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$body = $this->authJson('PATCH', "/api/v1/branch/{$address->getUuid()}", $user, ['active' => false]);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertFalse($body['data']['active']);
|
||||
}
|
||||
|
||||
public function testTimezoneIsUpdated(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$body = $this->authJson('PATCH', "/api/v1/branch/{$address->getUuid()}", $user, [
|
||||
'timezone' => 'Asia/Dubai',
|
||||
]);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertSame('Asia/Dubai', $body['data']['timezone']);
|
||||
}
|
||||
|
||||
/** با DateTimeZone::listIdentifiers سنجیده میشود، نه با regex. */
|
||||
public function testUnknownTimezoneIsRejected(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$body = $this->authJson('PATCH', "/api/v1/branch/{$address->getUuid()}", $user, ['timezone' => 'Tehran']);
|
||||
|
||||
self::assertSame(422, $this->responseCode());
|
||||
self::assertSame('timezone', $body['errors'][0]['field']);
|
||||
}
|
||||
|
||||
public function testForeignBranchCannotBePatched(): void
|
||||
{
|
||||
[$doctorUser] = $this->doctorWithAddress();
|
||||
[, , $foreignAddress] = $this->clinicWithAddress();
|
||||
|
||||
$this->authJson('PATCH', "/api/v1/branch/{$foreignAddress->getUuid()}", $doctorUser, ['active' => false]);
|
||||
|
||||
self::assertSame(404, $this->responseCode());
|
||||
}
|
||||
|
||||
/** شمارشها گروهیاند: تعداد کوئریها با تعداد شعبهها رشد نمیکند. */
|
||||
public function testListQueryCountDoesNotGrowWithBranches(): void
|
||||
{
|
||||
// یک کرنل برای هر دو اندازهگیری، وگرنه reboot دادهٔ کوئریها را میریزد.
|
||||
$this->client->disableReboot();
|
||||
|
||||
[$user, $doctor] = $this->doctorWithAddress();
|
||||
|
||||
$queriesForOne = $this->countQueries(
|
||||
fn () => $this->authJson('GET', '/api/v1/branches', $user)
|
||||
);
|
||||
|
||||
for ($i = 0; $i < 4; $i++) {
|
||||
$extra = DoctorAddress::forDoctor($doctor);
|
||||
$extra->setName("شعبهٔ $i");
|
||||
$this->em->persist($extra);
|
||||
}
|
||||
$this->em->flush();
|
||||
|
||||
$queriesForFive = $this->countQueries(
|
||||
fn () => $this->authJson('GET', '/api/v1/branches', $user)
|
||||
);
|
||||
|
||||
self::assertCount(5, json_decode($this->client->getResponse()->getContent(), true)['data']);
|
||||
self::assertSame($queriesForOne, $queriesForFive);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Branch;
|
||||
|
||||
use App\Auth\Entity\User;
|
||||
use App\Clinic\Entity\Clinic;
|
||||
use App\Doctor\Entity\Doctor;
|
||||
use App\Doctor\Entity\DoctorAddress;
|
||||
use App\Tests\ApiTestCase;
|
||||
|
||||
/**
|
||||
* فیکسچرهای مشترک دامنهٔ شعبه. «شعبه» همان DoctorAddress است، پس هر تست به یک آدرس
|
||||
* از محیط جاری و یک آدرس از محیط بیگانه نیاز دارد تا مرز ۴۰۴ را واقعاً بسنجد.
|
||||
*/
|
||||
abstract class BranchTestCase extends ApiTestCase
|
||||
{
|
||||
/** @return array{0: User, 1: Doctor, 2: DoctorAddress} */
|
||||
protected function doctorWithAddress(string $name = 'مطب مرکزی'): array
|
||||
{
|
||||
$user = $this->createUser(['ROLE_USER', 'ROLE_DOCTOR']);
|
||||
$doctor = new Doctor($user, 'دکتر شعبه');
|
||||
$doctor->setMobileNumber($user->getMobileNumber());
|
||||
$this->em->persist($doctor);
|
||||
$this->em->flush();
|
||||
|
||||
$address = DoctorAddress::forDoctor($doctor);
|
||||
$address->setName($name);
|
||||
$this->em->persist($address);
|
||||
$this->em->flush();
|
||||
|
||||
return [$user, $doctor, $address];
|
||||
}
|
||||
|
||||
/** @return array{0: User, 1: Clinic, 2: DoctorAddress} */
|
||||
protected function clinicWithAddress(string $name = 'شعبهٔ کلینیک'): array
|
||||
{
|
||||
$user = $this->createUser(['ROLE_USER', 'ROLE_CLINIC']);
|
||||
$clinic = new Clinic($user);
|
||||
$clinic->setName('کلینیک تست شعبه');
|
||||
$this->em->persist($clinic);
|
||||
$this->em->flush();
|
||||
|
||||
$address = DoctorAddress::forClinic($clinic->getId());
|
||||
$address->setName($name);
|
||||
$this->em->persist($address);
|
||||
$this->em->flush();
|
||||
|
||||
return [$user, $clinic, $address];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Branch;
|
||||
|
||||
use App\Branch\Entity\Room;
|
||||
|
||||
class RoomCrudTest extends BranchTestCase
|
||||
{
|
||||
/** @param array<string, mixed> $body */
|
||||
private function createRoom(\App\Auth\Entity\User $user, string $addressUuid, array $body = []): array
|
||||
{
|
||||
return $this->authJson('POST', '/api/v1/room', $user, $body + [
|
||||
'address_uuid' => $addressUuid,
|
||||
'name' => 'اتاق تزریق',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testRoomIsCreatedWithTenantPairDerivedFromTheBranch(): void
|
||||
{
|
||||
[$clinicUser, $clinic, $address] = $this->clinicWithAddress();
|
||||
|
||||
$body = $this->createRoom($clinicUser, $address->getUuid(), ['capacity' => 3, 'floor' => '2']);
|
||||
|
||||
self::assertSame(201, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
||||
self::assertSame(3, $body['data']['capacity']);
|
||||
self::assertSame('2', $body['data']['floor']);
|
||||
self::assertSame($address->getUuid(), $body['data']['address_uuid']);
|
||||
|
||||
$room = $this->em->getRepository(Room::class)->findOneBy(['uuid' => $body['data']['uuid']]);
|
||||
self::assertSame('clinic', $room->getEntityType());
|
||||
self::assertSame($clinic->getId(), $room->getEntityId());
|
||||
}
|
||||
|
||||
public function testPersonalBranchRoomBelongsToTheDoctor(): void
|
||||
{
|
||||
[$doctorUser, $doctor, $address] = $this->doctorWithAddress();
|
||||
|
||||
$body = $this->createRoom($doctorUser, $address->getUuid());
|
||||
|
||||
self::assertSame(201, $this->responseCode());
|
||||
|
||||
$room = $this->em->getRepository(Room::class)->findOneBy(['uuid' => $body['data']['uuid']]);
|
||||
self::assertSame('doctor', $room->getEntityType());
|
||||
self::assertSame($doctor->getId(), $room->getEntityId());
|
||||
}
|
||||
|
||||
public function testCapacityDefaultsToOne(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$body = $this->createRoom($user, $address->getUuid());
|
||||
|
||||
self::assertSame(1, $body['data']['capacity']);
|
||||
self::assertTrue($body['data']['active']);
|
||||
}
|
||||
|
||||
public function testZeroCapacityIsRejected(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$body = $this->createRoom($user, $address->getUuid(), ['capacity' => 0]);
|
||||
|
||||
self::assertSame(422, $this->responseCode());
|
||||
self::assertSame('capacity', $body['errors'][0]['field']);
|
||||
}
|
||||
|
||||
public function testBlankNameIsRejected(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$body = $this->authJson('POST', '/api/v1/room', $user, [
|
||||
'address_uuid' => $address->getUuid(),
|
||||
'name' => ' ',
|
||||
]);
|
||||
|
||||
self::assertSame(422, $this->responseCode());
|
||||
self::assertSame('name', $body['errors'][0]['field']);
|
||||
}
|
||||
|
||||
public function testMissingAddressUuidIsRejected(): void
|
||||
{
|
||||
[$user] = $this->doctorWithAddress();
|
||||
|
||||
$body = $this->authJson('POST', '/api/v1/room', $user, ['name' => 'اتاق']);
|
||||
|
||||
self::assertSame(422, $this->responseCode());
|
||||
self::assertSame('address_uuid', $body['errors'][0]['field']);
|
||||
}
|
||||
|
||||
/** جفت محیط از آدرس میآید، پس نمیشود اتاق را روی شعبهٔ محیط دیگر نشاند. */
|
||||
public function testRoomCannotBeCreatedOnAForeignBranch(): void
|
||||
{
|
||||
[$doctorUser] = $this->doctorWithAddress();
|
||||
[, , $foreignAddress] = $this->clinicWithAddress();
|
||||
|
||||
$this->createRoom($doctorUser, $foreignAddress->getUuid());
|
||||
|
||||
self::assertSame(404, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testRoomIsUpdated(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
$created = $this->createRoom($user, $address->getUuid());
|
||||
|
||||
$body = $this->authJson('PATCH', "/api/v1/room/{$created['data']['uuid']}", $user, [
|
||||
'name' => 'اتاق پانسمان',
|
||||
'capacity' => 2,
|
||||
'room_type' => 'پانسمان',
|
||||
'active' => false,
|
||||
]);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertSame('اتاق پانسمان', $body['data']['name']);
|
||||
self::assertSame(2, $body['data']['capacity']);
|
||||
self::assertSame('پانسمان', $body['data']['room_type']);
|
||||
self::assertFalse($body['data']['active']);
|
||||
}
|
||||
|
||||
/** رشتهٔ خالی روی فیلد اختیاری یعنی «پاک کن»، نه ذخیرهٔ رشتهٔ خالی. */
|
||||
public function testBlankOptionalFieldBecomesNull(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
$created = $this->createRoom($user, $address->getUuid(), ['room_type' => 'تزریق']);
|
||||
|
||||
$body = $this->authJson('PATCH', "/api/v1/room/{$created['data']['uuid']}", $user, ['room_type' => '']);
|
||||
|
||||
self::assertNull($body['data']['room_type']);
|
||||
}
|
||||
|
||||
public function testRoomIsDeleted(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
$created = $this->createRoom($user, $address->getUuid());
|
||||
|
||||
$this->authJson('DELETE', "/api/v1/room/{$created['data']['uuid']}", $user);
|
||||
self::assertSame(200, $this->responseCode());
|
||||
|
||||
$this->authJson('PATCH', "/api/v1/room/{$created['data']['uuid']}", $user, ['name' => 'x']);
|
||||
self::assertSame(404, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testForeignRoomIsNotFound(): void
|
||||
{
|
||||
[$clinicUser, , $clinicAddress] = $this->clinicWithAddress();
|
||||
$created = $this->createRoom($clinicUser, $clinicAddress->getUuid());
|
||||
|
||||
[$doctorUser] = $this->doctorWithAddress();
|
||||
|
||||
$this->authJson('PATCH', "/api/v1/room/{$created['data']['uuid']}", $doctorUser, ['name' => 'دزدیدهشده']);
|
||||
self::assertSame(404, $this->responseCode());
|
||||
|
||||
$this->authJson('DELETE', "/api/v1/room/{$created['data']['uuid']}", $doctorUser);
|
||||
self::assertSame(404, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testBranchRoomsAreListedForItsOwnerOnly(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
$this->createRoom($user, $address->getUuid(), ['name' => 'اتاق ۱']);
|
||||
$this->createRoom($user, $address->getUuid(), ['name' => 'اتاق ۲']);
|
||||
|
||||
$body = $this->authJson('GET', "/api/v1/branch/{$address->getUuid()}/rooms", $user);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertCount(2, $body['data']);
|
||||
|
||||
[, , $foreignAddress] = $this->clinicWithAddress();
|
||||
$this->authJson('GET', "/api/v1/branch/{$foreignAddress->getUuid()}/rooms", $user);
|
||||
self::assertSame(404, $this->responseCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace App\Tests\Branch;
|
||||
|
||||
use App\Branch\Entity\BranchWorkingHours;
|
||||
|
||||
/**
|
||||
* ساعت کاری هفتگی شعبه — GET/PUT روی /api/v1/branch/{addressUuid}/working-hours
|
||||
*/
|
||||
class WorkingHoursTest extends BranchTestCase
|
||||
{
|
||||
/** @param array<int, list<array{start_minute: int, end_minute: int}>> $days */
|
||||
private function put(\App\Auth\Entity\User $user, string $addressUuid, array $days): array
|
||||
{
|
||||
return $this->authJson('PUT', "/api/v1/branch/$addressUuid/working-hours", $user, ['days' => $days]);
|
||||
}
|
||||
|
||||
public function testEmptyBranchReportsSevenEmptyDaysAndUndefined(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$body = $this->authJson('GET', "/api/v1/branch/{$address->getUuid()}/working-hours", $user);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertFalse($body['data']['defined'], 'شعبهٔ بدون ساعت باید «تعریفنشده» باشد، نه همیشهباز');
|
||||
self::assertSame(range(0, 6), array_map('intval', array_keys($body['data']['days'])));
|
||||
foreach ($body['data']['days'] as $ranges) {
|
||||
self::assertSame([], $ranges);
|
||||
}
|
||||
}
|
||||
|
||||
public function testFullWeekIsStoredAndReadBackIdentically(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$days = [];
|
||||
foreach (range(0, 6) as $day) {
|
||||
$days[$day] = [
|
||||
['start_minute' => 540, 'end_minute' => 780], // 09:00-13:00
|
||||
['start_minute' => 960, 'end_minute' => 1200], // 16:00-20:00
|
||||
];
|
||||
}
|
||||
|
||||
$written = $this->put($user, $address->getUuid(), $days);
|
||||
self::assertSame(200, $this->responseCode(), json_encode($written, JSON_UNESCAPED_UNICODE));
|
||||
self::assertTrue($written['data']['defined']);
|
||||
|
||||
$read = $this->authJson('GET', "/api/v1/branch/{$address->getUuid()}/working-hours", $user);
|
||||
|
||||
self::assertSame($written['data']['days'], $read['data']['days']);
|
||||
self::assertSame('09:00', $read['data']['days'][0][0]['start_time']);
|
||||
self::assertSame('20:00', $read['data']['days'][0][1]['end_time']);
|
||||
self::assertSame([0, 1], array_column($read['data']['days'][0], 'sequence'));
|
||||
}
|
||||
|
||||
/** PUT قرارداد جایگزینی کامل دارد: آرایهٔ خالی یعنی شعبه بسته، نه «تغییری نده». */
|
||||
public function testEmptyPayloadClosesTheBranch(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
$this->put($user, $address->getUuid(), [3 => [['start_minute' => 600, 'end_minute' => 700]]]);
|
||||
|
||||
$body = $this->put($user, $address->getUuid(), []);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertFalse($body['data']['defined']);
|
||||
self::assertSame([], $body['data']['days'][3]);
|
||||
}
|
||||
|
||||
public function testEndBeforeStartIsRejected(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$body = $this->put($user, $address->getUuid(), [0 => [['start_minute' => 800, 'end_minute' => 800]]]);
|
||||
|
||||
self::assertSame(422, $this->responseCode());
|
||||
self::assertSame('end_minute', $body['errors'][0]['field']);
|
||||
}
|
||||
|
||||
public function testOverlappingRangesInOneDayAreRejected(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$body = $this->put($user, $address->getUuid(), [2 => [
|
||||
['start_minute' => 540, 'end_minute' => 780],
|
||||
['start_minute' => 700, 'end_minute' => 900],
|
||||
]]);
|
||||
|
||||
self::assertSame(422, $this->responseCode());
|
||||
self::assertStringContainsString('همپوشانی', $body['errors'][0]['message']);
|
||||
}
|
||||
|
||||
/** بازهٔ چسبیده مجاز است: پایان یکی = شروع بعدی، همپوشانی نیست. */
|
||||
public function testTouchingRangesAreAccepted(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$this->put($user, $address->getUuid(), [2 => [
|
||||
['start_minute' => 540, 'end_minute' => 780],
|
||||
['start_minute' => 780, 'end_minute' => 900],
|
||||
]]);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testAllDayRangeIsOneRow(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$body = $this->put($user, $address->getUuid(), [
|
||||
5 => [['start_minute' => 0, 'end_minute' => BranchWorkingHours::MINUTES_IN_DAY]],
|
||||
]);
|
||||
|
||||
self::assertSame(200, $this->responseCode());
|
||||
self::assertCount(1, $body['data']['days'][5]);
|
||||
self::assertSame('24:00', $body['data']['days'][5][0]['end_time']);
|
||||
}
|
||||
|
||||
public function testMinuteBeyondOneDayIsRejected(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$this->put($user, $address->getUuid(), [1 => [['start_minute' => 0, 'end_minute' => 1441]]]);
|
||||
|
||||
self::assertSame(422, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testInvalidDayKeyIsRejected(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$body = $this->put($user, $address->getUuid(), [7 => [['start_minute' => 0, 'end_minute' => 60]]]);
|
||||
|
||||
self::assertSame(422, $this->responseCode());
|
||||
self::assertSame('day_of_week', $body['errors'][0]['field']);
|
||||
}
|
||||
|
||||
/**
|
||||
* اتمی بودن: بازهٔ نامعتبر در روز ششم نباید روزهای درستِ قبل را پاک کند.
|
||||
* بدون اعتبارسنجیِ کاملِ پیش از DELETE، این تست هفتهٔ ذخیرهشده را خالی میبیند.
|
||||
*/
|
||||
public function testInvalidLaterDayLeavesTheStoredWeekUntouched(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$valid = [];
|
||||
foreach (range(0, 6) as $day) {
|
||||
$valid[$day] = [['start_minute' => 540, 'end_minute' => 780]];
|
||||
}
|
||||
$this->put($user, $address->getUuid(), $valid);
|
||||
|
||||
$broken = $valid;
|
||||
$broken[5] = [['start_minute' => 900, 'end_minute' => 100]];
|
||||
$this->put($user, $address->getUuid(), $broken);
|
||||
self::assertSame(422, $this->responseCode());
|
||||
|
||||
$read = $this->authJson('GET', "/api/v1/branch/{$address->getUuid()}/working-hours", $user);
|
||||
|
||||
self::assertTrue($read['data']['defined']);
|
||||
foreach (range(0, 6) as $day) {
|
||||
self::assertCount(1, $read['data']['days'][$day], "روز $day نباید پاک شده باشد");
|
||||
}
|
||||
}
|
||||
|
||||
public function testMissingDaysFieldIsRejected(): void
|
||||
{
|
||||
[$user, , $address] = $this->doctorWithAddress();
|
||||
|
||||
$body = $this->authJson('PUT', "/api/v1/branch/{$address->getUuid()}/working-hours", $user, ['x' => 1]);
|
||||
|
||||
self::assertSame(422, $this->responseCode());
|
||||
self::assertSame('days', $body['errors'][0]['field']);
|
||||
}
|
||||
|
||||
/** آدرس محیط دیگر: ۴۰۴ نه ۴۰۳ — وجود دادهٔ محیط بیگانه لو نمیرود. */
|
||||
public function testForeignBranchIsNotFound(): void
|
||||
{
|
||||
[$doctorUser] = $this->doctorWithAddress();
|
||||
[, , $foreignAddress] = $this->clinicWithAddress();
|
||||
|
||||
$this->authJson('GET', "/api/v1/branch/{$foreignAddress->getUuid()}/working-hours", $doctorUser);
|
||||
|
||||
self::assertSame(404, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testForeignBranchCannotBeWritten(): void
|
||||
{
|
||||
[$doctorUser] = $this->doctorWithAddress();
|
||||
[, , $foreignAddress] = $this->clinicWithAddress();
|
||||
|
||||
$this->put($doctorUser, $foreignAddress->getUuid(), [0 => [['start_minute' => 0, 'end_minute' => 60]]]);
|
||||
|
||||
self::assertSame(404, $this->responseCode());
|
||||
}
|
||||
|
||||
public function testClinicOwnerManagesItsOwnBranch(): void
|
||||
{
|
||||
[$clinicUser, , $address] = $this->clinicWithAddress();
|
||||
|
||||
$body = $this->put($clinicUser, $address->getUuid(), [
|
||||
0 => [['start_minute' => 480, 'end_minute' => 1020]],
|
||||
]);
|
||||
|
||||
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
||||
self::assertSame('08:00', $body['data']['days'][0][0]['start_time']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user