Files
clinicpro/tests/Resource/NationalHolidayAdminTest.php
hamedandClaude Opus 5 f144401dd3 feat(holidays): one official calendar, inherited everywhere
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>
2026-08-02 15:39:48 +03:30

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());
}
}