Files
clinicpro/docs/new_feture/taskes/task-15-resource-first-model/checklist.md
T
hamedandClaude Opus 5 0a2ba88808 Record which resource an appointment was booked for, and freeze its numbers
An appointment could say which services it was for but not which resource
performed them, so a booking on laser #2 was indistinguishable from one on
laser #1. Both columns are nullable: the appointments that already exist have
no resource and the migration must not break them.

resource_id is not a duplicate of resource_occupancy. Occupancy records what
was held and when — including rooms and devices held for a single segment. This
column records what the appointment is *for*, which is what the panel lists and
what the patient chose.

The option is kept separately from service_item because duration and price
resolve from the resource+service+option triple; without knowing the option,
the stored number cannot be explained later.

Tests: the resource and option survive a round-trip, stored minutes come from
the resolver rather than the service default (15 where the service says 30),
raising the tariff afterwards leaves the earlier snapshot at 8M, and an
appointment with no resource still serialises with nulls instead of failing.

Suite 1290 green, phpstan at its 14-error baseline.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 21:48:45 +03:30

13 KiB
Raw Blame History

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

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

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


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

# مورد وضعیت یادداشت
۰.۱ هیچ تم/پالت/فونت/کتابخانهٔ CSS تازه‌ای ساخته نشد
۰ رنگ‌ها فقط از توکن‌های styles.css — هیچ hex خام در کد جدید
۰ کامپوننت از components/ui/<select> خام صفر
۰ دارک‌مود · حالت فشرده · موبایل ۳۹۰px هر سه سالم با اسکرین‌شات واقعی
۰.۵ .card با card-pad و برچسب با .field-block
۰ دکمهٔ بازگشت در صفحات زیرمجموعه
۰.۷ وضعیت لیست در query string با useUrlState
۰ هیچ صفحه/فیلد/endpointی خارج از سند اضافه نشد
۰ هیچ interface/کلاس پایه بدون بیش از یک پیاده‌سازیِ فعلی
۰.۱۰ Controller نازک · منطق در Service · کوئری در 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 می‌گیرد
۵ با منبعِ پزشک، پزشک استنتاج می‌شود
۵ Appointment.doctor تهی‌پذیر شد (منبع دستگاهی)
۵ GET /api/v1/resource/{uuid}/services با مقادیر حل‌شده
۵.۵ curl: رزرو با resource_uuid → ۲۰۱
۵ curl: مسیر قدیمی doctor_uuid سالم ماند
۵.۷ curl: منبع بی‌ارتباط → ۴۲۲
۵ curl: بدون هیچ‌کدام → ۴۲۲

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

# مورد وضعیت یادداشت
۶.۱ تب روی صفحهٔ منبع موجود، بدون صفحهٔ جدید
۶.۲ DataTable + StatusBadge + ConfirmDialog
۶.۳ مقدار مؤثر به‌صورت placeholder با برچسب منبعش
۶.۴ فرم با React Hook Form + Zod، داده با TanStack Query
۶.۵ تست: نمایش مقدار مؤثر
۶.۶ تست: ذخیرهٔ override
۶.۷ تست: پاک‌کردن override → بازگشت به ارث
۶.۸ اسکرین‌شات دارک‌مود
۶.۹ اسکرین‌شات حالت فشرده
۶.۱۰ اسکرین‌شات موبایل ۳۹۰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 باقی مانده — در وظیفهٔ ۵ کنار endpoint منبع بسته می‌شود
۷ تست: «تمام بدن» → دست/پا 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 و پاسخ جدید
۹.۲ docs/api/appointment.md — بخش‌های حذف‌شده پاک شد
۹.۳ docs/api/clinic.md — endpoint سرویس‌های منبع
۹.۴ سند معماری مدل منبع‌محور
۹.۵ چک‌لیست تسک‌های ۹ تا ۱۴ با وضعیت «حذف‌شده»
۹.۶ TEST_USERS.md به‌روز شد

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

# مورد وضعیت یادداشت
۱۰.۱ ddev exec php bin/phpunit کامل سبز
۱۰.۲ --group=slot-mode-frozen سبز خط قرمز
۱۰.۳ phpstan روی baseline ۱۴ خطا
۱۰.۴ npx tsc --noEmit بدون خطا
۱۰.۵ npx vitest run assets/admin سبز
۱۰.۶ app:seed-scenarios --reset -n بدون خطا
۱۰.۷ کامیت + graphify update + کامیت گراف