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>
16 KiB
16 KiB
چکلیست تسک ۱۵ — نوبتدهی بر پایهٔ منبع
پرامپت: .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 سبز |
| ۱.۷ | تست: جفت تکراری → خطای یکتایی | ✅ | testTheSamePairCannotBeRegisteredTwice → UniqueConstraintViolationException |
| ۱.۸ | تست: جفت محیط از منبع مشتق میشود نه از ورودی | ✅ | 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.png — data-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 — تمام بدن → نیمتنه → پا |
| ۷.۸ | تست: یال دوری → ۴۲۲ | ✅ | testACycleIsRefused → AppException ۴۲۲؛ بهعلاوه یال به خود → 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.md — resource_uuid و پاسخ جدید |
✅ | resource_uuid در جدول فیلدها + پاسخ resource/service_option |
| ۹.۲ | docs/api/appointment.md — بخشهای حذفشده پاک شد |
✅ | docs/api/appointment.md هیچ ارجاعی به دامنههای حذفشده نداشت — بررسی شد |
| ۹.۳ | docs/api/clinic.md — endpoint سرویسهای منبع |
✅ | docs/api/resource.md — GET/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 + کامیت گراف |
⏳ |