The holiday model was already right — national holidays global, a per-tenant override in both directions, per-doctor and per-resource exceptions — but nothing could create a national holiday. The only writer was an import command, so the calendar the whole product inherits from had no owner. Three admin-only routes give it one. POST upserts, because `date` is unique and re-sending a day should rename it rather than surface a raw database error; PATCH takes only the title, because moving a date means a different holiday. The system admin has no work environment, so the list endpoint now returns the calendar with an empty `overrides` for that role instead of the 403 `pair()` would raise — the person who maintains the calendar has to be able to read it. Both holiday tabs — the doctor's and the resource's — now open with the official calendar above their own exceptions, from one shared card rather than two copies that would drift. Each row can be opted out of with a single click, which is the existing holiday-override endpoint. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
207 lines
8.5 KiB
PHP
207 lines
8.5 KiB
PHP
<?php
|
|
|
|
namespace App\Tests\Resource;
|
|
|
|
use App\Auth\Entity\User;
|
|
use App\Clinic\Entity\Clinic;
|
|
use App\Doctor\Entity\DoctorAddress;
|
|
use App\Resource\Entity\ClinicResource;
|
|
use App\Resource\Entity\ResourceCalendar;
|
|
use App\Resource\Entity\ResourceType;
|
|
use App\Tests\ApiTestCase;
|
|
|
|
/**
|
|
* تقویم تعطیلات رسمی، از دید مدیر سیستم.
|
|
*
|
|
* تعطیل رسمی به هیچ محیطی تعلق ندارد، پس ساختنش کارِ هیچ کلینیکی نیست: یک بار مرکزی
|
|
* ثبت میشود و همهٔ محیطها — پزشک و منبع — از همان ارث میبرند. کلینیکی که آن روز باز
|
|
* است با `holiday-overrides` استثنا میزند، نه با دستکاری خودِ تقویم.
|
|
*/
|
|
class NationalHolidayAdminTest extends ApiTestCase
|
|
{
|
|
/** @return array{0: User, 1: ClinicResource} */
|
|
private function clinicWithResource(): 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('شعبهٔ مرکزی');
|
|
$this->em->persist($address);
|
|
$this->em->flush();
|
|
|
|
$type = new ResourceType('clinic', (int) $clinic->getId(), 'device', 'دستگاه');
|
|
$this->em->persist($type);
|
|
$this->em->flush();
|
|
|
|
$resource = new ClinicResource($address, $type, 'لیزر دایود');
|
|
$this->em->persist($resource);
|
|
$this->em->flush();
|
|
|
|
return [$user, $resource];
|
|
}
|
|
|
|
/** یک تعطیلِ رسمی روی روزی که منبع شیفت دارد، تا اثرش در دسترسپذیری دیده شود. */
|
|
private function shiftOn(ClinicResource $resource, int $midnight): void
|
|
{
|
|
$dayOfWeek = (int) (((int) date('w', $midnight) + 1) % 7);
|
|
$this->em->persist(new ResourceCalendar($resource, $dayOfWeek, 540, 1020));
|
|
$this->em->flush();
|
|
}
|
|
|
|
private function admin(): User
|
|
{
|
|
return $this->createUser(['ROLE_USER', 'ROLE_ADMIN']);
|
|
}
|
|
|
|
// ── ✅ موفق ──────────────────────────────────────────────────────────────
|
|
|
|
public function testAnAdminRegistersAnOfficialHoliday(): void
|
|
{
|
|
$body = $this->authJson('POST', '/api/v1/admin/national-holidays', $this->admin(), [
|
|
'jalali_date' => '1405-01-13',
|
|
'title' => 'سیزدهبدر',
|
|
]);
|
|
|
|
self::assertSame(201, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
|
self::assertSame('1405-01-13', $body['data']['jalali_date']);
|
|
self::assertSame('سیزدهبدر', $body['data']['title']);
|
|
self::assertSame(1405, $body['data']['jalali_year']);
|
|
}
|
|
|
|
/** ثبت روی `date` کلید یکتا دارد؛ دوباره فرستادن باید عنوان را عوض کند نه بشکند. */
|
|
public function testTheSameDayTwiceUpdatesTheTitle(): void
|
|
{
|
|
$admin = $this->admin();
|
|
|
|
$this->authJson('POST', '/api/v1/admin/national-holidays', $admin, [
|
|
'jalali_date' => '1405-01-13', 'title' => 'سیزده بدر',
|
|
]);
|
|
$body = $this->authJson('POST', '/api/v1/admin/national-holidays', $admin, [
|
|
'jalali_date' => '1405-01-13', 'title' => 'روز طبیعت',
|
|
]);
|
|
|
|
self::assertSame('روز طبیعت', $body['data']['title']);
|
|
self::assertCount(
|
|
1,
|
|
array_filter(
|
|
$this->authJson('GET', '/api/v1/national-holidays?year=1405', $admin)['data']['holidays'],
|
|
static fn (array $h): bool => $h['jalali_date'] === '1405-01-13',
|
|
),
|
|
);
|
|
}
|
|
|
|
public function testAHolidayCanBeRenamedAndRemoved(): void
|
|
{
|
|
$admin = $this->admin();
|
|
|
|
$uuid = $this->authJson('POST', '/api/v1/admin/national-holidays', $admin, [
|
|
'jalali_date' => '1405-02-03', 'title' => 'مناسبت',
|
|
])['data']['uuid'];
|
|
|
|
$renamed = $this->authJson('PATCH', "/api/v1/admin/national-holiday/{$uuid}", $admin, ['title' => 'مناسبت درست']);
|
|
self::assertSame('مناسبت درست', $renamed['data']['title']);
|
|
|
|
$this->authJson('DELETE', "/api/v1/admin/national-holiday/{$uuid}", $admin);
|
|
self::assertSame(200, $this->responseCode());
|
|
|
|
// فهرست سال را کامل نمیسنجیم: `national_holidays` جدول سراسری است و بین
|
|
// تستها پاک نمیشود. آنچه مهم است، نبودنِ همین uuid است.
|
|
$uuids = array_column(
|
|
$this->authJson('GET', '/api/v1/national-holidays?year=1405', $admin)['data']['holidays'],
|
|
'uuid',
|
|
);
|
|
self::assertNotContains($uuid, $uuids);
|
|
}
|
|
|
|
/** تعطیلِ ثبتشدهٔ مرکزی باید بیواسطه روز منبعِ هر محیطی را ببندد. */
|
|
public function testTheHolidayClosesThatDayForEveryResource(): void
|
|
{
|
|
[$user, $resource] = $this->clinicWithResource();
|
|
|
|
// ۱۴۰۵-۰۱-۱۳ به میلادی: ۲۰۲۶-۰۴-۰۲.
|
|
$midnight = (int) strtotime('2026-04-02 00:00:00');
|
|
$this->shiftOn($resource, $midnight);
|
|
|
|
$this->authJson('POST', '/api/v1/admin/national-holidays', $this->admin(), [
|
|
'jalali_date' => '1405-01-13', 'title' => 'سیزدهبدر',
|
|
]);
|
|
|
|
$days = $this->authJson(
|
|
'GET',
|
|
sprintf('/api/v1/resource/%s/availability?from=%d&to=%d', $resource->getUuid(), $midnight, $midnight),
|
|
$user,
|
|
)['data']['days'];
|
|
|
|
self::assertSame([], $days[0]['intervals']);
|
|
self::assertContains('national_holiday', $days[0]['reasons']);
|
|
}
|
|
|
|
// ── ⚠️ مرزی ─────────────────────────────────────────────────────────────
|
|
|
|
/** محیطی که آن روز باز است، با استثنای خودش تعطیلیِ سراسری را خنثی میکند. */
|
|
public function testAnEnvironmentCanOptOutOfTheOfficialHoliday(): void
|
|
{
|
|
[$user, $resource] = $this->clinicWithResource();
|
|
|
|
$midnight = (int) strtotime('2026-04-02 00:00:00');
|
|
$this->shiftOn($resource, $midnight);
|
|
|
|
$this->authJson('POST', '/api/v1/admin/national-holidays', $this->admin(), [
|
|
'jalali_date' => '1405-01-13', 'title' => 'سیزدهبدر',
|
|
]);
|
|
|
|
$this->authJson('POST', '/api/v1/holiday-overrides', $user, [
|
|
'date' => $midnight, 'is_working' => true, 'note' => 'شیفت اورژانس',
|
|
]);
|
|
|
|
$days = $this->authJson(
|
|
'GET',
|
|
sprintf('/api/v1/resource/%s/availability?from=%d&to=%d', $resource->getUuid(), $midnight, $midnight),
|
|
$user,
|
|
)['data']['days'];
|
|
|
|
self::assertNotSame([], $days[0]['intervals'], 'استثنای محیط بر تقویم رسمی مقدم است');
|
|
self::assertSame(480, $days[0]['total_minutes']);
|
|
}
|
|
|
|
// ── ❌ خطا ───────────────────────────────────────────────────────────────
|
|
|
|
public function testADoctorCannotRegisterANationalHoliday(): void
|
|
{
|
|
[$user] = $this->clinicWithResource();
|
|
|
|
$this->authJson('POST', '/api/v1/admin/national-holidays', $user, [
|
|
'jalali_date' => '1405-01-13', 'title' => 'سیزدهبدر',
|
|
]);
|
|
|
|
self::assertSame(403, $this->responseCode());
|
|
}
|
|
|
|
public function testAMalformedJalaliDateIsRefused(): void
|
|
{
|
|
$this->authJson('POST', '/api/v1/admin/national-holidays', $this->admin(), [
|
|
'jalali_date' => '1405/1/13', 'title' => 'سیزدهبدر',
|
|
]);
|
|
|
|
self::assertSame(422, $this->responseCode());
|
|
}
|
|
|
|
public function testTitleIsRequired(): void
|
|
{
|
|
$this->authJson('POST', '/api/v1/admin/national-holidays', $this->admin(), ['jalali_date' => '1405-01-13']);
|
|
|
|
self::assertSame(422, $this->responseCode());
|
|
}
|
|
|
|
public function testAnUnknownHolidayIs404(): void
|
|
{
|
|
$this->authJson('DELETE', '/api/v1/admin/national-holiday/00000000-0000-4000-8000-000000000000', $this->admin());
|
|
|
|
self::assertSame(404, $this->responseCode());
|
|
}
|
|
}
|