Files
hamed 021d0eb6b2 feat: implement cancellation policy, no-show tracking, and waitlist management
- Add implementation notes for cancellation and waitlist features.
- Create task documentation outlining goals, current status, and acceptance criteria for cancellation policy and resource utilization reporting.
- Establish architecture for domain events and outbox pattern to ensure reliable event publishing.
- Define database schema for domain events and necessary queries for resource utilization and plan accuracy reports.
- Implement detailed implementation notes covering edge cases, testing strategies, and documentation requirements.
2026-07-30 11:43:58 +03:30

112 lines
6.0 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# نکات پیاده‌سازی — تسک ۰۲
## ۱. ظرفیت: یک ردیف با ظرفیت ۳، نه سه ردیف
مستند بند ۶ صریح است و دلیلش در تسک ۰۶ روشن می‌شود: با سه ردیف، موتور جستجو باید سه
تقویم را ادغام کند و «کدام تخت» بشود یک تصمیم که هیچ‌کس نمی‌خواهد بگیرد. با ظرفیت ۳،
شرط اشغال یک شمارش ساده است:
```
SELECT COUNT(*) FROM resource_occupancy
WHERE resource_id = ? AND [بازه‌ها تداخل دارند] AND status IN (…)
→ اگر < capacity، جا هست
```
این را همین حالا در `docs/api/resource.md` بنویس تا کسی وسوسه نشود «اتاق ۱ تخت ۲» بسازد.
## ۲. `ClinicResource` نه `Resource`
نام کلاس `Resource` در PHP مشکل ندارد ولی در این کدبیس با `App\Shared\…` و مفهوم
«منبع API» قاطی می‌شود و در جستجوی کد نویز شدیدی می‌سازد. نام جدول `clinic_resources`
هم به همان دلیل. متغیرها و متدها می‌توانند `$resource` باشند.
## ۳. پل‌ها و `active`
`ClinicStaff.active = false` باید `ClinicResource.active` را هم false کند، وگرنه پرسنل
غیرفعال همچنان در جستجوی وقت ظاهر می‌شود. این را در `StaffService` (تسک موجود) با یک
فراخوانی به `ResourceLinker::syncActive()` انجام بده — نه با Doctrine lifecycle callback،
چون callback در `getArrayResult()` اجرا نمی‌شود و رفتار نامتقارن می‌سازد.
عکسش برقرار نیست: غیرفعال کردن منبع، پرسنل را غیرفعال نمی‌کند (پرسنل ممکن است فقط
اداری باشد).
## ۴. `setup_minutes` / `cleanup_minutes` چه هستند و چه نیستند
- **جزو نوبت بیمار نیستند** — بیمار ساعت ۱۰:۰۰ می‌آید و ۱۰:۳۰ می‌رود
- **منبع را اشغال می‌کنند** — یونیت از ۹:۵۵ تا ۱۰:۴۰ در دسترس نیست
پس در تسک ۰۷ بازهٔ ثبت‌شده در `resource_occupancy` گسترده‌تر از بازهٔ نوبت است. الان فقط
ستون را بساز و در `docs/api/resource.md` این تفاوت را بنویس؛ محاسبه‌اش کار تسک ۰۶ است.
اشتباه رایج: این را با `WeeklySchedule.meta.buffer_minutes` موجود یکی گرفتن. آن یکی
فاصلهٔ سراسری بین دو نوبتِ **پزشک** است؛ این یکی per منبع است. تا وقتی حالت `resource`
نیامده، هر دو کنار هم زندگی می‌کنند و `buffer_minutes` دست‌نخورده می‌ماند.
## ۵. مهارت را با `jobTitle` قاطی نکن
`ClinicStaff.jobTitle` متن آزاد و برای نمایش است. مهارت یک موجودیت با هویت است که در
شرط نیازمندی (تسک ۰۵) و شرط قانون (تسک ۰۹) استفاده می‌شود. هیچ‌جا `jobTitle` را برای
تصمیم‌گیری parse نکن.
## ۶. edge case ها
| حالت | رفتار درست |
|---|---|
| منبع بدون هیچ پل (دستگاه) | معتبر — حالت عادی تجهیزات |
| دو پل هم‌زمان (`doctor_id` و `staff_id`) | `422` در سازنده |
| حذف `resource_type` که منبع دارد | `422` |
| حذف `resource_type` با `is_system=1` | `422` همیشه |
| غیرفعال کردن منبعی که نوبت آیندهٔ فعال دارد | مجاز، ولی پاسخ شامل `warnings[]` با تعداد نوبت‌ها |
| استخر با صفر عضو | معتبر (در حال ساخت)، ولی تسک ۰۶ آن را «هیچ منبعی» می‌بیند |
| `capacity` روی منبع `type=doctor` بزرگ‌تر از ۱ | `422` — پزشک هم‌زمان دو بیمار ندارد |
| `level` خارج از ۱..۵ | `422` |
| مهارت محیط A روی منبع محیط B | `404` (پیش از هر بررسی: `TenantOwnershipChecker`) |
## ۷. کارایی
پرس‌وجوی داغ تسک ۰۶: «منابع فعالِ شعبهٔ X از نوع Y که مهارت Z را دارند».
```php
// ClinicResourceRepository::findEligible()
// یک کوئری با JOIN به resource_skills، بدون N+1
$qb->select('r')
->from(ClinicResource::class, 'r')
->join('r.skills', 'rs')
->where('r.branch = :branch')
->andWhere('r.type = :type')
->andWhere('r.active = true')
->andWhere('rs.skill IN (:skills)')
->groupBy('r.id')
->having('COUNT(DISTINCT rs.skill) = :skillCount'); // همهٔ مهارت‌ها، نه یکی
```
`HAVING COUNT(DISTINCT …)` عمدی است: نیازمندی «مهارت الف و ب» یعنی هر دو، نه یکی.
## ۸. تست
```
tests/Resource/ResourceCrudTest.php
- ساخت منبع + خواندن → tenant و branch درست
- منبع با شعبهٔ محیط دیگر → 404
- capacity=0 → 422 · capacity=2 روی type=doctor → 422
- دو پل هم‌زمان → 422
tests/Resource/SkillAssignmentTest.php
- PUT skills جایگزینی کامل (حذف نداده‌ها)
- level خارج بازه → 422
- حذف مهارتِ در استفاده → 422
tests/Resource/ResourcePoolTest.php
- عضو از شعبهٔ دیگر → 422
- عضو از نوع دیگر → 422
tests/Resource/ResourceEligibilityTest.php
- findEligible با دو مهارت: منبعی که فقط یکی را دارد برنمی‌گردد
tests/Resource/BackfillResourceTest.php
- idempotent: دو بار اجرا = یک بار
tests/Shared/TenantSchemaCoverageTest.php
tests/Shared/TenantLookupInventoryTest.php ← شمارنده به‌روز شود
```
## ۹. مستندات
`docs/api/resource.md` بساز. در `docs/api/staff.md` یک بخش «رابطه با منبع» اضافه کن.
`docs/architecture/tenancy.md` جدول طبقه‌بندی را به‌روز کن.