From 4711ba0af7616762700c83b04e26051db4908090 Mon Sep 17 00:00:00 2001 From: hamed <15238-genius.ha@users.noreply.drupalcode.org> Date: Sat, 1 Aug 2026 20:13:55 +0330 Subject: [PATCH] docs: prompt and checklist for the resource-first booking model Co-Authored-By: Claude Opus 5 --- .../prompt/resource-first-booking-model.md | 385 ++++++++++++++++++ .../task-15-resource-first-model/checklist.md | 155 +++++++ 2 files changed, 540 insertions(+) create mode 100644 .claude/prompt/resource-first-booking-model.md create mode 100644 docs/new_feture/taskes/task-15-resource-first-model/checklist.md diff --git a/.claude/prompt/resource-first-booking-model.md b/.claude/prompt/resource-first-booking-model.md new file mode 100644 index 00000000..ca479a1b --- /dev/null +++ b/.claude/prompt/resource-first-booking-model.md @@ -0,0 +1,385 @@ +# نوبت‌دهی بر پایهٔ منبع: رابطهٔ منبع↔سرویس، گزینهٔ سرویس، و حل مدت/قیمت + +## پروژه + +`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`. **`` خام؛ فرم با 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 یک **گراف جهت‌دار بدون دور** است، جدا از سلسله‌مراتب نمایشی. + +سه تغییر: + +```php +// ۱) عضویت چندگانهٔ منبع در دسته‌ها — 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 ممنوع، و بستارِ گذرا نباید به خودش برگردد. +} +``` + +```php +// ۳) بستار گذرا: «تمام بدن» شامل «نیم‌تنهٔ پایین» و آن شامل «پا» ⇒ تمام بدن شامل پا +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` رعایت می‌کنند. `TenantSchemaCoverageTest` entity طبقه‌بندی‌نشده را قرمز می‌کند. +- **سازگاری داده:** `resource_id` و `service_option_item_id` روی نوبت nullable؛ فیلتر سرویس در + `findEligible` فقط وقتی رابطه‌ای ثبت شده باشد. +- **cross-repo:** بعد از تغییر قرارداد، مصرف واقعی در `nobat724_front/services/response.js` و + `nobat724_front/components/appointment/` دستی بررسی شود؛ سایت امروز فقط `doctor_uuid` می‌فرستد و + با اختیاری‌شدنش نمی‌شکند، ولی برای رزرو دستگاه باید به‌روز شود. +- **ترتیب اجرا:** اول وظیفهٔ ۷ (حذف) یا اول ۱ تا ۶؟ حذف اول انجام شود — وگرنه resolver و + `PlanBuilder` را دوبار می‌نویسی: یک‌بار با قلاب سیاست، یک‌بار بدون آن. diff --git a/docs/new_feture/taskes/task-15-resource-first-model/checklist.md b/docs/new_feture/taskes/task-15-resource-first-model/checklist.md new file mode 100644 index 00000000..de7dff22 --- /dev/null +++ b/docs/new_feture/taskes/task-15-resource-first-model/checklist.md @@ -0,0 +1,155 @@ +# چک‌لیست تسک ۱۵ — نوبت‌دهی بر پایهٔ منبع + +پرامپت: [`.claude/prompt/resource-first-booking-model.md`](../../../../.claude/prompt/resource-first-booking-model.md) + +وضعیت‌ها: ✅ انجام شد · 🔄 در حال انجام · ⏳ در صف · ⛔ خارج از محدوده با دلیل · ⚠️ ناقص با دلیل + +--- + +## ۰. قواعد غیرقابل‌مذاکره + +| # | مورد | وضعیت | یادداشت | +|---|---|---|---| +| ۰.۱ | هیچ تم/پالت/فونت/کتابخانهٔ CSS تازه‌ای ساخته نشد | ⏳ | | +| ۰.۲ | رنگ‌ها فقط از توکن‌های `styles.css` — هیچ hex خام در کد جدید | ⏳ | | +| ۰.۳ | کامپوننت از `components/ui/` — `