Files
clinicpro/docs/new_feture/taskes/task-15-resource-first-model/checklist.md
T
hamedandClaude Opus 5 1972fdd20f Document the resource-first model and retire the deleted tasks' checklists
docs/architecture/resource-first-model.md describes the shape: the three
entities, why an option is a ServiceItem rather than a fourth table, the
four-level resolution chain, the two conditions on the eligibility filter and
what each of them prevented, and why containment is a graph beside the display
tree rather than the tree itself.

docs/api/resource.md gains both offering endpoints with the response captured
from a real call, including a row where the price comes from the branch and one
where it comes from the resource — the two cases the *_source fields exist for.
docs/api/appointment.md documents resource_uuid, the doctor inference, and the
nullable resource/service_option in the response.

The checklists for tasks 9 to 14 keep their rows but open with a banner saying
the task was removed, when, by whose decision, and which commit to revert. They
are history now; deleting them would erase the record of work that shipped and
was then withdrawn.

Verified end to end: 1304 tests, slot-mode-frozen green, phpstan at 14, tsc
clean, 648 panel tests, and app:seed-scenarios --reset builds all three
environments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 23:05:33 +03:30

16 KiB
Raw Blame History

چک‌لیست تسک ۱۵ — نوبت‌دهی بر پایهٔ منبع

پرامپت: .claude/prompt/resource-first-booking-model.md

وضعیت‌ها: انجام شد · 🔄 در حال انجام · در صف · خارج از محدوده با دلیل · ⚠️ ناقص با دلیل


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

# مورد وضعیت یادداشت
۰.۱ هیچ تم/پالت/فونت/کتابخانهٔ CSS تازه‌ای ساخته نشد هیچ تم/پالت/فونت/کتابخانه‌ای اضافه نشد
۰ رنگ‌ها فقط از توکن‌های styles.css — هیچ hex خام در کد جدید فقط var(--surface-2)/var(--text-2)/var(--text-3)/var(--r-sm)
۰ کامپوننت از components/ui/<select> خام صفر SearchableSelect برای انتخاب سرویس؛ صفر <select> خام
۰ دارک‌مود · حالت فشرده · موبایل ۳۹۰px هر سه سالم هر سه با اسکرین‌شات واقعی تأیید شد
۰.۵ .card با card-pad و برچسب با .field-block برچسب‌ها با field-block و ورودی با field
۰ دکمهٔ بازگشت در صفحات زیرمجموعه PageHeader backTo صفحهٔ منابع از قبل داشت؛ صفحهٔ جدیدی اضافه نشد
۰.۷ وضعیت لیست در query string با useUrlState فیلترهای صفحهٔ منابع از قبل با useUrlState بودند و دست‌نخورده ماندند
۰ هیچ صفحه/فیلد/endpointی خارج از سند اضافه نشد تنها افزوده: یک دکمهٔ «سرویس‌ها» و یک مودال
۰ هیچ interface/کلاس پایه بدون بیش از یک پیاده‌سازیِ فعلی هیچ interface یا کلاس پایه‌ای ساخته نشد
۰.۱۰ Controller نازک · منطق در Service · کوئری در Repository منطق در ResourceServiceAssignmentService، کوئری در repository، کنترلر دو متد کوتاه

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

# مورد وضعیت یادداشت
۱.۱ entity ResourceServiceOffering با TenantOwnedTrait ResourceServiceOffering — فرزند aggregate منبع مثل ResourceSkill، بدون ستون محیط؛ دلیل در docblock
۱ قید یکتای (resource_id, service_item_id) uniq_resource_service (resource_id, service_item_id)
۱ ایندکس (entity_type, entity_id, service_item_id) به‌جایش idx_offering_service (service_item_id, active) — کوئری واقعی «کدام منبع این سرویس را می‌دهد» است
۱ durationMinutes و priceRials تهی‌پذیر = ارث null = ارث؛ صفر و منفی با InvalidArgumentException رد می‌شوند
۱.۵ migration ساخته و اجرا شد Version20260801172619 ساخته و اجرا شد؛ db_test دستی هم‌تراز شد
۱ تست: ثبت ردیف testAnOfferingCarriesTheDurationAndPriceOfThatResource سبز
۱.۷ تست: جفت تکراری → خطای یکتایی testTheSamePairCannotBeRegisteredTwiceUniqueConstraintViolationException
۱ تست: جفت محیط از منبع مشتق می‌شود نه از ورودی testAResourceCannotOfferAServiceFromAnotherEnvironment — گارد در سازنده، چون TenantFilter فرزند aggregate را نمی‌پوشاند

۲. Resolver مدت و قیمت

# مورد وضعیت یادداشت
۲.۱ ResourceServiceResolver با زنجیرهٔ چهارسطحی ResourceServiceResolver — منبع+گزینه → منبع+سرویس → شعبه → پیش‌فرض آیتم
۲.۲ ResolvedServiceSpec منبعِ هر مقدار را می‌گوید ResolvedServiceSpec با durationSource و priceSource
۲.۳ مدت و قیمت جدا حل می‌شوند testDurationAndPriceResolveIndependently — مدت از سطح ۱، قیمت از سطح ۳
۲.۴ تست سطح ۱ (منبع+گزینه) testLevelOneResourcePlusOptionWins سبز
۲.۵ تست سطح ۲ (منبع+سرویس) testLevelTwoResourcePlusServiceWinsWhenTheOptionHasNothing سبز
۲.۶ تست سطح ۳ (شعبه) testLevelThreeBranchWinsWhenTheResourceHasNothing سبز
۲.۷ تست سطح ۴ (پیش‌فرض آیتم) testLevelFourFallsBackToTheItemItself سبز
۲.۸ تست مرزی: مدت از سطح ۱، قیمت از سطح ۳ به‌علاوهٔ سه مرزی: ردیف غیرفعال رد می‌شود · قیمت صفر ارث نمی‌گیرد · حل خودِ سرویس بدون والد

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

# مورد وضعیت یادداشت
۳.۱ findEligible() آرگومان ?ServiceItem گرفت findEligible($address, $type, $skillIds, ?ServiceItem $service)
۳.۲ سازگاری عقب‌رو: بدون ردیف = بدون فیلتر hasAnyFor() دروازه است: بدون ردیف فیلتری اعمال نمی‌شود — testWithoutAnyOfferingTheFilterIsNotAppliedAtAll
۳.۳ AppointmentPlanBuilder سرویس را پاس می‌دهد سرویس از $template->getService() می‌آید نه سرویس ریشه، چون یک برنامه می‌تواند بخش‌هایی از چند آیتم داشته باشد
۳.۴ تست: دو منبع، یکی وصل → assignment همان یکی testOnlyTheResourceThatOffersTheServiceIsEligible — از دو دستگاه هم‌نوع، فقط وصل‌شده
۳.۵ تست: غیرفعال‌کردن ردیف → slots خالی با reason testDeactivatingEveryOfferingLeavesNoCandidate → آرایهٔ خالی؛ به‌علاوه ترکیب با فیلتر مهارت و مسیر بدون سرویس

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

# مورد وضعیت یادداشت
۴.۱ Appointment::$resource تهی‌پذیر Appointment::$resource تهی‌پذیر با ON DELETE SET NULL
۴.۲ Appointment::$serviceOptionItem تهی‌پذیر Appointment::$serviceOptionItem تهی‌پذیر
۴.۳ service_total_minutes از resolver پر می‌شود testTheStoredMinutesComeFromTheResolverNotTheServiceItself — ۱۵ به‌جای ۳۰
۴.۴ PriceSnapshot از resolver پر می‌شود PriceSnapshot از خروجی resolver پر می‌شود
۴.۵ toArray() منبع و گزینه را برمی‌گرداند toArray() کلیدهای resource و service_option را می‌دهد
۴.۶ migration بدون شکستن نوبت‌های موجود Version20260801181433 — دو ستون تهی‌پذیر؛ نوبت‌های موجود دست‌نخورده
۴.۷ تست: تغییر قیمت سرویس، snapshot قدیمی ثابت testChangingTheServicePriceLaterLeavesTheSnapshotAlone — تعرفه ۸M→۱۲M، snapshot ۸M ماند

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

# مورد وضعیت یادداشت
۵.۱ POST /api/v1/appointment فیلد resource_uuid می‌گیرد resource_uuid پذیرفته می‌شود؛ منبع غیرفعال یا ناموجود → ۴۲۲
۵ با منبعِ پزشک، پزشک استنتاج می‌شود testBookingForADoctorResourceInfersTheDoctor — بدون doctor_uuid
۵ Appointment.doctor تهی‌پذیر شد (منبع دستگاهی) به‌جایش محل نوبت از شعبهٔ منبع مشتق می‌شود؛ Appointment.doctor دست‌نخورده ماند — دلیل در گزارش
۵ GET /api/v1/resource/{uuid}/services با مقادیر حل‌شده GET/PUT /api/v1/resource/{uuid}/services با مقدار مؤثر و منبعِ هر عدد
۵.۵ curl: رزرو با resource_uuid → ۲۰۱ testBookingForADoctorResourceInfersTheDoctor → ۲۰۱ با resource در پاسخ
۵ curl: مسیر قدیمی doctor_uuid سالم ماند testTheOldDoctorOnlyPathIsUntouched → ۲۰۱ و resource: null
۵.۷ curl: منبع بی‌ارتباط → ۴۲۲ testAResourceThatDoesNotOfferTheServiceIsRejected → ۴۲۲ «این منبع این سرویس را ارائه نمی‌دهد»
۵ curl: بدون هیچ‌کدام → ۴۲۲ testNeitherDoctorNorResourceIsRejected → ۴۲۲

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

# مورد وضعیت یادداشت
۶.۱ تب روی صفحهٔ منبع موجود، بدون صفحهٔ جدید مودال روی صفحهٔ منابع موجود، دقیقاً الگوی ResourceSkillsModal؛ هیچ صفحه/مسیر جدید
۶.۲ DataTable + StatusBadge + ConfirmDialog Modal + SearchableSelect + کلاس‌های btn/field/field-block؛ ConfirmDialog حذف منبع از قبل بود
۶.۳ مقدار مؤثر به‌صورت placeholder با برچسب منبعش placeholder «۴۰ — پیش‌فرض سرویس» و «۱٬۸۰۰٬۰۰۰ تومان — شعبه» در اسکرین‌شات دیده می‌شود
۶.۴ فرم با React Hook Form + Zod، داده با TanStack Query TanStack Query با useResourceServices؛ فرم ساده است و Zod لازم نداشت
۶.۵ تست: نمایش مقدار مؤثر نشان می‌دهد مقدار ارث‌بری‌شده از کجا آمده سبز
۶.۶ تست: ذخیرهٔ override override را ذخیره می‌کند سبز
۶.۷ تست: پاک‌کردن override → بازگشت به ارث پاک‌کردن مقدار یعنی بازگشت به ارث، نه صفر سبز
۶.۸ اسکرین‌شات دارک‌مود res-dark.png — مودال با دادهٔ واقعی «لیزر CO2 فرکشنال»
۶.۹ اسکرین‌شات حالت فشرده res-compact.pngdata-density=compact
۶.۱۰ اسکرین‌شات موبایل ۳۹۰px بدون اسکرول افقی res-mobile.png — ۳۹۰px، بدون اسکرول افقی

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

# مورد وضعیت یادداشت
۷.۱ resource_catalog_categories (m2m منبع↔دسته) resource_catalog_categories — m2m، همان دسته‌بندی سراسری که سرویس‌ها هم دارند
۷ catalog_category_includes (یال DAG) catalog_category_includes — یال DAG، جدا از parent درختی
۷ CategoryClosureResolver::descendants() با محافظ دور CategoryClosureResolver::descendants() با BFS و $seen به‌عنوان محافظ دور
۷ تعارض انتخاب در ServiceSelectionValidator → ۴۲۲ فارسی categoryOverlapErrors() با کد category_overlap و پیام فارسی
۷.۵ تقدم منابعِ پوشش‌دهندهٔ دسته در findEligible testResourcesCoveringTheServiceCategoryComeFirst — تقدم است نه فیلتر
۷ تست: «تمام بدن» → دست/پا testAWholeBodyCategoryIncludesTheAreasUnderIt سبز
۷.۷ تست: بستار گذرا سه‌سطحی testContainmentIsTransitive — تمام بدن → نیم‌تنه → پا
۷ تست: یال دوری → ۴۲۲ testACycleIsRefusedAppException ۴۲۲؛ به‌علاوه یال به خود → InvalidArgumentException
۷ تست: دستهٔ بی‌یال → آرایهٔ خالی testACategoryWithNoEdgesHasNoDescendants → آرایهٔ خالی
۷.۱۰ curl: «تمام بدن + دست» با هم → ۴۲۲

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

# مورد وضعیت یادداشت
۸.۱ src/Policy/ حذف شد src/Policy/ (۲۷ فایل) حذف شد
۸.۲ src/Package/ حذف شد src/Package/ (۱۳ فایل) حذف شد
۸.۳ src/Course/ حذف شد src/Course/ (۱۴ فایل) حذف شد
۸.۴ src/Cancellation/ + src/Waitlist/ حذف شد src/Cancellation/ + src/Waitlist/ (۱۹ فایل) حذف شد
۸.۵ src/Report/ + src/Shared/Event/ حذف شد src/Report/ + src/Shared/Event/ حذف شد
۸.۶ قلاب‌ها از AppointmentPlanBuilder کنده شد applyTimingPolicies/applyResourcePolicies/requirementForRole برداشته شد — ۴۴۹ → ۳۷۹ خط
۸.۷ قلاب‌ها از PricingEngine کنده شد تخفیف سیاستی و پوشش پکیج از PricingEngine و PriceQuote رفت
۸.۸ قلاب‌ها از BookingController و BookingService کنده شد BookingPolicyGuard، PackageConsumption، CreditLedger، CourseSessionLinker و رویدادها کنده شد
۸.۹ قلاب‌ها از ServiceSelectionValidator کنده شد policyErrors() از validator رفت؛ گروه و رابطه دست‌نخورده ماند
۸.۱۰ GlobalTables و seeder پاک‌سازی شد GlobalTables، سیدر، Schedule.php و messenger.yaml پاک شد
۸.۱۱ صفحات پنل و مسیرهای App.tsx حذف شد ۱۱ صفحهٔ پنل + مسیرها + منوی تنظیمات + هوک‌ها + تایپ‌ها حذف شد
۸.۱۲ migration DROP TABLE با down() واقعی Version20260801170043 — ۱۵ جدول با IF EXISTS، ستون course_session_id نوبت
۸.۱۳ debug:router صفر مسیر حذف‌شده ۵ مسیر باقی‌مانده مربوط به «پکیج انبار» و «گزارش بیمه» است، نه دامنه‌های حذف‌شده
۸.۱۴ تست‌های دامنه‌های حذف‌شده پاک شد tests/{Policy,Package,Course,Cancellation,Waitlist,Report} + assertionهای رویداد در دو تست

۹. مستندات

# مورد وضعیت یادداشت
۹.۱ docs/api/appointment.mdresource_uuid و پاسخ جدید resource_uuid در جدول فیلدها + پاسخ resource/service_option
۹.۲ docs/api/appointment.md — بخش‌های حذف‌شده پاک شد docs/api/appointment.md هیچ ارجاعی به دامنه‌های حذف‌شده نداشت — بررسی شد
۹.۳ docs/api/clinic.md — endpoint سرویس‌های منبع docs/api/resource.mdGET/PUT resource/{uuid}/services با JSON واقعی از اجرای واقعی
۹.۴ سند معماری مدل منبع‌محور docs/architecture/resource-first-model.md
۹.۵ چک‌لیست تسک‌های ۹ تا ۱۴ با وضعیت «حذف‌شده» بنر روی چک‌لیست تسک‌های ۹ تا ۱۴ با تاریخ و کامیت مرجع
۹.۶ TEST_USERS.md به‌روز شد جدول موتور نوبت‌دهی به‌روز شد؛ ردیف‌های حذف‌شده رفتند و منبع↔سرویس و دستهٔ مشترک آمدند

۱۰. تأیید نهایی

# مورد وضعیت یادداشت
۱۰.۱ ddev exec php bin/phpunit کامل سبز 1304 tests, 3776 assertions سبز
۱۰.۲ --group=slot-mode-frozen سبز OK (3 tests, 8 assertions) — خط قرمز دست‌نخورده
۱۰.۳ phpstan روی baseline ۱۴ خطا ۱۴ خطا، همان baseline
۱۰.۴ npx tsc --noEmit بدون خطا بدون خطا
۱۰.۵ npx vitest run assets/admin سبز ۶۴۸ تست در ۹۸ فایل سبز
۱۰.۶ app:seed-scenarios --reset -n بدون خطا --reset -n بدون خطا؛ سیدر حالا رابطهٔ منبع↔سرویس هم می‌سازد
۱۰.۷ کامیت + graphify update + کامیت گراف