Files
clinicpro/.claude/prompt/resource-first-booking-model.md

27 KiB

نوبت‌دهی بر پایهٔ منبع: رابطهٔ منبع↔سرویس، گزینهٔ سرویس، و حل مدت/قیمت

پروژه

clinicpro (backend + پنل ادمین). cross-repo: قرارداد POST /api/v1/appointment و GET /api/v1/appointment-booking-locations/{doctorUuid} را سایت عمومی مصرف می‌کند (nobat724_front/services/response.jsgetBookingLocations, getServiceSlots, postAppointment). تغییر قرارداد در build سایت خطا نمی‌دهد؛ باید دستی دنبال شود.

قواعد غیرقابل‌مذاکره

این سه بند شرط پذیرش‌اند، نه توصیه. کاری که این‌ها را نقض کند «تمام‌شده» نیست حتی اگر تست‌هایش سبز باشد.

۱. هر UI جدید داخل تم فعلی، در حد یک متخصص UI/UX. هیچ تم، پالت، فونت یا کتابخانهٔ CSS تازه‌ای ساخته نمی‌شود. مشخصاً:

  • رنگ‌ها فقط از توکن‌های assets/admin/styles.css (var(--primary)، var(--surface)، var(--border)، var(--text-2)، …). هیچ hex خامی در کامپوننت جدید.
  • کامپوننت از assets/admin/components/ui/ استفاده شود، نه نسخهٔ دست‌ساز: DataTable، Modal، ConfirmDialog، PageHeader (با backToSearchableSelect، StatusBadge، Pagination، PersianDateInput. <select> خام ممنوع — همیشه SearchableSelect.
  • سه حالت نمایش باید سالم باشند: دارک‌مود ([data-theme="dark"])، حالت فشرده ([data-density="compact"]) و موبایل ۳۹۰px بدون اسکرول افقی.
  • دو تلهٔ شناخته‌شدهٔ همین CSS: .card padding ندارد (برای فاصله card-pad اضافه کن) و .field خودش جعبهٔ ورودی است (برچسبِ بالای فیلد با .field-block می‌آید، نه داخل .field).
  • RTL و متن فارسی؛ تاریخ‌ها شمسی با formatDate().
  • صفحهٔ زیرمجموعه بدون دکمهٔ بازگشت پذیرفته نیست: PageHeader backTo=… یا <BackButton fallback=…/>.
  • وضعیت لیست‌ها (جستجو، فیلتر، صفحه) در query string با hooks/useUrlState.ts، نه در useState.

۲. هر چیزِ اضافه حذف می‌شود. فقط منبع، سرویس، گزینهٔ سرویس و دسته‌بندی می‌ماند. وظیفهٔ ۸ فهرست حذف را دارد؛ ولی قاعده کلی‌تر است: در همین کار هم صفحه، فیلد، endpoint یا گزینه‌ای که سند نخواسته اضافه نکن.

۳. ساختار ساده. بدون abstraction برای آینده:

  • جدول جدید فقط وقتی هیچ جدول موجودی — حتی با یک ستون تازه — کافی نباشد؛ دلیلش نوشته شود. (به همین دلیل «گزینهٔ سرویس» جدول جدید نمی‌گیرد — پایین‌تر.)
  • Controller نازک، منطق در Service، کوئری در Repository، وابستگی با constructor injection.
  • interface و کلاس پایه فقط وقتی الان بیش از یک پیاده‌سازی دارد.
  • نام‌گذاری و سبک کد دقیقاً مثل فایل‌های همسایه.

زمینه

تسک‌های docs/new_feture/taskes/ لایهٔ منبع را ساخته‌اند — ResourceType، ClinicResource، تقویم منبع، استثنا، استخر، مهارت، موتور دسترس‌پذیری و اشغال واقعی. ولی واحد رزرو هنوز پزشک است: نوبت به Doctor گره خورده، و «کدام منبع این سرویس را می‌دهد» و «مدت/قیمت این سرویس برای این منبع» هیچ‌جا داده نیست. سند مالک محصول می‌گوید مدل باید بر پایهٔ منبع باشد، و هر چیزی خارج از منبع/سرویس/گزینه از محصول حذف شود.

مشکل / هدف

سه شکاف واقعی نسبت به سند:

  1. رابطهٔ منبع↔سرویس وجود ندارد. انتخاب منبع فقط با type + skill انجام می‌شود، پس «دستگاه لیزر ۱ این سرویس را می‌دهد ولی دستگاه ۲ نه» قابل بیان نیست.
  2. مدت و قیمت بُعد منبع ندارند. override موجود per شعبه است؛ «دکتر احمدی ۳۰ دقیقه / دکتر رضایی ۴۵ دقیقه برای همان گزینه» نمایش‌دادنی نیست.
  3. نوبت منبع را نگه نمی‌دارد. Appointment.doctor غیرتهی است و رزرو بدون پزشک ممکن نیست، پس نوبتِ «دستگاه لیزر ۲» در مدل جا ندارد.

هدف: منبع واحد رزرو شود، مدت و قیمت با زنجیرهٔ منبع+گزینه → منبع+سرویس → پیش‌فرض حل شود، و نوبت منبع و snapshot را نگه دارد.

گزینهٔ سرویس در این کدبیس جدول جدید نمی‌خواهد. «گزینه» همان ServiceItem عضو یک ItemGroup است (select_min/select_max از قبل هست) و «سرویس» همان آیتم والد. ساختن جدول سوم service_options یعنی دو منبع حقیقت برای یک چیز، و همهٔ مسیرهای امروز (appointment-booking-services، appointment-service-slots، PriceListItem، Tariff) باید دوباره نوشته شوند. دلیل انتخاب در «نکات مهم» ثبت شده است.

معیار پذیرش

  • موفق: برای سرویس «لیزر» و گزینهٔ «پا» دو ردیف منبع ثبت شود (دستگاه ۱: ۲۰ دقیقه/۸ میلیون ریال، دستگاه ۲: ۱۵ دقیقه/۹٫۵ میلیون ریال). GET /api/v1/resource/{uuid}/services هر دو را برگرداند و POST /api/v1/appointment با resource_uuid دستگاه ۲ → 201 با service_total_minutes: 15 و price_snapshot.final_rials: 9500000.
  • خطا: رزرو با resource_uuid منبعی که آن سرویس را ندارد → 422 با ERR_VALIDATION_001 و پیام «این منبع این سرویس را ارائه نمی‌دهد»؛ بدون توکن → 401 با envelope خطا.
  • دسته‌بندی: دستهٔ «تمام بدن» شامل «دست» و «پا» تعریف شود؛ انتخاب هم‌زمان «لیزر تمام بدن» و «لیزر دست» → 422 با پیام فارسی، و یک دستگاه بتواند هم‌زمان به چند دسته وصل باشد.
  • ⚠️ مرزی: منبعی که برای گزینه مقدار ندارد ولی برای سرویس دارد → مقدار سطح سرویس استفاده شود؛ منبعی که هیچ‌کدام را ندارد → پیش‌فرض خودِ آیتم؛ و نوبت‌های قبلاً ثبت‌شده (بدون resource_id) باید همچنان در لیست‌ها و پنل بدون خطا نمایش داده شوند.

فایل‌های مرتبط

فایل نقش
src/Resource/Entity/ClinicResource.php منبع؛ امروز به Doctor/ClinicStaff/Room لینک می‌شود
src/Resource/Repository/ClinicResourceRepository.php findEligible() — انتخاب کاندید با type+skill
src/ClinicService/Entity/ServiceItem.php سرویس و گزینه (هر دو item) با solo/additional و قیمت
src/ClinicService/Entity/ItemGroup.php · ItemGroupMember.php گروه گزینه‌ها با بازهٔ انتخاب
src/ClinicService/Entity/CatalogCategory.php دستهٔ سراسری محیط؛ parent تک‌والدی و MAX_DEPTH = 4
src/ClinicService/Service/ServiceSelectionValidator.php اعتبارسنجی ترکیب انتخاب‌ها
src/ClinicService/Entity/ServiceBranchOverride.php override per شعبه (باید در زنجیره بماند)
src/ClinicService/Service/DurationCalculator.php جمع solo/additional
src/Pricing/Service/PricingEngine.php quote() — بدون بُعد منبع
src/Appointment/Entity/Appointment.php private Doctor $doctor غیرتهی
src/Appointment/Controller/AppointmentController.php book()doctor_uuid الزامی
src/Appointment/Plan/Service/AppointmentPlanBuilder.php ساخت برنامه و نیازمندی‌ها
src/Appointment/Availability/Service/AvailabilityEngine.php جستجوی وقت آزاد روی منابع
assets/admin/pages/ResourcesPage.tsx صفحهٔ منابع پنل
docs/api/appointment.md · docs/api/clinic.md سند endpointها

وضعیت فعلی

انتخاب کاندید هیچ ربطی به سرویس ندارد — src/Resource/Repository/ClinicResourceRepository.php:108:

public function findEligible(DoctorAddress $address, ResourceType $type, array $skillIds = []): array
{
    $qb = $this->createQueryBuilder('r')
        ->where('r.address = :address')
        ->andWhere('r.type = :type')
        ->andWhere('r.active = true')
        // … فیلتر مهارت

override فقط per شعبه است — src/ClinicService/Entity/ServiceBranchOverride.php:41:

#[ORM\Column(name: 'price_rials', type: 'bigint', nullable: true)]
private ?int $priceRials = null;

#[ORM\Column(name: 'solo_duration_minutes', type: 'smallint', nullable: true)]
private ?int $soloDurationMinutes = null;

نوبت بدون پزشک ساخته نمی‌شود — src/Appointment/Entity/Appointment.php:101 و :242:

private Doctor $doctor;

public function __construct(Doctor $doctor, User $user, int $slotStart, int $slotEnd)

قیمت‌گذاری بُعد منبع ندارد — src/Pricing/Service/PricingEngine.php:59:

public function quote(
    ServiceItem $service,
    array $items,
    DoctorAddress $address,
    int $at,
    array $policy = [],
    ?PatientRecord $patient = null,
): PriceQuote {

وظایف

۱. جدول رابطهٔ منبع↔سرویس

src/Resource/Entity/ResourceServiceOffering.php — رابطهٔ چند‌به‌چند با تنظیمات اختصاصی. همان الگوی ResourceSkill/ResourcePoolMember: entity رابطه‌ای با جفت یکتا.

#[ORM\Entity(repositoryClass: ResourceServiceOfferingRepository::class)]
#[ORM\Table(name: 'resource_service_offerings')]
#[ORM\UniqueConstraint(name: 'uniq_resource_service', columns: ['resource_id', 'service_item_id'])]
#[ORM\Index(columns: ['entity_type', 'entity_id', 'service_item_id'], name: 'idx_offering_tenant_service')]
class ResourceServiceOffering
{
    use TenantOwnedTrait;   // جفت از خود منبع مشتق می‌شود، نه از بدنهٔ درخواست

    public function __construct(ClinicResource $resource, ServiceItem $serviceItem) {  }

    private ?int $durationMinutes = null;   // null = ارث از سطح بالاتر
    private ?int $priceRials      = null;   // null = ارث از سطح بالاتر
    private bool $active          = true;
}

نکته: چون «گزینه» هم ServiceItem است، همین یک جدول هر دو سطحِ سند را پوشش می‌دهد — ردیف با آیتمِ والد = «منبع + سرویس»، ردیف با آیتمِ عضو گروه = «منبع + گزینه».

نحوه تست: migration ساخته و اجرا شود؛ سپس ddev exec php bin/phpunit tests/Resource/ResourceServiceOfferingTest.php با سه تست: ثبت ردیف، جفت تکراری → خطای یکتایی، و اینکه جفت محیط از منبع گرفته می‌شود نه از ورودی.

۲. Resolver مدت و قیمت

src/ClinicService/Service/ResourceServiceResolver.php — الگوی Chain of Responsibility، چون سند صریحاً ترتیب اولویت تعریف کرده و افزودن سطح بعدی (مثلاً قرارداد بیمه) نباید if تازه در دل موتور بگذارد.

ترتیب (از خاص به عام) — سطح شعبه عمداً در زنجیره می‌ماند چون داده‌اش امروز وجود دارد:

۱. منبع + گزینه            → ResourceServiceOffering(resource, optionItem)
۲. منبع + سرویس            → ResourceServiceOffering(resource, parentItem)
۳. شعبه + آیتم             → ServiceBranchOverride(item, address)
۴. پیش‌فرض خودِ آیتم        → ServiceItem::getSoloDurationMinutes() / getPriceRials()
public function resolve(ClinicResource $resource, ServiceItem $item, DoctorAddress $address): ResolvedServiceSpec
{
    // اولین سطحی که مقدارِ غیرnull دارد برنده است — مدت و قیمت **جدا** حل می‌شوند:
    // منبعی که فقط مدت را override کرده نباید قیمتش هم از همان سطح بیاید.
}

ResolvedServiceSpec باید بگوید هر مقدار از کدام سطح آمده (durationSource, priceSource) — بدون این، دیباگِ «چرا این عدد؟» در پنل غیرممکن است.

نحوه تست: tests/ClinicService/ResourceServiceResolverTest.php — چهار تست، هر سطح یکی، به‌علاوهٔ تستِ مرزی «مدت از سطح ۱ و قیمت از سطح ۳».

۳. فیلتر کاندیدها بر اساس سرویس

ClinicResourceRepository::findEligible() یک آرگومان اختیاری ?ServiceItem $service بگیرد و وقتی داده شد، فقط منابعی برگردد که ردیف فعال در resource_service_offerings دارند.

قاعدهٔ سازگاری عقب‌رو: اگر برای آن سرویس هیچ ردیفی ثبت نشده باشد، فیلتر اعمال نشود (همان رفتار امروز). وگرنه هر محیطی که هنوز رابطه‌ها را پر نکرده، یک‌شبه بدون وقت آزاد می‌شود.

AppointmentPlanBuilder::planRequirements() سرویس را به eligibleFor() پاس بدهد.

نحوه تست: دو منبع از یک نوع بساز، فقط یکی را به سرویس وصل کن، POST /api/v1/appointment-availability بزن و مطمئن شو assignment همیشه همان یک منبع است. سپس ردیف را غیرفعال کن → slots خالی با reason.

۴. منبع و snapshot روی نوبت

  • Appointment::$resource (ManyToOne, nullable) + Appointment::$serviceOptionItem (nullable). nullable بودن اجباری است: ۷۲ نوبت موجود منبع ندارند و migration نباید آن‌ها را بشکند.
  • service_total_minutes و PriceSnapshot از قبل هستند — فقط باید از خروجی resolver پر شوند، نه از ServiceItem مستقیم.
  • Appointment::toArray() باید resource (uuid, name, type) و service_option را برگرداند.

نحوه تست: tests/Appointment/ResourceBookingTest.php — رزرو با منبع، سپس تغییر قیمت سرویس و اطمینان از اینکه price_snapshot نوبت قبلی تکان نمی‌خورد (همان قاعدهٔ snapshot سند).

۵. رزرو با منبع در endpointها

  • POST /api/v1/appointment: resource_uuid پذیرفته شود. اگر آمد، doctor_uuid اختیاری است و پزشک از resource->subject() استنتاج می‌شود (اگر منبع پزشک باشد). اگر منبع دستگاه باشد و نوبت پزشک ندارد، Appointment.doctor باید nullable شود — این تغییر schema است و migration جدا می‌خواهد.
  • GET /api/v1/appointment-booking-services/{doctorUuid} مکمل بگیرد: GET /api/v1/resource/{uuid}/services → سرویس‌های آن منبع با مدت و قیمتِ حل‌شده.
  • POST /api/v1/appointment بدون هیچ‌کدام → 422.

نحوه تست: با curl و توکن بیمار روی دادهٔ app:seed-scenarios: یک‌بار با resource_uuid دستگاه، یک‌بار با doctor_uuid (مسیر قدیمی باید سالم بماند)، یک‌بار با منبعِ بی‌ارتباط → 422.

۶. پنل ادمین: تب «سرویس‌های این منبع»

در assets/admin/pages/ResourcesPage.tsx (یا صفحهٔ جزئیات منبع) جدولی با ستون‌های سرویس/گزینه · مدت · قیمت · فعال، با ویرایش inline. خالی‌گذاشتن مدت یا قیمت یعنی «ارث از سطح بالاتر» و باید مقدار مؤثر را به‌صورت placeholder با برچسب منبعش نشان دهد (از شعبه، پیش‌فرض سرویس).

از SearchableSelect استفاده شود، نه <select> خام؛ فرم با React Hook Form + Zod؛ داده با TanStack Query.

صفحهٔ تازه ساخته نشود — این یک تب روی صفحهٔ منبع موجود است. جدول با DataTable و ستون وضعیت با StatusBadge؛ حذف رابطه با ConfirmDialog. طبق قاعدهٔ ۱، هیچ رنگ خام و هیچ کامپوننت موازی.

نحوه تست: npx vitest run assets/admin/pages/ResourceServicesTab.test.tsx — سه تست: نمایش مقدار مؤثر، ذخیرهٔ override، پاک‌کردن override → بازگشت به ارث. سپس بازبینی چشمی در مرورگر: دارک‌مود، حالت فشرده و موبایل ۳۹۰px — هر سه بدون شکستگی و بدون اسکرول افقی. بدون این سه اسکرین‌شات، وظیفه تمام‌شده نیست.

۷. دسته‌بندی سراسری کلینیک، مشترک بین سرویس و منبع، با «شامل بودن»

سند مالک محصول: دسته‌بندی باید در سطح کلینیک تعریف شود و هم سرویس‌ها هم منابع از همان استفاده کنند؛ و یک دسته می‌تواند شامل دسته‌های دیگر باشد («تمام بدن» شامل دست و پا و …).

وضعیت امروز: CatalogCategory از قبل سراسریِ محیط است (جفت (entity_type, entity_id) + parent + MAX_DEPTH = 4) و روی ServiceItem::$catalogCategory می‌نشیند. دو چیز کم است:

الف) ClinicResource هیچ فیلد دسته‌ای ندارد، پس «این دستگاه برای دست و پا است» گفتنی نیست.

ب) parent برای «شامل بودن» کافی نیست. درخت تک‌والدی است: «دست» نمی‌تواند هم‌زمان زیر «تمام بدن» و زیر «اندام فوقانی» باشد، در حالی که در لیزر مجموعه‌ها روی هم می‌افتند. پس containment یک گراف جهت‌دار بدون دور است، جدا از سلسله‌مراتب نمایشی.

سه تغییر:

// ۱) عضویت چندگانهٔ منبع در دسته‌ها — m2m، چون یک دستگاه چند ناحیه را پوشش می‌دهد
#[ORM\Table(name: 'resource_catalog_categories')]
#[ORM\UniqueConstraint(name: 'uniq_resource_category', columns: ['resource_id', 'category_id'])]

// ۲) یال «شامل بودن» بین دسته‌ها — DAG، نه درخت
#[ORM\Table(name: 'catalog_category_includes')]
#[ORM\UniqueConstraint(name: 'uniq_category_include', columns: ['parent_category_id', 'child_category_id'])]
class CatalogCategoryInclude
{
    // «تمام بدن» → «دست» ، «تمام بدن» → «پا» …
    // parent === child ممنوع، و بستارِ گذرا نباید به خودش برگردد.
}
// ۳) بستار گذرا: «تمام بدن» شامل «نیم‌تنهٔ پایین» و آن شامل «پا» ⇒ تمام بدن شامل پا
final class CategoryClosureResolver
{
    /** @return int[] شناسهٔ همهٔ دسته‌های زیرمجموعه، با پیمایش عمقی و محافظ دور */
    public function descendants(CatalogCategory $category): array {  }

    public function overlaps(CatalogCategory $a, CatalogCategory $b): bool {  }
}

مصرفش در دو نقطه:

  • تعارض انتخاب: ServiceSelectionValidator وقتی دو آیتم انتخاب‌شده دسته‌هایی دارند که یکی دیگری را شامل می‌شود → 422 با پیام «تمام بدن شامل دست است؛ هر دو با هم انتخاب نمی‌شوند». این جای رابطهٔ دستیِ incompatible_with را برای این حالت می‌گیرد — یک بار در دسته تعریف می‌شود، نه به‌ازای هر جفت آیتم.
  • فیلتر منبع: در findEligible() (وظیفهٔ ۳) اگر سرویس دسته دارد، منابعی که آن دسته یا یکی از اجدادش را پوشش می‌دهند مقدم‌اند.

حلقه ممنوع: پیش از ذخیرهٔ یال، descendants($child) بررسی شود و اگر $parent در آن بود 422 برگردد. بدون این، descendants() تا سرریز استک می‌رود.

نحوه تست: tests/ClinicService/CategoryClosureTest.php «تمام بدن» → دست/پا ثبت شود و descendants هر دو را بدهد · زنجیرهٔ سه‌سطحی بستار گذرا را درست بدهد · یال دوری («دست شامل تمام بدن») → 422 · ⚠️ دسته‌ای که هیچ یالی ندارد → آرایهٔ خالی، نه خطا. سپس با curl: انتخاب هم‌زمان «لیزر تمام بدن» و «لیزر دست» در POST /api/v1/appointment422.

۸. حذف زیرسیستم‌های خارج از این مدل

تصمیم مالک محصول (۱۴۰۵/۰۵/۱۰): هر چیزی خارج از منبع/سرویس/گزینه از محصول حذف شود. ریسکش گفته شد (جریمهٔ لغو و پکیج معمولاً نیاز واقعی کلینیک‌اند) و مالک محصول تصمیم را تکرار کرد.

حذف کامل این پنج زیرسیستم — کد، جدول، endpoint، تست، صفحهٔ پنل، سند:

دامنه مسیر حجم
موتور سیاست src/Policy/ ۲۷ فایل · ۶ فایل تست
پکیج و دفتر اعتبار src/Package/ ۱۳ فایل · ۲ تست
دورهٔ درمان src/Course/ ۱۴ فایل · ۲ تست
لغو/جریمه/لیست انتظار src/Cancellation/ + src/Waitlist/ ۱۹ فایل · ۲ تست
رویداد و گزارش src/Report/ + src/Shared/Event/ ۳ فایل · ۳ تست

صفحات پنل: PolicyFormPage, PolicySimulationPage, PackagesPage, PatientPackageLedgerPage, CourseProtocolsPage, CancellationPolicyPage, ResourceUtilizationPage (+ تست‌هایشان) و مسیرهایشان در App.tsx.

قلاب‌هایی که باید از کد باقی‌مانده کنده شوند (اینجا کامپایل می‌شکند، پس ترتیب مهم است):

src/Appointment/Plan/Service/AppointmentPlanBuilder.php   → applyTimingPolicies() و applyResourcePolicies()
src/Pricing/Service/PricingEngine.php                     → PricingPolicyEngine و PackageConsumptionService
src/Appointment/Booking/Controller/BookingController.php  → BookingPolicyGuard
src/ClinicService/Service/ServiceSelectionValidator.php   → SelectionPolicyEngine
src/Appointment/Booking/Service/BookingService.php        → PackageConsumptionService، CreditLedgerService، CourseSessionLinker
src/Shared/Tenant/GlobalTables.php                        → ردیف‌های همین دامنه‌ها
src/Shared/Command/BookingEngineSeeder.php                → متدهای policies/packagesAndCourses/cancellationAndWaitlist

migration جدا برای DROP TABLE جدول‌های این دامنه‌ها با down() واقعی.

نحوه تست: بعد از حذف: ddev exec php bin/phpunit کامل سبز · ddev exec php vendor/bin/phpstan analyse روی همان baseline ۱۴ خطا · ddev exec npx tsc --noEmit بدون خطا · ddev exec php bin/console debug:router | grep -cE "policy|package|course|waitlist|cancellation"0 · ddev exec php bin/console app:seed-scenarios --reset -n بدون خطا.

۹. مستندات

  • docs/api/appointment.md: فیلد resource_uuid در رزرو + پاسخ resource/service_option، و حذف بخش‌های سیاست/پکیج/دوره/لغو.
  • docs/api/clinic.md: endpoint جدید resource/{uuid}/services.
  • docs/architecture/: سند مدل منبع‌محور با همان چهار سطح زنجیره.
  • docs/new_feture/taskes/: چک‌لیست تسک‌های ۹ تا ۱۴ با وضعیت «حذف‌شده به تصمیم مالک محصول» و تاریخ.
  • TEST_USERS.md: جدول «موتور نوبت‌دهی» باید سطرهای حذف‌شده را از دست بدهد.

نکات مهم

  • خط قرمز: منطق نوبت‌دهی اسلاتی نباید تغییر کند. ddev exec php bin/phpunit --group=slot-mode-frozen باید در هر مرحله سبز بماند.
  • چرا جدول service_options جدید نمی‌سازیم: گزینه از قبل ServiceItem است و ItemGroup قواعد «حداقل یکی، حداکثر سه‌تا» را دارد. جدول سوم یعنی PriceListItem، Tariff، appointment_service_items، SegmentTemplate و کل مسیر appointment-service-slots باید دو نوع ورودی بشناسند — دو منبع حقیقت برای یک مفهوم.
  • مدت و قیمت جدا حل می‌شوند. منبعی که فقط مدت را override کرده نباید قیمتش هم از همان سطح بیاید.
  • جفت محیط (entity_type,entity_id) از خود منبع مشتق شود، نه از بدنهٔ درخواست — همان قاعده‌ای که ClinicResource و ServiceItem رعایت می‌کنند. TenantSchemaCoverageTest entity طبقه‌بندی‌نشده را قرمز می‌کند.
  • سازگاری داده: resource_id و service_option_item_id روی نوبت nullable؛ فیلتر سرویس در findEligible فقط وقتی رابطه‌ای ثبت شده باشد.
  • cross-repo: بعد از تغییر قرارداد، مصرف واقعی در nobat724_front/services/response.js و nobat724_front/components/appointment/ دستی بررسی شود؛ سایت امروز فقط doctor_uuid می‌فرستد و با اختیاری‌شدنش نمی‌شکند، ولی برای رزرو دستگاه باید به‌روز شود.
  • ترتیب اجرا: اول وظیفهٔ ۷ (حذف) یا اول ۱ تا ۶؟ حذف اول انجام شود — وگرنه resolver و PlanBuilder را دوبار می‌نویسی: یک‌بار با قلاب سیاست، یک‌بار بدون آن.