Files
clinicpro/tests/ClinicService/CategoryManagementEndpointTest.php
T
hamedandClaude Opus 5 03d8d7d68a feat(catalog): manage global categories from settings
Categories are the taxonomy both services and resources select from, but
until now they could only be reached through the service catalog, so every
environment ended up with its own spelling of "whole body".

- Settings > Categories page: global CRUD plus the "includes" edge
- POST/GET/DELETE /api/v1/service-category/{uuid}/includes — a DAG, kept
  separate from `parent` because "hand" sits under both "whole body" and
  "upper limb"; a cycle is refused with 422
- PUT /api/v1/resource/{uuid}/categories — full replacement, and a category
  from another environment is rejected explicitly since the uuid arrives in
  the request body where TenantFilter does not reach

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-02 12:58:26 +03:30

171 lines
6.8 KiB
PHP

<?php
namespace App\Tests\ClinicService;
use App\Auth\Entity\User;
use App\Clinic\Entity\Clinic;
use App\ClinicService\Entity\CatalogCategory;
use App\Doctor\Entity\DoctorAddress;
use App\Resource\Entity\ClinicResource;
use App\Resource\Entity\ResourceType;
use App\Tests\ApiTestCase;
/**
* مدیریت دستهٔ **سراسری** از تنظیمات: ساخت، یال «شامل بودن»، و تخصیص به منبع.
*
* دسته یک بار در سطح کلینیک تعریف می‌شود و همه‌جا — سرویس و منبع — از همان استفاده
* می‌کنند. صفحهٔ سرویس و منبع فقط **انتخاب** می‌کنند.
*/
class CategoryManagementEndpointTest extends ApiTestCase
{
/** @return array{0: User, 1: Clinic, 2: 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();
$address = DoctorAddress::forClinic($clinic->getId());
$address->setName('شعبهٔ مرکزی');
$this->em->persist($address);
$this->em->flush();
return [$user, $clinic, $address];
}
private function category(User $user, string $name): string
{
$body = $this->authJson('POST', '/api/v1/service-category', $user, ['name' => $name]);
self::assertSame(201, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
return $body['data']['uuid'];
}
// ── ✅ موفق ──────────────────────────────────────────────────────────────
public function testACategoryCanIncludeAnother(): void
{
[$user] = $this->clinic();
$whole = $this->category($user, 'تمام بدن');
$hand = $this->category($user, 'دست');
$this->authJson('POST', "/api/v1/service-category/{$whole}/includes", $user, [
'child_category_uuid' => $hand,
]);
self::assertSame(201, $this->responseCode());
$body = $this->authJson('GET', "/api/v1/service-category/{$whole}/includes", $user);
self::assertCount(1, $body['data']);
self::assertSame('دست', $body['data'][0]['name']);
}
public function testTheSameEdgeTwiceIsNotAnError(): void
{
[$user] = $this->clinic();
$whole = $this->category($user, 'تمام بدن');
$hand = $this->category($user, 'دست');
$this->authJson('POST', "/api/v1/service-category/{$whole}/includes", $user, ['child_category_uuid' => $hand]);
// تکرار همان یال idempotent است: کاربر دوبار کلیک می‌کند، خطا معنایی ندارد.
$this->authJson('POST', "/api/v1/service-category/{$whole}/includes", $user, ['child_category_uuid' => $hand]);
self::assertSame(200, $this->responseCode());
self::assertCount(1, $this->authJson('GET', "/api/v1/service-category/{$whole}/includes", $user)['data']);
}
public function testAnEdgeCanBeRemoved(): void
{
[$user] = $this->clinic();
$whole = $this->category($user, 'تمام بدن');
$hand = $this->category($user, 'دست');
$this->authJson('POST', "/api/v1/service-category/{$whole}/includes", $user, ['child_category_uuid' => $hand]);
$this->authJson('DELETE', "/api/v1/service-category/{$whole}/includes/{$hand}", $user);
self::assertSame(200, $this->responseCode());
self::assertSame([], $this->authJson('GET', "/api/v1/service-category/{$whole}/includes", $user)['data']);
}
public function testAResourceTakesTheGlobalCategories(): void
{
[$user, $clinic, $address] = $this->clinic();
$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();
$hand = $this->category($user, 'دست');
$foot = $this->category($user, 'پا');
$body = $this->authJson('PUT', "/api/v1/resource/{$resource->getUuid()}/categories", $user, [
'category_uuids' => [$hand, $foot],
]);
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
self::assertCount(2, $body['data']['categories']);
// جایگزینی کامل: فهرست خالی یعنی هیچ دسته‌ای.
$emptied = $this->authJson('PUT', "/api/v1/resource/{$resource->getUuid()}/categories", $user, ['category_uuids' => []]);
self::assertSame([], $emptied['data']['categories']);
}
// ── ❌ خطا ───────────────────────────────────────────────────────────────
public function testACycleIsRefusedByTheEndpoint(): void
{
[$user] = $this->clinic();
$whole = $this->category($user, 'تمام بدن');
$hand = $this->category($user, 'دست');
$this->authJson('POST', "/api/v1/service-category/{$whole}/includes", $user, ['child_category_uuid' => $hand]);
// «دست شامل تمام بدن» حلقه می‌بندد.
$body = $this->authJson('POST', "/api/v1/service-category/{$hand}/includes", $user, ['child_category_uuid' => $whole]);
self::assertSame(422, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
}
public function testACategoryFromAnotherEnvironmentCannotBeAssigned(): void
{
[$user, $clinic, $address] = $this->clinic();
[$otherUser] = $this->clinic();
$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();
$foreign = $this->category($otherUser, 'دستهٔ کلینیک دیگر');
$this->authJson('PUT', "/api/v1/resource/{$resource->getUuid()}/categories", $user, [
'category_uuids' => [$foreign],
]);
self::assertSame(422, $this->responseCode());
}
public function testTheBodyMustCarryTheField(): void
{
[$user] = $this->clinic();
$whole = $this->category($user, 'تمام بدن');
$this->authJson('POST', "/api/v1/service-category/{$whole}/includes", $user, ['wrong' => 'x']);
self::assertSame(422, $this->responseCode());
}
}