Files
clinicpro/docs/new_feture/taskes/task-15-resource-first-model/checklist.md
T
hamedandClaude Opus 5 826b940c00 Add the resource↔service link that decides who offers what, and for how much
Until now a resource was picked by type and skill alone, so two devices of the
same type were indistinguishable even when only one of them performed the
service — and there was nowhere to say that this doctor takes 30 minutes for a
filler while that one takes 45.

ResourceServiceOffering is that link: resource ↔ service item, with an optional
duration, an optional price and an active flag. Because a "service option" here
is itself a ServiceItem inside an ItemGroup, one table covers both levels the
spec asks for — a row against the parent item is "resource + service", a row
against a member item is "resource + option". A third table would have meant
two sources of truth for one concept and a rewrite of every path that already
speaks ServiceItem.

It is an aggregate child of ClinicResource, like ResourceSkill: no tenant
columns of its own, since the resource already carries the pair and a copy is
just something that can drift. The constructor refuses a resource and a service
from different environments — TenantFilter does not cover that case, as both
uuids arrive from the request body and the filter does not apply to aggregate
children.

null means inherit, not zero: an explicit zero is a duration that does not
exist, while null means this resource has nothing to say and the resolver
should look one level up. Zero and negative values are rejected outright.

Tests cover the pair being stored, the duplicate pair hitting the unique
constraint, the cross-environment guard, null-means-inherit, one service across
two devices with different numbers, and deactivating without losing them.

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

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

11 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 با زنجیرهٔ چهارسطحی
۲.۲ ResolvedServiceSpec منبعِ هر مقدار را می‌گوید
۲.۳ مدت و قیمت جدا حل می‌شوند
۲.۴ تست سطح ۱ (منبع+گزینه)
۲.۵ تست سطح ۲ (منبع+سرویس)
۲.۶ تست سطح ۳ (شعبه)
۲.۷ تست سطح ۴ (پیش‌فرض آیتم)
۲.۸ تست مرزی: مدت از سطح ۱، قیمت از سطح ۳

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

# مورد وضعیت یادداشت
۳.۱ findEligible() آرگومان ?ServiceItem گرفت
۳.۲ سازگاری عقب‌رو: بدون ردیف = بدون فیلتر
۳.۳ AppointmentPlanBuilder سرویس را پاس می‌دهد
۳.۴ تست: دو منبع، یکی وصل → assignment همان یکی
۳.۵ تست: غیرفعال‌کردن ردیف → slots خالی با reason

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

# مورد وضعیت یادداشت
۴.۱ Appointment::$resource تهی‌پذیر
۴.۲ Appointment::$serviceOptionItem تهی‌پذیر
۴.۳ service_total_minutes از resolver پر می‌شود
۴.۴ PriceSnapshot از resolver پر می‌شود
۴.۵ toArray() منبع و گزینه را برمی‌گرداند
۴.۶ migration بدون شکستن نوبت‌های موجود
۴.۷ تست: تغییر قیمت سرویس، snapshot قدیمی ثابت

۵. رزرو با منبع در 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 منبع↔دسته)
۷ catalog_category_includes (یال DAG)
۷ CategoryClosureResolver::descendants() با محافظ دور
۷ تعارض انتخاب در ServiceSelectionValidator → ۴۲۲ فارسی
۷.۵ تقدم منابعِ پوشش‌دهندهٔ دسته در findEligible
۷ تست: «تمام بدن» → دست/پا
۷.۷ تست: بستار گذرا سه‌سطحی
۷ تست: یال دوری → ۴۲۲
۷ تست: دستهٔ بی‌یال → آرایهٔ خالی
۷.۱۰ 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 + کامیت گراف