27 KiB
نوبتدهی بر پایهٔ منبع: رابطهٔ منبع↔سرویس، گزینهٔ سرویس، و حل مدت/قیمت
پروژه
clinicpro (backend + پنل ادمین).
cross-repo: قرارداد POST /api/v1/appointment و GET /api/v1/appointment-booking-locations/{doctorUuid} را سایت عمومی مصرف میکند
(nobat724_front/services/response.js → getBookingLocations, 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(باbackTo)،SearchableSelect،StatusBadge،Pagination،PersianDateInput.<select>خام ممنوع — همیشهSearchableSelect. - سه حالت نمایش باید سالم باشند: دارکمود (
[data-theme="dark"])، حالت فشرده ([data-density="compact"]) و موبایل ۳۹۰px بدون اسکرول افقی. - دو تلهٔ شناختهشدهٔ همین CSS:
.cardpadding ندارد (برای فاصله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 گره خورده، و «کدام منبع این سرویس را میدهد» و «مدت/قیمت این سرویس برای این منبع»
هیچجا داده نیست. سند مالک محصول میگوید مدل باید بر پایهٔ منبع باشد، و هر چیزی خارج از
منبع/سرویس/گزینه از محصول حذف شود.
مشکل / هدف
سه شکاف واقعی نسبت به سند:
- رابطهٔ منبع↔سرویس وجود ندارد. انتخاب منبع فقط با
type + skillانجام میشود، پس «دستگاه لیزر ۱ این سرویس را میدهد ولی دستگاه ۲ نه» قابل بیان نیست. - مدت و قیمت بُعد منبع ندارند. override موجود per شعبه است؛ «دکتر احمدی ۳۰ دقیقه / دکتر رضایی ۴۵ دقیقه برای همان گزینه» نمایشدادنی نیست.
- نوبت منبع را نگه نمیدارد.
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/appointment → 422.
۸. حذف زیرسیستمهای خارج از این مدل
تصمیم مالک محصول (۱۴۰۵/۰۵/۱۰): هر چیزی خارج از منبع/سرویس/گزینه از محصول حذف شود. ریسکش گفته شد (جریمهٔ لغو و پکیج معمولاً نیاز واقعی کلینیکاند) و مالک محصول تصمیم را تکرار کرد.
حذف کامل این پنج زیرسیستم — کد، جدول، 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رعایت میکنند.TenantSchemaCoverageTestentity طبقهبندینشده را قرمز میکند. - سازگاری داده:
resource_idوservice_option_item_idروی نوبت nullable؛ فیلتر سرویس درfindEligibleفقط وقتی رابطهای ثبت شده باشد. - cross-repo: بعد از تغییر قرارداد، مصرف واقعی در
nobat724_front/services/response.jsوnobat724_front/components/appointment/دستی بررسی شود؛ سایت امروز فقطdoctor_uuidمیفرستد و با اختیاریشدنش نمیشکند، ولی برای رزرو دستگاه باید بهروز شود. - ترتیب اجرا: اول وظیفهٔ ۷ (حذف) یا اول ۱ تا ۶؟ حذف اول انجام شود — وگرنه resolver و
PlanBuilderرا دوبار مینویسی: یکبار با قلاب سیاست، یکبار بدون آن.