Strategies (task 06 debt, task 12 dependency) - ResourcePicker orders candidates; it deliberately does not choose. Only the engine knows which resource actually fits this slot and which was already taken by another role, and a strategy that picked would have to duplicate both checks - Four implementations behind a tagged iterator: first_available (name order, the previous behaviour and still the default because it is predictable), least_gap, least_loaded, same_as_previous - least_gap and least_loaded are deliberate opposites and both are correct; choosing between them is a business decision, so it lives in settings - same_as_previous lifts a course's preferred resource to the front and keeps everyone else behind it. A preference, not a filter: forcing the same operator would make the patient wait two weeks, which is worse than a different operator - Availability accepts course_uuid to supply that preference, closing the dependency task 12 recorded against task 06 - An unknown strategy falls back at search time but is rejected at save time. Stale settings must not stop bookings; a user typing a wrong value must not believe it took effect Test suite flake createUser() retries on a mobile-number collision — db_test is never reset and holds tens of thousands of users, so the random draw does collide. The failed INSERT closes the EntityManager, and the retry asked the container for it again, which hands back the *same closed instance*. So the retry threw, and every later test in that process inherited a dead manager. That is the intermittent "EntityManager is closed" on an unrelated, always-different test that made roughly half of full runs red and never reproduced in a subset. Resetting the registry gives a live manager back. UserCollisionRetryTest pins it by closing the manager on purpose. Two consecutive full runs are green: 1334 tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.8 KiB
Appointment Availability API — جستجوی وقت چندمنبعی
Base:
/api/v1· Auth: JWT وابسته به appointment-plan.md و resource-calendar.md.
چه چیزی را حل میکند
بند ۱۰ مستند: برنامهٔ چندبخشی نوبت را روی تقویم منابع بلغزان و بگو چه ساعتهایی واقعاً ممکناند، با پیشنهاد اینکه کدام منبع استفاده شود.
مسیر قبلی فقط تداخل پزشک را میسنجید؛ اتاق، دستگاه و اپراتور اصلاً وجود نداشتند.
چرا این ظرفیت آزاد میکند
تخصیص per نقش است، نه per بخش. اپراتوری که در بخش «انتظار اثر کرم» نیازمندی ندارد، در آن دقایق بررسی نمیشود و برای بیمار دیگری آزاد است.
نمونهٔ عینی (و تستِ مرجعِ این تسک): بیمار الف ۱۰:۰۰–۱۱:۰۰ نوبت دارد ولی اپراتور فقط ۱۰:۰۰–۱۰:۰۵ و ۱۰:۳۵–۱۱:۰۰ درگیر است. اگر اتاق دومی آزاد باشد، بیمار ب در بازهٔ ۱۰:۰۵–۱۰:۳۵ جا میشود. با مدل تکبازهای، آن نیمساعت هدر میرفت.
چرا همان منبع در بخشهای غیرمجاور
یک منبع برای همهٔ بخشهایی که آن نقش را میخواهند انتخاب میشود. اپراتور بخش ۱ و بخش ۳ باید یک نفر باشد؛ انتخاب مستقل per بخش، دو نفر میداد و بیمار وسط کار تحویل شخص دیگری میشد.
POST /api/v1/appointment-availability
{
"service_uuid": "…",
"branch_uuid": "…",
"from": 1785529800,
"to": 1785616200,
"item_uuids": ["…"],
"patient_gender": "female",
"doctor_uuid": "…",
"step_minutes": 15
}
from/to هر دو شاملاند، سقف ۹۰ روز. step_minutes گام تولید کاندید است
(پیشفرض ۱۵، حداقل ۵).
۲۰۰:
{
"success": true,
"data": {
"plan": { "total_minutes": 60, "segments": [ … ] },
"slots": [
{
"start": 1785562200,
"end": 1785565800,
"assignment": {
"room": [{ "uuid": "…", "name": "اتاق ۲" }],
"operator": [{ "uuid": "…", "name": "اپراتور ۱" }],
"device": [{ "uuid": "…", "name": "لیزر ۳" }]
}
}
],
"reason": null
}
}
plan هم برمیگردد تا کلاینت مجبور نباشد جدا preview بزند.
فهرست خالی خطا نیست و ۴۰۴ هم نیست. reason: "no_capacity_in_range" میآید تا
کلاینت مجبور نباشد از خالی بودن حدس بزند — ممکن است واقعاً ظرفیتی نباشد.
پاسخ حداکثر ۵۰۰ زمان دارد؛ جستجوی یکماهه نباید هزاران ردیف برگرداند.
خطاها
| کد | HTTP | کِی |
|---|---|---|
ERR_WRONG_BOOKING_MODE |
۴۲۲ | این محل روی booking_mode = resource نیست |
ERR_NO_ELIGIBLE_RESOURCE |
۴۲۲ | هیچ منبعی شرایط یک بخش را ندارد (از برنامهساز) |
ERR_VALIDATION_001 |
۴۲۲ | بازهٔ بیش از ۹۰ روز · to < from |
| — | ۴۰۴ | سرویس یا شعبهٔ محیط دیگر |
GET /api/v1/appointment-availability/month
?service_uuid=&branch_uuid=&from=&to= → فقط { "days": [نیمهشبِ روزهای دارای ظرفیت] }.
عمداً سبک است: تقویم ماهانه نباید تخصیص منبع هر زمان را بسازد.
حالت booking_mode = resource
مقدار سومِ کنار slot و service. افزودنی محض: پیشفرض همچنان slot است و هیچ
محیطی خودبهخود به این حالت نمیرود — ارتقا داوطلبانه و صریح است. محلی که روی این
حالت نرفته باشد، همان appointment-slots / appointment-service-slots را دارد و
مسیر قدیمی دستنخورده است.
قواعدی که موتور رعایت میکند
| مورد | رفتار |
|---|---|
setup/cleanup منبع |
بازهٔ اشغال گستردهتر از بازهٔ بخش است و در تداخل لحاظ میشود |
| ظرفیت منبع | شمارش است نه حضور: اتاق سهتخته سه نوبت همزمان میگیرد |
| زمان گذشته | حذف میشود |
| یک منبع، دو نقش همزمان | مجاز نیست |
کارایی
هدف مستند: جستجوی یکماهه زیر نیم ثانیه.
همهٔ ورودیها یک بار خوانده میشوند (تقویم منابع، اشغالها) و بقیه در حافظه است؛ هیچ کوئری داخل حلقهٔ کاندید یا حلقهٔ روز نیست. کاندیدها هم فقط از پنجرههای آزادِ محدودکنندهترین نقش ساخته میشوند — هرس زودهنگام، جستجوی یکماهه را از دهها هزار کاندید به چند صد میرساند.
tests/Appointment/AvailabilityPerformanceTest.php این را با ۲۰ منبع و ۵۰۰ نوبت
ثبتشده در ۳۰ روز میسنجد و بخشی از تسک است، نه اختیاری.
resource_occupancy
یک ردیف بهازای هر (بخشِ نوبت × منبع) — نه یکی بهازای کل نوبت. همین ریزدانگی است که ظرفیت آزاد میکند.
status ∈ booked | hold. نوشتن در این جدول کارِ تسک بعدی (رزرو و ثبت) است؛ این
تسک فقط میخواندش.
تستها
ddev exec php bin/phpunit tests/Appointment/AvailabilityEngineTest.php # ۹ تست
ddev exec php bin/phpunit tests/Appointment/AvailabilityPerformanceTest.php
استراتژی ترتیب منابع
وقتی چند منبع برای یک نقش واجد شرایطاند، ترتیب امتحانکردنشان از تنظیمات محیط
میآید (meta.resource_strategy). استراتژی فقط مرتب میکند؛ تصمیم نهایی همچنان با
موتور است، چون فقط موتور میداند کدام منبع در این زمان جا دارد و کدام برای نقش دیگری
برداشته شده.
| کلید | رفتار | کِی مناسب است |
|---|---|---|
first_available |
ترتیب نام (پیشفرض) | خروجی کاملاً قابل پیشبینی |
least_gap |
کمترین وقت مردهٔ باقیمانده | تقویم کمتر تکهتکه شود |
least_loaded |
منبعِ آزادتر زودتر | بار بین چند اپراتور پخش شود |
same_as_previous |
منبع ترجیحی جلو، بقیه پشت آن | دورهٔ درمان با همان اپراتور |
least_gap و least_loaded عکس هم عمل میکنند و هر دو درستاند؛ انتخاب بینشان
تصمیم کسبوکاری است نه فنی.
ترجیح منبع دوره
POST /api/v1/appointment-availability یک فیلد اختیاری course_uuid میگیرد. با آن،
preferred_resource همان دوره به بالای فهرست میرود.
ترجیح است نه فیلتر: اگر آن منبع آزاد نباشد، رزرو رد نمیشود و به ترتیب پایه برمیگردد — اجبار یعنی بیمار دو هفته منتظر بماند، و آن بدتر از عوض شدن اپراتور است.
فهرست استراتژیها
GET /api/v1/appointment-settings/resource-strategies
{
"success": true,
"data": [
{ "code": "first_available", "label": "به ترتیب نام — ساده و قابل پیشبینی" },
{ "code": "least_gap", "label": "کمترین وقت مرده — تقویم کمتر تکهتکه میشود" }
]
}
انتخابگر پنل از همین ساخته میشود؛ افزودن استراتژی تازه یعنی افزودن یک کلاس با تگ
app.resource_picker — نه تغییر موتور، نه تغییر فرانت.
رفتار با کلید ناشناخته
هنگام جستجو به پیشفرض برمیگردد (تنظیماتِ قدیمی نباید نوبتدهی را بخواباند)، ولی
هنگام ذخیرهٔ تنظیمات 422 میگیرد — وگرنه کاربر فکر میکند استراتژیاش اعمال
میشود در حالی که نمیشود.