feat(services): pick a global category from the service page
The service page could not say which category a service belongs to, so the
containment edges defined in settings had nothing to match against.
A Categories tab now selects one — and only selects. Creating, renaming and
deleting stay in Settings > Categories: if every page could create one,
"whole body" would exist three times with three spellings and the
includes edge would stop catching anything.
PATCH /api/v1/service-item/{uuid} carries the choice as
catalog_category_uuid. Absent field leaves the current category alone, null
clears it, and a category from another environment is refused with 422 —
the uuid arrives in the request body where TenantFilter does not reach.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
|||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import SearchableSelect from './ui/SearchableSelect';
|
||||||
|
import { api, ApiError, type ApiResponse } from '../lib/api';
|
||||||
|
import { useCatalogCategories, useCategoryIncludes } from '../hooks/useCatalogCategories';
|
||||||
|
import type { CatalogCategory } from '../types';
|
||||||
|
|
||||||
|
type Flat = { uuid: string; name: string; depth: number };
|
||||||
|
|
||||||
|
function flatten(nodes: CatalogCategory[], depth = 0): Flat[] {
|
||||||
|
return nodes.flatMap((n) => [
|
||||||
|
{ uuid: n.uuid, name: n.name, depth },
|
||||||
|
...flatten(n.children ?? [], depth + 1),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* دستهٔ یک سرویس — فقط **انتخاب** از کاتالوگ سراسری.
|
||||||
|
*
|
||||||
|
* ساخت، ویرایش و حذف دسته عمداً اینجا نیست؛ فقط در «تنظیمات ← دستهبندیها». اگر هر
|
||||||
|
* صفحهای بتواند دسته بسازد، «تمام بدن» چند بار با املاهای مختلف ساخته میشود و یال
|
||||||
|
* «شامل بودن» دیگر چیزی را نمیگیرد.
|
||||||
|
*/
|
||||||
|
export default function ServiceCategoryTab({ serviceUuid, categoryUuid, canEdit }: {
|
||||||
|
serviceUuid: string;
|
||||||
|
categoryUuid: string | null;
|
||||||
|
canEdit: boolean;
|
||||||
|
}) {
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const { tree, loading } = useCatalogCategories();
|
||||||
|
const [selected, setSelected] = useState<string | null>(categoryUuid);
|
||||||
|
|
||||||
|
useEffect(() => setSelected(categoryUuid), [categoryUuid]);
|
||||||
|
|
||||||
|
const all = useMemo(() => flatten(tree), [tree]);
|
||||||
|
const { includes } = useCategoryIncludes(selected ?? undefined);
|
||||||
|
|
||||||
|
const save = useMutation({
|
||||||
|
mutationFn: (uuid: string | null) =>
|
||||||
|
api.patch<ApiResponse<unknown>>(`/api/v1/service-item/${serviceUuid}`, { catalog_category_uuid: uuid }),
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success('دستهبندی سرویس ذخیره شد');
|
||||||
|
qc.invalidateQueries({ queryKey: ['service-item', serviceUuid] });
|
||||||
|
},
|
||||||
|
onError: (e) => toast.error(e instanceof ApiError ? e.message : 'ذخیرهٔ دستهبندی ناموفق بود'),
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card card-pad" style={{ display: 'grid', gap: 14 }}>
|
||||||
|
<p style={{ margin: 0, fontSize: 12.5, color: 'var(--text-3)', lineHeight: 1.9 }}>
|
||||||
|
دستهبندی سراسری کلینیک است و اینجا فقط انتخاب میشود. ساخت، ویرایش و حذف فقط در{' '}
|
||||||
|
<Link to="/admin/service-categories" style={{ color: 'var(--primary)' }}>تنظیمات ← دستهبندیها</Link>{' '}
|
||||||
|
انجام میشود.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<span style={{ fontSize: 13, color: 'var(--text-3)' }}>در حال بارگذاری...</span>
|
||||||
|
) : all.length === 0 ? (
|
||||||
|
<p style={{ fontSize: 13, color: 'var(--text-3)', margin: 0 }}>
|
||||||
|
هنوز هیچ دستهبندیای تعریف نشده است.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: 'grid', gap: 6, maxWidth: 380 }}>
|
||||||
|
<label style={{ fontSize: 12, color: 'var(--text-2)' }}>دستهبندی این سرویس</label>
|
||||||
|
<SearchableSelect
|
||||||
|
options={all.map((c) => ({ value: c.uuid, label: '— '.repeat(c.depth) + c.name }))}
|
||||||
|
value={selected}
|
||||||
|
onChange={(v) => setSelected(v ? String(v) : null)}
|
||||||
|
placeholder="بدون دستهبندی"
|
||||||
|
isDisabled={!canEdit}
|
||||||
|
isClearable
|
||||||
|
height={38}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{selected && includes.length > 0 && (
|
||||||
|
<div style={{ display: 'grid', gap: 6 }}>
|
||||||
|
<span style={{ fontSize: 12, color: 'var(--text-2)' }}>این دسته شامل:</span>
|
||||||
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
||||||
|
{includes.map((c) => (
|
||||||
|
<span key={c.uuid} className="badge gray" style={{ fontSize: 11 }}>{c.name}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: 12, color: 'var(--text-3)' }}>
|
||||||
|
انتخاب همزمان این سرویس با سرویسی از این زیرمجموعهها هنگام رزرو رد میشود.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{canEdit && (
|
||||||
|
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn primary"
|
||||||
|
disabled={save.isPending || selected === categoryUuid}
|
||||||
|
onClick={() => save.mutate(selected)}
|
||||||
|
>
|
||||||
|
{save.isPending ? 'در حال ذخیره...' : 'ذخیره'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ import ServiceInsuranceModal from '../components/ServiceInsuranceModal';
|
|||||||
import ServiceItemFormModal from '../components/ServiceItemFormModal';
|
import ServiceItemFormModal from '../components/ServiceItemFormModal';
|
||||||
import ServiceGroupsTab from '../components/ServiceGroupsTab';
|
import ServiceGroupsTab from '../components/ServiceGroupsTab';
|
||||||
import ServiceSegmentsTab from '../components/ServiceSegmentsTab';
|
import ServiceSegmentsTab from '../components/ServiceSegmentsTab';
|
||||||
|
import ServiceCategoryTab from '../components/ServiceCategoryTab';
|
||||||
|
|
||||||
interface Tariff {
|
interface Tariff {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
@@ -55,6 +56,7 @@ const TABS = [
|
|||||||
{ id: 'insurance', label: 'بیمهها' },
|
{ id: 'insurance', label: 'بیمهها' },
|
||||||
{ id: 'groups', label: 'گروهها و آیتمها' },
|
{ id: 'groups', label: 'گروهها و آیتمها' },
|
||||||
{ id: 'segments', label: 'بخشهای نوبت' },
|
{ id: 'segments', label: 'بخشهای نوبت' },
|
||||||
|
{ id: 'categories',label: 'دستهبندیها' },
|
||||||
{ id: 'goods', label: 'کالاهای مرتبط' },
|
{ id: 'goods', label: 'کالاهای مرتبط' },
|
||||||
{ id: 'history', label: 'لاگ تغییرات' },
|
{ id: 'history', label: 'لاگ تغییرات' },
|
||||||
] as const;
|
] as const;
|
||||||
@@ -543,6 +545,13 @@ function ServiceDetailPageInner() {
|
|||||||
{tab === 'insurance' && <InsuranceTab item={item} onManage={() => setInsuranceOpen(true)} canUpdate={canUpdate} />}
|
{tab === 'insurance' && <InsuranceTab item={item} onManage={() => setInsuranceOpen(true)} canUpdate={canUpdate} />}
|
||||||
{tab === 'groups' && <ServiceGroupsTab serviceUuid={item.uuid} canEdit={canUpdate} />}
|
{tab === 'groups' && <ServiceGroupsTab serviceUuid={item.uuid} canEdit={canUpdate} />}
|
||||||
{tab === 'segments' && <ServiceSegmentsTab serviceUuid={item.uuid} canEdit={canUpdate} />}
|
{tab === 'segments' && <ServiceSegmentsTab serviceUuid={item.uuid} canEdit={canUpdate} />}
|
||||||
|
{tab === 'categories' && (
|
||||||
|
<ServiceCategoryTab
|
||||||
|
serviceUuid={item.uuid}
|
||||||
|
categoryUuid={item.catalog_category_uuid ?? null}
|
||||||
|
canEdit={canUpdate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{tab === 'goods' && <GoodsTab item={item} onEdit={() => setEditOpen(true)} canUpdate={canUpdate} />}
|
{tab === 'goods' && <GoodsTab item={item} onEdit={() => setEditOpen(true)} canUpdate={canUpdate} />}
|
||||||
{tab === 'history' && <HistoryTab item={item} />}
|
{tab === 'history' && <HistoryTab item={item} />}
|
||||||
|
|
||||||
|
|||||||
@@ -719,6 +719,8 @@ export interface ServiceItem {
|
|||||||
/** نوع خدمت (سرپایی/بستری) — درصد پوشش بیمه بر همین اساس انتخاب میشود. */
|
/** نوع خدمت (سرپایی/بستری) — درصد پوشش بیمه بر همین اساس انتخاب میشود. */
|
||||||
service_category?: string;
|
service_category?: string;
|
||||||
service_category_label?: string;
|
service_category_label?: string;
|
||||||
|
/** دستهٔ کاتالوگ سراسری — با `service_category` (enum بیمهای) اشتباه نشود. */
|
||||||
|
catalog_category_uuid?: string | null;
|
||||||
duration_minutes?: number | null;
|
duration_minutes?: number | null;
|
||||||
bookable?: boolean;
|
bookable?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -551,6 +551,23 @@ override فقط وقتی اعمال میشود که `branch_uuid` به `valid
|
|||||||
|
|
||||||
خطاها: `404` دستهٔ نامعتبر · `422` نبودِ `child_category_uuid`، حلقه، یا دستهٔ محیط دیگر.
|
خطاها: `404` دستهٔ نامعتبر · `422` نبودِ `child_category_uuid`، حلقه، یا دستهٔ محیط دیگر.
|
||||||
|
|
||||||
|
### دستهٔ سرویس — `catalog_category_uuid` روی `POST/PATCH /api/v1/service-item[/{uuid}]`
|
||||||
|
|
||||||
|
سرویس **یک** دسته دارد (`ManyToOne`)، برخلاف منبع که چند دسته میگیرد؛ اندپوینت جدایی
|
||||||
|
ندارد و همان `PATCH` سرویس این فیلد را میپذیرد.
|
||||||
|
|
||||||
|
| مقدار | اثر |
|
||||||
|
|---|---|
|
||||||
|
| `"<uuid>"` | دسته ست میشود |
|
||||||
|
| `null` یا `""` | دسته پاک میشود |
|
||||||
|
| فیلد در بدنه نباشد | دستهٔ فعلی دستنخورده میماند |
|
||||||
|
|
||||||
|
پاسخ ۲۰۰ کلِ سرویس است و `catalog_category_uuid` را برمیگرداند. **۴۲۲:** دستهٔ محیط دیگر
|
||||||
|
(`{"code":"ERR_VALIDATION_002","message":"دستهبندی یافت نشد","field":"catalog_category_uuid"}`).
|
||||||
|
|
||||||
|
در پنل، تب «دستهبندیها»ی `/admin/service/{uuid}` فقط همین انتخاب را انجام میدهد؛ دکمهٔ
|
||||||
|
ساخت دسته عمداً آنجا نیست.
|
||||||
|
|
||||||
### دستهٔ منبع — `PUT /api/v1/resource/{uuid}/categories`
|
### دستهٔ منبع — `PUT /api/v1/resource/{uuid}/categories`
|
||||||
|
|
||||||
مستند کامل در [`resource.md`](resource.md).
|
مستند کامل در [`resource.md`](resource.md).
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use App\ClinicService\Entity\Tariff;
|
|||||||
use App\Clinic\Security\ClinicDoctorAccessChecker;
|
use App\Clinic\Security\ClinicDoctorAccessChecker;
|
||||||
use App\Secretary\Security\SecretaryAccessChecker;
|
use App\Secretary\Security\SecretaryAccessChecker;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use App\ClinicService\Repository\CatalogCategoryRepository;
|
||||||
use App\ClinicService\Repository\ServiceItemAuditLogRepository;
|
use App\ClinicService\Repository\ServiceItemAuditLogRepository;
|
||||||
use App\ClinicService\Repository\ServiceItemRepository;
|
use App\ClinicService\Repository\ServiceItemRepository;
|
||||||
use App\ClinicService\Repository\ServiceSectionRepository;
|
use App\ClinicService\Repository\ServiceSectionRepository;
|
||||||
@@ -55,6 +56,7 @@ class ClinicServiceController extends BaseController
|
|||||||
private readonly RequestStack $requestStack,
|
private readonly RequestStack $requestStack,
|
||||||
private readonly SecretaryAccessChecker $secretaryAccess,
|
private readonly SecretaryAccessChecker $secretaryAccess,
|
||||||
private readonly ClinicDoctorAccessChecker $clinicDoctorAccess,
|
private readonly ClinicDoctorAccessChecker $clinicDoctorAccess,
|
||||||
|
private readonly CatalogCategoryRepository $catalogCategoryRepo,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/** گِیتِ ترکیبی: منشی + پزشکِ عضوِ کلینیک (هرکدام فقط نقشِ خودش را محدود میکند). */
|
/** گِیتِ ترکیبی: منشی + پزشکِ عضوِ کلینیک (هرکدام فقط نقشِ خودش را محدود میکند). */
|
||||||
@@ -120,6 +122,36 @@ class ClinicServiceController extends BaseController
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* دستهٔ کاتالوگ سرویس — از دستهٔ **سراسری** محیط انتخاب میشود.
|
||||||
|
*
|
||||||
|
* uuid از بدنهٔ درخواست میآید و `TenantFilter` رویش اعمال نمیشود، پس محیط دسته صریحاً
|
||||||
|
* با محیط کاربر مقایسه میشود. `null` یعنی «بدون دسته»، پس با `array_key_exists`
|
||||||
|
* تشخیص داده میشود نه `isset`.
|
||||||
|
*/
|
||||||
|
private function applyCatalogCategory(ServiceItem $item, array $data, string $entityType, ?int $entityId): ?JsonResponse
|
||||||
|
{
|
||||||
|
if (!array_key_exists('catalog_category_uuid', $data)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$uuid = $data['catalog_category_uuid'];
|
||||||
|
if ($uuid === null || $uuid === '') {
|
||||||
|
$item->setCatalogCategory(null);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$category = $this->catalogCategoryRepo->findByUuid((string) $uuid);
|
||||||
|
if ($category === null || $category->getEntityType() !== $entityType || $category->getEntityId() !== $entityId) {
|
||||||
|
return $this->error(ErrorCodes::ERR_VALIDATION_002, 'دستهبندی یافت نشد', 422, 'catalog_category_uuid');
|
||||||
|
}
|
||||||
|
|
||||||
|
$item->setCatalogCategory($category);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/** نوع خدمت (سرپایی/بستری) را ست میکند؛ مقدار نامعتبر ۴۲۲ میدهد. */
|
/** نوع خدمت (سرپایی/بستری) را ست میکند؛ مقدار نامعتبر ۴۲۲ میدهد. */
|
||||||
private function applyServiceCategory(ServiceItem $item, array $data): ?JsonResponse
|
private function applyServiceCategory(ServiceItem $item, array $data): ?JsonResponse
|
||||||
{
|
{
|
||||||
@@ -380,6 +412,9 @@ class ClinicServiceController extends BaseController
|
|||||||
if (($categoryError = $this->applyServiceCategory($item, $data)) !== null) {
|
if (($categoryError = $this->applyServiceCategory($item, $data)) !== null) {
|
||||||
return $categoryError;
|
return $categoryError;
|
||||||
}
|
}
|
||||||
|
if (($catalogError = $this->applyCatalogCategory($item, $data, $entityType, $entityId)) !== null) {
|
||||||
|
return $catalogError;
|
||||||
|
}
|
||||||
$packageError = $this->applyInventoryPackage($item, $data, $entityType, $entityId);
|
$packageError = $this->applyInventoryPackage($item, $data, $entityType, $entityId);
|
||||||
if ($packageError !== null) {
|
if ($packageError !== null) {
|
||||||
return $packageError;
|
return $packageError;
|
||||||
@@ -435,6 +470,9 @@ class ClinicServiceController extends BaseController
|
|||||||
if (($categoryError = $this->applyServiceCategory($item, $data)) !== null) {
|
if (($categoryError = $this->applyServiceCategory($item, $data)) !== null) {
|
||||||
return $categoryError;
|
return $categoryError;
|
||||||
}
|
}
|
||||||
|
if (($catalogError = $this->applyCatalogCategory($item, $data, $entityType, $entityId)) !== null) {
|
||||||
|
return $catalogError;
|
||||||
|
}
|
||||||
$packageError = $this->applyInventoryPackage($item, $data, $entityType, $entityId);
|
$packageError = $this->applyInventoryPackage($item, $data, $entityType, $entityId);
|
||||||
if ($packageError !== null) {
|
if ($packageError !== null) {
|
||||||
return $packageError;
|
return $packageError;
|
||||||
|
|||||||
@@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\ClinicService;
|
||||||
|
|
||||||
|
use App\Auth\Entity\User;
|
||||||
|
use App\ClinicService\Entity\ServiceItem;
|
||||||
|
use App\ClinicService\Entity\ServiceSection;
|
||||||
|
use App\Doctor\Entity\Doctor;
|
||||||
|
use App\Tests\ApiTestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* تب «دستهبندیها»ی صفحهٔ سرویس فقط **انتخاب** میکند و همین `PATCH` پشتش است.
|
||||||
|
*
|
||||||
|
* دسته ساخته نمیشود؛ ساختش فقط از «تنظیمات ← دستهبندیها» است. اینجا اثبات میشود که
|
||||||
|
* انتخاب مینشیند، پاک میشود، و دستهٔ محیط دیگر رد میشود.
|
||||||
|
*/
|
||||||
|
class ServiceItemCatalogCategoryTest extends ApiTestCase
|
||||||
|
{
|
||||||
|
/** @return array{0: User, 1: ServiceItem} */
|
||||||
|
private function doctorWithItem(): array
|
||||||
|
{
|
||||||
|
$owner = $this->createUser(['ROLE_DOCTOR']);
|
||||||
|
$doctor = new Doctor($owner, 'دکتر دستهبندی');
|
||||||
|
$this->em->persist($doctor);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
$section = new ServiceSection('doctor', $doctor->getId(), 'لیزر');
|
||||||
|
$item = new ServiceItem($section, 'لیزر دست');
|
||||||
|
$item->setPriceRials(5_000_000);
|
||||||
|
$this->em->persist($section);
|
||||||
|
$this->em->persist($item);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return [$owner, $item];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 testAServiceTakesAGlobalCategory(): void
|
||||||
|
{
|
||||||
|
[$owner, $item] = $this->doctorWithItem();
|
||||||
|
$hand = $this->category($owner, 'دست');
|
||||||
|
|
||||||
|
$body = $this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
|
||||||
|
'catalog_category_uuid' => $hand,
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertSame(200, $this->responseCode(), json_encode($body, JSON_UNESCAPED_UNICODE));
|
||||||
|
self::assertSame($hand, $body['data']['catalog_category_uuid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testNullClearsTheCategory(): void
|
||||||
|
{
|
||||||
|
[$owner, $item] = $this->doctorWithItem();
|
||||||
|
$hand = $this->category($owner, 'دست');
|
||||||
|
|
||||||
|
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, ['catalog_category_uuid' => $hand]);
|
||||||
|
$body = $this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, ['catalog_category_uuid' => null]);
|
||||||
|
|
||||||
|
self::assertSame(200, $this->responseCode());
|
||||||
|
self::assertNull($body['data']['catalog_category_uuid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ⚠️ مرزی ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function testNotSendingTheFieldLeavesTheCategoryAlone(): void
|
||||||
|
{
|
||||||
|
[$owner, $item] = $this->doctorWithItem();
|
||||||
|
$hand = $this->category($owner, 'دست');
|
||||||
|
|
||||||
|
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, ['catalog_category_uuid' => $hand]);
|
||||||
|
// فیلد در بدنه نیست: ویرایش نام نباید دسته را پاک کند.
|
||||||
|
$body = $this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, ['name' => 'لیزر دست کامل']);
|
||||||
|
|
||||||
|
self::assertSame($hand, $body['data']['catalog_category_uuid']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── ❌ خطا ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function testACategoryFromAnotherEnvironmentIsRefused(): void
|
||||||
|
{
|
||||||
|
[$owner, $item] = $this->doctorWithItem();
|
||||||
|
[$stranger] = $this->doctorWithItem();
|
||||||
|
|
||||||
|
$foreign = $this->category($stranger, 'دستهٔ پزشک دیگر');
|
||||||
|
|
||||||
|
$this->authJson('PATCH', '/api/v1/service-item/' . $item->getUuid(), $owner, [
|
||||||
|
'catalog_category_uuid' => $foreign,
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertSame(422, $this->responseCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user