test(booking): freeze slot-mode contract before service-mode work
Adds SlotModeFrozenTest (#[Group('slot-mode-frozen')]) locking three things
against the multi-resource booking phase:
- GET /api/v1/appointment-slots response shape
- GET /api/v1/appointment-settings/month-availability/{uuid} response shape
- public method signatures of SlotCalculatorService
Fixtures are structural, not raw snapshots: a fixed past date is rejected by
isWithinBookingWindow so an empty snapshot would prove nothing. Instead a
deterministic schedule on a computed near-future date, with epoch/uuid values
normalized to placeholders. What stays locked is the contract itself: keys,
ordering, types and local times.
No production code touched.
Task: docs/new_feture/taskes/task-00-service-mode-completion/
Slot-mode contract: unchanged (--group=slot-mode-frozen green, 3 tests)
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,569 @@
|
|||||||
|
# راهبر اجرای تسکهای موتور نوبتدهی — یک تسک در هر اجرا
|
||||||
|
|
||||||
|
## پروژه
|
||||||
|
|
||||||
|
`clinicpro` (بکاند + پنل ادمین)
|
||||||
|
|
||||||
|
پرامپت همتا برای سایت عمومی: `nobat724_front/.claude/prompt/booking-engine-task-00b-service-mode.md`
|
||||||
|
(همین راهبر وقتی به تسک ۰۰ب برسد، تحویلش میدهد و میایستد.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## زمینه
|
||||||
|
|
||||||
|
`docs/new_feture/taskes/` شانزده تسک برای پیادهسازی
|
||||||
|
[مستند موتور نوبتدهی](../../docs/new_feture/clinic-pro-mostanad-sade.md) دارد. هر تسک یک
|
||||||
|
پوشه با شش فایل است و **خودش یک پرامپت کامل است**: `task.md` (دامنه و معیار پذیرش)،
|
||||||
|
`architecture.md` (فایلها و سرویسها)، `database.md` (جداول و migration)،
|
||||||
|
`implementation_notes.md` (نکات و edge case و تست)، `checklist.md` (وضعیت)، و برای
|
||||||
|
تسکهای پیچیده `user_flow.md`.
|
||||||
|
|
||||||
|
جمع زمان تخمینی ۲۱۴ تا ۲۵۶ ساعت است. هیچ اجرایی نمیتواند همه را در یک نشست تمام کند.
|
||||||
|
|
||||||
|
**پس این پرامپت خودش قابلیت نیست — راهبر است.** هر اجرا:
|
||||||
|
|
||||||
|
```
|
||||||
|
وضعیت را از checklist.md ها بخوان
|
||||||
|
→ تسک بعدی واجد شرایط را انتخاب کن
|
||||||
|
→ همان یک تسک را کامل انجام بده (پیادهسازی + تست + مستند + چکلیست)
|
||||||
|
→ commit + graphify update
|
||||||
|
→ بایست و بگو تسک بعدی چیست
|
||||||
|
```
|
||||||
|
|
||||||
|
همین پرامپت را دوباره اجرا کن تا تسک بعدی برود. تا وقتی همهٔ چکلیستها ✅ نشدهاند، کار
|
||||||
|
تمام نیست.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## مشکل / هدف
|
||||||
|
|
||||||
|
**هدف:** یک مسیر اجرای تکرارشونده و ایمن که شانزده تسک را به ترتیب وابستگی، یکییکی و
|
||||||
|
بدون از دست دادن وضعیت پیش ببرد — طوری که هر اجرا مستقل باشد و قطع شدن وسط کار،
|
||||||
|
کار انجامشده را از بین نبرد.
|
||||||
|
|
||||||
|
**چرا این شکل و نه یک پرامپت غول:** وضعیت در `checklist.md` روی دیسک زندگی میکند، نه در
|
||||||
|
context. اجرای بعدی همان فایل را میخواند و میفهمد کجا مانده. هزینهٔ این طراحی یک
|
||||||
|
مرحلهٔ «خواندن وضعیت» در ابتدای هر اجراست؛ سودش این است که هیچ اجرایی به context اجرای
|
||||||
|
قبلی وابسته نیست.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## معیار پذیرش
|
||||||
|
|
||||||
|
معیار پذیرش این راهبر، **رفتار خودِ راهبر** است. معیار پذیرش هر تسک در `task.md` همان تسک
|
||||||
|
است و اینجا تکرار نمیشود.
|
||||||
|
|
||||||
|
- ✅ **موفق:** اجرا با شناسایی وضعیت شروع میشود (جدول ۱۶ تسک با ✅/🔄/⏳)، دقیقاً یک تسک
|
||||||
|
انتخاب میشود، همهٔ ردیفهای `checklist.md` آن تسک به ✅ میرسند، `bin/phpunit` و
|
||||||
|
`--group=slot-mode-frozen` سبزند، `docs/api/*` بهروز است، یک commit زده میشود،
|
||||||
|
`graphify update .` اجرا میشود، و اجرا با اعلام تسک بعدی تمام میشود.
|
||||||
|
- ✅ **موفق (ازسرگیری):** اجرای دوم روی همان مخزن، تسک تمامشده را **دوباره انجام نمیدهد**
|
||||||
|
و مستقیم سراغ تسک بعدی میرود.
|
||||||
|
- ✅ **موفق (نیمهکاره):** اگر چکلیستی ردیف 🔄 دارد، همان تسک ادامه داده میشود، نه تسک بعدی.
|
||||||
|
- ❌ **خطا:** اگر تسکی وابستگیاش ✅ نیست → اجرا شروع نمیشود؛ پیام روشن با نام تسک
|
||||||
|
پیشنیاز و توقف.
|
||||||
|
- ❌ **خطا:** اگر `--group=slot-mode-frozen` قرمز شد → **توقف کامل**، rollback تغییرات آن
|
||||||
|
مرحله، گزارش دقیق کدام fixture شکست. fixture هرگز بهروز نمیشود.
|
||||||
|
- ❌ **خطا:** اگر تستی سبز نشد → تسک `completed` نمیشود؛ ردیف چکلیست 🔄 میماند و اجرا
|
||||||
|
با گزارش خطا تمام میشود. تسک بعدی شروع نمیشود.
|
||||||
|
- ⚠️ **مرزی:** همهٔ ۱۶ چکلیست ✅ → پیام «همهٔ تسکها تمام شدهاند» و توقف بدون تغییر.
|
||||||
|
- ⚠️ **مرزی:** تسک انتخابشده ۰۰ب است (پروژهٔ دیگر) → اجرا **کد نمیزند**؛ دستور اجرای
|
||||||
|
پرامپت `nobat724_front` را میدهد و میایستد.
|
||||||
|
- ⚠️ **مرزی:** کاربر شمارهٔ تسک را صریح داد (`/run-prompt … --task=05`) → همان تسک، ولی
|
||||||
|
وابستگیها همچنان بررسی میشوند و اگر ناقص بودند توقف.
|
||||||
|
- ⚠️ **مرزی:** ردیفی از چکلیست به دلیل موجه قابل انجام نیست → ⏳ میماند **با دلیل مکتوب و
|
||||||
|
تسک مقصد** در ستون یادداشت. ⏳ بیدلیل = تسک تمام نشده.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## فایلهای مرتبط
|
||||||
|
|
||||||
|
| فایل | نقش |
|
||||||
|
|------|-----|
|
||||||
|
| `docs/new_feture/taskes/README.md` | فهرست ۱۶ تسک، وابستگیها، ترتیب اجرا |
|
||||||
|
| `docs/new_feture/taskes/00-current-state-report.md` | تحلیل شکاف وضعیت فعلی در برابر مستند |
|
||||||
|
| `docs/new_feture/taskes/_shared/red-lines.md` | ⛔ قواعد قفلشده — منطق اسلاتی دستکاری نمیشود |
|
||||||
|
| `docs/new_feture/taskes/_shared/ui-conventions.md` | 🎨 توکنها و کامپوننتهای اجباری |
|
||||||
|
| `docs/new_feture/taskes/_shared/definition-of-done.md` | ☑️ نمادها و بازبینی پایانی |
|
||||||
|
| `docs/new_feture/taskes/task-XX-*/task.md` | دامنه و معیار پذیرش هر تسک |
|
||||||
|
| `docs/new_feture/taskes/task-XX-*/architecture.md` | فایلها، سرویسها، قواعد UI |
|
||||||
|
| `docs/new_feture/taskes/task-XX-*/database.md` | جداول، ایندکس، migration، backfill |
|
||||||
|
| `docs/new_feture/taskes/task-XX-*/implementation_notes.md` | edge case و فهرست تست |
|
||||||
|
| `docs/new_feture/taskes/task-XX-*/checklist.md` | **وضعیت پایدار — منبع حقیقت پیشرفت** |
|
||||||
|
| `docs/new_feture/taskes/task-XX-*/user_flow.md` | جریان کاربری (۰۰، ۰۰ب، ۰۵، ۰۶، ۰۷، ۱۲) |
|
||||||
|
| `CLAUDE.md` · `docs/architecture/tenancy.md` | قواعد ثابت پروژه |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## وضعیت فعلی
|
||||||
|
|
||||||
|
هر شانزده `checklist.md` ساخته شده و **همهٔ ردیفهایشان `⏳` است**. هیچ تسکی شروع نشده.
|
||||||
|
|
||||||
|
ساختار ثابت هر چکلیست:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# چکلیست — تسک XX (عنوان)
|
||||||
|
|
||||||
|
**وضعیت کلی:** ⏳ شروع نشده · **آخرین بازبینی:** —
|
||||||
|
|
||||||
|
## ۰. خط سرخ
|
||||||
|
| # | مورد | وضعیت | یادداشت |
|
||||||
|
|---|---|---|---|
|
||||||
|
| ۰.۱ | `--group=slot-mode-frozen` سبز | ⏳ | |
|
||||||
|
...
|
||||||
|
|
||||||
|
## ۱. بکاند
|
||||||
|
## ۲. دیتابیس
|
||||||
|
## ۳. UI
|
||||||
|
## ۴. تست
|
||||||
|
## ۵. مستندات
|
||||||
|
## ۶. بازبینی پایانی
|
||||||
|
```
|
||||||
|
|
||||||
|
نمادها (از `_shared/definition-of-done.md`):
|
||||||
|
|
||||||
|
| نماد | معنی | اجازهٔ باقیماندن در پایان تسک |
|
||||||
|
|---|---|---|
|
||||||
|
| ✅ | انجامشده و تأییدشده | بله |
|
||||||
|
| 🔄 | در حال انجام | **نه** |
|
||||||
|
| ⏳ | انجامنشده | **نه** — مگر با دلیل مکتوب و تسک مقصد |
|
||||||
|
| ⚠️ | نیازمند بررسی یا تست | **نه** — باید تعیین تکلیف شود |
|
||||||
|
|
||||||
|
ترتیب و وابستگی (از `README.md`):
|
||||||
|
|
||||||
|
```
|
||||||
|
۰۰ ── ۰۰ب
|
||||||
|
│
|
||||||
|
├─ ۰۱ ─┬─ ۰۲ ── ۰۳ ─┐
|
||||||
|
│ └─ ۰۴ ── ۰۵ ─┴─ ۰۶ ── ۰۷ ─┬─ ۰۸ ─┬─ ۰۹ ── ۱۰
|
||||||
|
│ │ └─ ۱۱ ── ۱۲
|
||||||
|
│ ├─ ۱۳
|
||||||
|
│ └─ ۱۴
|
||||||
|
```
|
||||||
|
|
||||||
|
| تسک | پروژه | وابستگی |
|
||||||
|
|---|---|---|
|
||||||
|
| ۰۰ تکمیل نوبتدهی سرویسی | `clinicpro` | — |
|
||||||
|
| ۰۰ب سازگارسازی سایت | `nobat724_front` | ۰۰ |
|
||||||
|
| ۰۱ شعبه و اتاق | `clinicpro` | ۰۰ |
|
||||||
|
| ۰۲ منبع، مهارت، استخر | `clinicpro` | ۰۱ |
|
||||||
|
| ۰۳ تقویم منبع | `clinicpro` | ۰۱، ۰۲ |
|
||||||
|
| ۰۴ کاتالوگ خدمات v2 | `clinicpro` | ۰۱ |
|
||||||
|
| ۰۵ بخشهای نوبت | `clinicpro` | ۰۲، ۰۴ |
|
||||||
|
| ۰۶ جستجوی وقت چندمنبعی | `clinicpro` | ۰۳، ۰۵ |
|
||||||
|
| ۰۷ رزرو موقت و ثبت | `clinicpro` | ۰۶ |
|
||||||
|
| ۰۸ قیمت و snapshot | `clinicpro` | ۰۴، ۰۷ |
|
||||||
|
| ۰۹ موتور قوانین | `clinicpro` | ۰۵، ۰۶، ۰۸ |
|
||||||
|
| ۱۰ فرم و sandbox قانون | `clinicpro` | ۰۹ |
|
||||||
|
| ۱۱ پکیج و دفتر اعتبار | `clinicpro` | ۰۸ |
|
||||||
|
| ۱۲ دوره درمان | `clinicpro` | ۰۷، ۱۱ |
|
||||||
|
| ۱۳ لغو، عدم حضور، انتظار | `clinicpro` | ۰۷ |
|
||||||
|
| ۱۴ رویدادها و بهرهوری | `clinicpro` | ۰۷ |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## وظایف
|
||||||
|
|
||||||
|
> ⚠️ این هفت وظیفه **مراحل یک اجرا**ی راهبرند، نه هفت قابلیت مستقل. `todo` را از روی
|
||||||
|
> ردیفهای `checklist.md` تسکِ انتخابشده بساز، نه از روی این هفت مرحله.
|
||||||
|
|
||||||
|
### ۱. خواندن قواعد حاکم — پیش از هر چیز
|
||||||
|
|
||||||
|
سه سند را کامل بخوان و در همین اجرا اعمال کن:
|
||||||
|
|
||||||
|
```
|
||||||
|
docs/new_feture/taskes/_shared/red-lines.md
|
||||||
|
docs/new_feture/taskes/_shared/ui-conventions.md
|
||||||
|
docs/new_feture/taskes/_shared/definition-of-done.md
|
||||||
|
```
|
||||||
|
|
||||||
|
در تناقض با متن هر تسک، **این سه برندهاند**.
|
||||||
|
|
||||||
|
مهمترین بندشان:
|
||||||
|
|
||||||
|
- ⛔ منطق اسلاتی (`booking_mode = 'slot'`) در **هیچ تسکی** دستکاری نمیشود. فهرست فایلها و
|
||||||
|
متدهای قفلشده در `red-lines.md` است.
|
||||||
|
- 🎨 هر صفحه یا کامپوننت جدید عیناً با دیزاینسیستم موجود — توکنهای `assets/admin/styles.css`،
|
||||||
|
کامپوننتهای `assets/admin/components/ui/`، پنج قاعدهٔ غیرقابلمذاکره.
|
||||||
|
- ☑️ هیچ تسکی بدون تکمیل چکلیستش تمام نیست.
|
||||||
|
|
||||||
|
**نحوه تست:** بعد از خواندن، در گزارش شروع بنویس کدام سه سند خوانده شد و مهمترین قید
|
||||||
|
مربوط به تسک انتخابشده چیست (یک جمله).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۲. شناسایی وضعیت و انتخاب تسک
|
||||||
|
|
||||||
|
همهٔ چکلیستها را بخوان و وضعیت هر تسک را از **ردیفهایش** استنتاج کن، نه از خط
|
||||||
|
«وضعیت کلی» (که ممکن است بهروز نشده باشد):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# فهرست چکلیستها به ترتیب
|
||||||
|
ls -1 docs/new_feture/taskes/*/checklist.md | sort
|
||||||
|
|
||||||
|
# شمارش نمادها per تسک — دیدِ سریع وضعیت
|
||||||
|
for f in docs/new_feture/taskes/*/checklist.md; do
|
||||||
|
echo "$(dirname "$f" | xargs basename): ✅=$(grep -c '| ✅ |' "$f") 🔄=$(grep -c '| 🔄 |' "$f") ⏳=$(grep -c '| ⏳ |' "$f") ⚠️=$(grep -c '| ⚠️ |' "$f")"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
قاعدهٔ استنتاج:
|
||||||
|
|
||||||
|
```
|
||||||
|
تمامشده = هیچ ردیف 🔄 · هیچ ردیف ⚠️ · هیچ ردیف ⏳ بدون یادداشتِ دلیل
|
||||||
|
در جریان = حداقل یک ردیف 🔄 یا ⚠️
|
||||||
|
شروع نشده = همهٔ ردیفها ⏳ و هیچ یادداشتی
|
||||||
|
```
|
||||||
|
|
||||||
|
قاعدهٔ انتخاب، به همین ترتیب:
|
||||||
|
|
||||||
|
1. اگر تسکی **در جریان** است → همان. (تسک نیمهکاره رها نمیشود.)
|
||||||
|
2. وگرنه اولین تسکِ **شروع نشده** که همهٔ وابستگیهایش **تمامشده** اند.
|
||||||
|
3. اگر کاربر `--task=XX` داد → همان، ولی وابستگیها همچنان بررسی میشوند.
|
||||||
|
4. اگر همه تمامشدهاند → پیام و توقف.
|
||||||
|
5. اگر تسک انتخابشده وابستگی ناقص دارد → توقف با نام تسک پیشنیاز.
|
||||||
|
|
||||||
|
جدول وضعیت را به کاربر نشان بده و تسک انتخابشده را اعلام کن:
|
||||||
|
|
||||||
|
```
|
||||||
|
📊 وضعیت تسکها
|
||||||
|
۰۰ تکمیل سرویسی ✅ تمامشده
|
||||||
|
۰۰ب سازگارسازی سایت ✅ تمامشده
|
||||||
|
۰۱ شعبه و اتاق 🔄 در جریان (۱۲/۳۸)
|
||||||
|
۰۲ منبع و مهارت ⏳ شروع نشده
|
||||||
|
…
|
||||||
|
🎯 تسک انتخابشده: ۰۱ — شعبه و اتاق (ادامهٔ کار نیمهکاره)
|
||||||
|
```
|
||||||
|
|
||||||
|
**نحوه تست:** دستور شمارش بالا را اجرا کن و خروجی واقعیاش را در گزارش بگذار. جدول
|
||||||
|
نمایشدادهشده باید با آن خروجی بخواند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۳. تحویل به پرامپت همتا اگر تسک ۰۰ب انتخاب شد
|
||||||
|
|
||||||
|
تسک ۰۰ب روی `nobat724_front` است و در این پروژه هیچ کدی ندارد.
|
||||||
|
|
||||||
|
اگر انتخاب ۰۰ب شد، **هیچ فایلی را تغییر نده** و این را چاپ کن و بایست:
|
||||||
|
|
||||||
|
```
|
||||||
|
🔀 تسک ۰۰ب روی پروژهٔ nobat724_front است.
|
||||||
|
اجرا کن: /run-prompt nobat724_front/.claude/prompt/booking-engine-task-00b-service-mode.md
|
||||||
|
بعد از تمام شدنش، همین راهبر را دوباره اجرا کن تا تسک ۰۱ برود.
|
||||||
|
```
|
||||||
|
|
||||||
|
**نحوه تست:** موقتاً همهٔ ردیفهای `task-00-service-mode-completion/checklist.md` را ✅ کن،
|
||||||
|
راهبر را اجرا کن، و ببین همین پیام چاپ میشود و هیچ فایلی در `src/` عوض نمیشود.
|
||||||
|
بعد تغییر موقت را برگردان.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۴. اجرای تسک انتخابشده — یکی، کامل
|
||||||
|
|
||||||
|
چهار (یا پنج) فایل تسک را **کامل** بخوان:
|
||||||
|
|
||||||
|
```
|
||||||
|
task-XX-*/task.md ← دامنه، endpoint ها، معیار پذیرش
|
||||||
|
task-XX-*/architecture.md ← فایلها، سرویسها، الگوها، قواعد UI
|
||||||
|
task-XX-*/database.md ← جداول، ایندکس، migration، backfill
|
||||||
|
task-XX-*/implementation_notes.md ← edge case ها، فهرست تست، تلههای مشخص
|
||||||
|
task-XX-*/user_flow.md ← اگر وجود دارد
|
||||||
|
```
|
||||||
|
|
||||||
|
بعد کد واقعی مرتبط را بخوان — **هیچ فرضی از حافظه** (guidelines §۱).
|
||||||
|
|
||||||
|
سپس `todo` بساز: **یک آیتم به ازای هر ردیفِ غیر-✅ چکلیست**، مرتب بر اساس بخش
|
||||||
|
(۰ خط سرخ → ۱ بکاند → ۲ دیتابیس → ۳ UI → ۴ تست → ۵ مستندات → ۶ بازبینی پایانی).
|
||||||
|
|
||||||
|
اجرای هر ردیف با همان هفتمرحلهٔ `run-prompt`:
|
||||||
|
|
||||||
|
```
|
||||||
|
① تحلیل ② طراحی ③ پیادهسازی ④ تست ⑤ رفع خطا ⑥ مستندسازی ⑦ چکلیست + گزارش
|
||||||
|
```
|
||||||
|
|
||||||
|
قواعدی که در این مرحله شکستنی نیستند:
|
||||||
|
|
||||||
|
- **بخش ۰ چکلیست اول از همه.** خط سرخها پیش از هر کد بررسی میشوند. در تسک ۰۰ این یعنی
|
||||||
|
ساختن `SlotModeFrozenTest` و سه fixture **قبل** از هر تغییر — وگرنه snapshot وضعیت
|
||||||
|
تغییریافته گرفته میشود و چیزی را تضمین نمیکند.
|
||||||
|
- **یک ردیف در هر لحظه `in_progress`.**
|
||||||
|
- **ردیف فقط با هر سه ✅ میشود:** پیادهسازی + تست سبز + مستند بهروز.
|
||||||
|
- **خطا = توقف کامل.** هیچ ردیفی روی خرابهٔ ردیف قبلی ساخته نمیشود.
|
||||||
|
- **کشف کار جدید** (باگ جانبی، وابستگی پنهان) → ردیف جدید به همان `checklist.md` **اضافه
|
||||||
|
کن** و به کاربر بگو. انجام بیصدا ممنوع.
|
||||||
|
|
||||||
|
**نحوه تست:** دستورهای هر پروژه بعد از هر ردیفِ منطقدار:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ddev exec php bin/console cache:clear
|
||||||
|
ddev exec php bin/console doctrine:migrations:diff --no-interaction # اگر entity عوض شد
|
||||||
|
ddev exec php bin/console doctrine:migrations:migrate --no-interaction
|
||||||
|
ddev exec php bin/phpunit tests/<Domain> # تست همان تسک
|
||||||
|
ddev exec php bin/phpunit --group=slot-mode-frozen # ⛔ بعد از هر ردیف
|
||||||
|
ddev exec php vendor/bin/phpstan analyse src/<Domain>
|
||||||
|
npx tsc --noEmit --project tsconfig.json # اگر tsx عوض شد
|
||||||
|
yarn dev # بررسی خطای build
|
||||||
|
yarn test
|
||||||
|
```
|
||||||
|
|
||||||
|
برای هر endpoint، تست واقعی با داده واقعی — نه فقط `php -l`. کاربر تست:
|
||||||
|
`09390039833 / 09390039833` (اگر کار نکرد، اعتبارهای per-نقش با `QaTest@1234`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۵. بهروزرسانی زندهٔ `checklist.md`
|
||||||
|
|
||||||
|
چکلیست **حافظهٔ بیناجرایی** است. اگر بهروز نشود، اجرای بعدی کار انجامشده را دوباره
|
||||||
|
انجام میدهد.
|
||||||
|
|
||||||
|
قواعد نوشتن:
|
||||||
|
|
||||||
|
- ردیف را **همان لحظه** که شروع میکنی `⏳ → 🔄` کن، نه در پایان تسک
|
||||||
|
- تمام شد → `🔄 → ✅` با یادداشت کوتاه اگر چیزی برای گفتن هست (مسیر فایل، عدد اندازهگیریشده)
|
||||||
|
- شکست خورد یا بلاک شد → `⚠️` با **دلیل** در ستون یادداشت
|
||||||
|
- به تعویق افتاد → `⏳` با **دلیل + تسک مقصد** در یادداشت. `⏳` بیدلیل = تسک تمام نشده.
|
||||||
|
- خط سربرگ را هم بهروز کن:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
**وضعیت کلی:** 🔄 در حال انجام · **آخرین بازبینی:** ۱۴۰۵/۰۵/۰۳
|
||||||
|
```
|
||||||
|
|
||||||
|
پیش از رفتن به وظیفهٔ ۶، **بخش ۶ (یا ۷/۸ بسته به تسک) بازبینی پایانی** را ردیفبهردیف
|
||||||
|
اجرا کن. ردیفهای مشترک همهٔ تسکها:
|
||||||
|
|
||||||
|
```
|
||||||
|
همهٔ ردیفهای بالا وضعیت نهایی دارند (هیچ 🔄 و ⏳ بیدلیل)
|
||||||
|
ddev exec php bin/phpunit کامل سبز
|
||||||
|
ddev exec php bin/phpunit --group=slot-mode-frozen سبز
|
||||||
|
ddev exec php vendor/bin/phpstan analyse بدون خطای جدید
|
||||||
|
npx tsc --noEmit بدون خطا
|
||||||
|
yarn test سبز
|
||||||
|
TenantSchemaCoverageTest + TenantLookupInventoryTest سبز
|
||||||
|
docs/api/* بهروز شد
|
||||||
|
چکلیست UI کامل شد (اگر تسک صفحه دارد)
|
||||||
|
nobat724_front و clinic-pro-tauri دستی بررسی شدند
|
||||||
|
commit شد، سپس graphify update .
|
||||||
|
موارد بهتعویقافتاده با دلیل و تسک مقصد ثبت شدند
|
||||||
|
```
|
||||||
|
|
||||||
|
ردیف «دو کلاینت دیگر» در build هیچکدام خطا نمیدهد — بررسی **فقط دستی** ممکن است
|
||||||
|
(guidelines §۳). نتیجه را با ذکر فایلهای بررسیشده گزارش بده، نه با «بررسی شد».
|
||||||
|
|
||||||
|
**نحوه تست:** بعد از بهروزرسانی، دستور شمارش وظیفهٔ ۲ را دوباره اجرا کن. تسک باید
|
||||||
|
`✅=<همه> 🔄=0 ⏳=0 ⚠️=0` بدهد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۶. Commit و بهروزرسانی گراف
|
||||||
|
|
||||||
|
**ترتیب مهم است:** اول commit، بعد `graphify update` — وگرنه گراف وضعیت کامیتنشده را
|
||||||
|
ایندکس میکند.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add -A
|
||||||
|
git commit -m "$(cat <<'EOF'
|
||||||
|
feat(booking): <عنوان انگلیسی تسک>
|
||||||
|
|
||||||
|
<دو-سه خط: چه چیزی اضافه شد، چه چیزی عمداً دستنخورده ماند>
|
||||||
|
|
||||||
|
Task: docs/new_feture/taskes/task-XX-<name>/
|
||||||
|
Slot-mode contract: unchanged (--group=slot-mode-frozen green)
|
||||||
|
|
||||||
|
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
||||||
|
EOF
|
||||||
|
)"
|
||||||
|
|
||||||
|
graphify update .
|
||||||
|
```
|
||||||
|
|
||||||
|
پیام کامیت انگلیسی (قاعدهٔ پروژه). خط `Slot-mode contract` اجباری است — رد مکتوبِ
|
||||||
|
اینکه خط سرخ رعایت شده.
|
||||||
|
|
||||||
|
اگر روی برنچ `main` هستی، **اول برنچ بساز**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git checkout -b feat/booking-engine-task-XX
|
||||||
|
```
|
||||||
|
|
||||||
|
**نحوه تست:** `git log --oneline -1` و `git status` را در گزارش بگذار.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ۷. گزارش پایانی و اعلام تسک بعدی
|
||||||
|
|
||||||
|
اجرا با این قالب تمام میشود و **تسک بعدی شروع نمیشود**:
|
||||||
|
|
||||||
|
```
|
||||||
|
✅ تسک XX — <عنوان> تمام شد
|
||||||
|
|
||||||
|
پیادهسازی
|
||||||
|
• <سه تا پنج خط: چه ساخته شد، با مسیر فایل>
|
||||||
|
|
||||||
|
تست
|
||||||
|
• bin/phpunit : <N> تست، سبز
|
||||||
|
• --group=slot-mode-frozen : سبز
|
||||||
|
• phpstan / tsc / yarn test : سبز
|
||||||
|
• تستهای جدید این تسک : <فهرست کوتاه>
|
||||||
|
|
||||||
|
مستندات
|
||||||
|
• <فایلهای docs/api/* و docs/architecture/* که عوض شدند>
|
||||||
|
|
||||||
|
خط سرخ
|
||||||
|
• منطق اسلاتی دستنخورده — <یک جمله شاهد، مثلاً: هیچ متد SlotCalculatorService عوض نشد>
|
||||||
|
|
||||||
|
کلاینتهای دیگر
|
||||||
|
• nobat724_front : <چه بررسی شد و نتیجه>
|
||||||
|
• clinic-pro-tauri: <همان>
|
||||||
|
|
||||||
|
به تعویق افتاد (اگر هست)
|
||||||
|
• <ردیف> — دلیل: <…> — تسک مقصد: <XX>
|
||||||
|
|
||||||
|
commit: <hash کوتاه> · graphify: بهروز شد
|
||||||
|
|
||||||
|
────────────────────────────────
|
||||||
|
🎯 تسک بعدی: XX — <عنوان> (وابستگیها: ✅)
|
||||||
|
اجرا کن: /run-prompt clinicpro/.claude/prompt/booking-engine-task-runner.md
|
||||||
|
```
|
||||||
|
|
||||||
|
اگر تسک ناتمام ماند، بهجای `✅` بنویس:
|
||||||
|
|
||||||
|
```
|
||||||
|
⛔ تسک XX ناتمام ماند
|
||||||
|
|
||||||
|
بلاککننده: <چه چیزی و کجا>
|
||||||
|
ردیفهای باقی: <فهرست با وضعیت>
|
||||||
|
چکلیست بهروز شد؛ اجرای بعدی از همینجا ادامه میدهد.
|
||||||
|
```
|
||||||
|
|
||||||
|
**نحوه تست:** گزارش باید با خروجی واقعی دستورها بخواند. عدد تست ساختگی ننویس.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## نکات مهم
|
||||||
|
|
||||||
|
### خط سرخ — تنها چیزی که هیچ تسکی نمیتواند نقض کند
|
||||||
|
|
||||||
|
`booking_mode = 'slot'` منطق تولیدیِ زنده است. `_shared/red-lines.md` فهرست دقیق دارد؛
|
||||||
|
خلاصهاش:
|
||||||
|
|
||||||
|
| قفل | یعنی |
|
||||||
|
|---|---|
|
||||||
|
| هفت متد عمومی `SlotCalculatorService` | امضا و رفتار عوض نمیشود |
|
||||||
|
| `GET /api/v1/appointment-slots` | قرارداد request/response |
|
||||||
|
| `GET /api/v1/appointment-settings/month-availability/{doctorUuid}` | قرارداد |
|
||||||
|
| `Appointment::active_slot_key` و `refreshActiveSlotKey()` | مکانیزم یکتایی موجود |
|
||||||
|
| `AppointmentRepository::isSlotTaken` | امضا و معنا |
|
||||||
|
| `WeeklySchedule::DEFAULT_META['booking_mode']` | مقدار `slot` میماند |
|
||||||
|
|
||||||
|
**مجاز:** افزودن متد جدید · کلاس موازی · ستون تهیپذیر · کلید جدید در `meta` با حفظ پیشفرض
|
||||||
|
**ممنوع:** تغییر امضای متد موجود · refactor «یکدستسازی» · اعمال قوانین جدید روی حالت `slot`
|
||||||
|
|
||||||
|
fixture های `tests/Appointment/fixtures/slot-mode-contract.json` و
|
||||||
|
`month-availability-contract.json` و `slot-calculator-signatures.php` **read-only** اند.
|
||||||
|
اگر تست قرمز شد، **کد برمیگردد، نه fixture**. تسک ۰۰ آنها را میسازد؛ هیچ تسک دیگری
|
||||||
|
اجازهٔ بهروزرسانیشان را ندارد.
|
||||||
|
|
||||||
|
### فاز ۰ اختیاری نیست
|
||||||
|
|
||||||
|
تسک ۰۰ و ۰۰ب پیشنیاز واقعیاند، نه توصیه. حالت `service` امروز پنج شکاف در چرخهٔ عمر
|
||||||
|
نوبت دارد (ویرایش، جابهجایی، رزرو، پنل بیمار، دیزاینسیستم سایت — جزئیات در
|
||||||
|
`00-current-state-report.md` بخش ۲-۵ب). اگر حالت `resource` (تسک ۰۶) روی این پایه ساخته
|
||||||
|
شود، هر باگ موجود به موتور جدید ارث میرسد و تشخیص منبعش غیرممکن میشود.
|
||||||
|
|
||||||
|
### دیزاینسیستم — هر صفحهٔ جدید
|
||||||
|
|
||||||
|
`_shared/ui-conventions.md` کامل است. پنج قاعدهای که رعایت نکردنشان یعنی رد شدن ردیف:
|
||||||
|
|
||||||
|
1. `SearchableSelect`، هرگز `<select>` بومی
|
||||||
|
2. زیرصفحهها `backTo` یا `<BackButton fallback="…" />` — دکمهٔ دستساز نه
|
||||||
|
3. وضعیت لیست (جستجو/فیلتر/صفحه) در URL با `hooks/useUrlState.ts`، نه `useState`
|
||||||
|
4. داده فقط TanStack Query از `lib/api.ts`؛ استخراج: paginated → `data?.data` +
|
||||||
|
`data?.meta?.totalRecords` · single → `data?.data` · Category → `data?.data?.data ?? []`
|
||||||
|
5. فرم با React Hook Form + Zod
|
||||||
|
|
||||||
|
و: هیچ رنگ/شعاع/سایهٔ hard-code — همه از توکنهای `:root` در `assets/admin/styles.css`.
|
||||||
|
دارکمود (`[data-theme="dark"]`) و حالت فشرده (`[data-density="compact"]`) بررسی شوند.
|
||||||
|
مقدار hex در دارکمود میشکند.
|
||||||
|
|
||||||
|
پیش از ساختن هر کامپوننت، `assets/admin/components/ui/` را بگرد. ساختن نسخهٔ موازیِ
|
||||||
|
`DataTable`/`Modal`/`ConfirmDialog`/`PageHeader`/`StatCard`/`StatusBadge`/`Pagination`/
|
||||||
|
`PersianDatePicker`/`PriceInput` رد میشود.
|
||||||
|
|
||||||
|
### قواعد ثابت پروژه در هر تسک
|
||||||
|
|
||||||
|
از `CLAUDE.md` و `docs/architecture/tenancy.md`:
|
||||||
|
|
||||||
|
- entity جدید یا `TenantOwnedTrait` میگیرد یا با دلیل در `App\Shared\Tenant\GlobalTables`
|
||||||
|
ثبت میشود؛ `TenantSchemaCoverageTest` هر دو حالت را اجبار میکند
|
||||||
|
- `entity_type, entity_id` **ستونهای اول** هر ایندکس ترکیبیِ لیست — وگرنه MariaDB برای
|
||||||
|
شرط `TenantFilter` از ایندکس استفاده نمیکند
|
||||||
|
- هر uuid که از request میآید با `App\Shared\Tenant\TenantOwnershipChecker` سنجیده
|
||||||
|
میشود؛ `TenantLookupInventoryTest` شمارنده per-file دارد و `findByUuid` تازه تست را
|
||||||
|
قرمز میکند
|
||||||
|
- timestamp ها `int` یونیکس، نه `DateTime`؛ نمایش شمسی فقط در UI با `formatDate`
|
||||||
|
- کنترلر نازک · `extends BaseController` · `$this->success()/paginated()/error()`
|
||||||
|
- خطا با `throw new AppException(ErrorCodes::ERR_XXX, null, $status)`؛ کد و پیام فارسی در
|
||||||
|
`src/Shared/Constant/ErrorCodes.php`
|
||||||
|
- لیستهای ادمین با `->getArrayResult()`
|
||||||
|
- **API جدید فقط وقتی هیچ endpoint موجودی — حتی با توسعه — کافی نباشد؛ دلیلش نوشته شود**
|
||||||
|
- هر endpoint جدید یا تغییریافته → `docs/api/*.md` در **همان** نشست
|
||||||
|
|
||||||
|
### الگوهای معماری که تسکها تصریح کردهاند
|
||||||
|
|
||||||
|
| الگو | تسک | دلیل انتخاب (در همان تسک مکتوب است) |
|
||||||
|
|---|---|---|
|
||||||
|
| Strategy + tagged_iterator | ۰۶ `ResourcePickerInterface` | افزودن استراتژی پنجم نباید کلاس موجود را عوض کند (OCP) |
|
||||||
|
| Repository + سرویس واحدِ نویسنده | ۰۷ `OccupancyWriter` | تنها نقطهٔ نوشتن در `resource_occupancy` |
|
||||||
|
| Registry بسته | ۰۹ `FieldRegistry`/`OperatorRegistry`/`EffectRegistry` | مستند بند ۸: کد دلخواه در قانون ممنوع، وگرنه جستجوی وقت کند میشود |
|
||||||
|
| Outbox | ۱۴ `DomainEventPublisher` | commit اتمی رویداد با کار؛ نه «پیامک رفته و نوبت نیست» |
|
||||||
|
| Ledger (دفتر) نه شمارنده | ۱۱ `SessionCreditLedger` | مستند بند ۱۲: با شمارنده، اولین اشتباه غیرقابلردیابی است |
|
||||||
|
| کلید یکتای سطل زمانی | ۰۷ `resource_occupancy_slot` | MariaDB `EXCLUDE` ندارد؛ تضمین از دیتابیس نه از کد |
|
||||||
|
|
||||||
|
اینها تصمیم گرفتهشدهاند، نه پیشنهاد. اگر در اجرا به مشکل خوردی، **قبل از تغییر
|
||||||
|
الگو** دلیل را به کاربر بگو و تعیین تکلیف بخواه (guidelines §۶).
|
||||||
|
|
||||||
|
### تسکهای پرخطر — جای کندی و دقت
|
||||||
|
|
||||||
|
| تسک | ریسک | کاری که باید بکنی |
|
||||||
|
|---|---|---|
|
||||||
|
| ۰۰ | fixture بعد از تغییرات ساخته شود | fixture **اول**، بعد کد |
|
||||||
|
| ۰۶ | I/O داخل حلقه → جستجو کند | `AvailabilityPerformanceTest` تعداد کوئری را قفل میکند: ≤۵ |
|
||||||
|
| ۰۷ | تست همزمانی با mock بیارزش است | دو اتصال DBAL واقعی، `assertSame(1, ok1+ok2)` |
|
||||||
|
| ۰۹ | `spacing` per-slot حلقه بزند | `forbiddenRanges()` کوئریمحور؛ `AvailabilityPerformanceTest` باید با قوانین فعال هم سبز بماند |
|
||||||
|
| ۱۰ | شبیهسازی داده واقعی بنویسد | rollback در `finally` + `em->clear()` + تست شمارش ردیف |
|
||||||
|
| ۱۱ | `quote` اعتبار مصرف کند | مصرف **فقط** در `confirm`؛ تست: ده `quote` → مانده بیتغییر |
|
||||||
|
| ۱۲ | `book-all` نیمهکاره | همه یا هیچ در یک تراکنش |
|
||||||
|
|
||||||
|
### cross-repo — بررسی دستی اجباری
|
||||||
|
|
||||||
|
`nobat724_front/services/response.js` و `clinic-pro-tauri/src/service/response.js` هر دو
|
||||||
|
مصرفکنندهٔ همان `/api/v1/...` اند. تغییر قرارداد در build هیچکدام خطا نمیدهد و در
|
||||||
|
runtime میشکند (guidelines §۳).
|
||||||
|
|
||||||
|
تسکهایی که قرارداد را واقعاً عوض میکنند و بررسی دستیشان بحرانی است:
|
||||||
|
|
||||||
|
- **۰۰** — `service_item` تکی باید با `service_items` همگام بماند (چهار مصرفکننده رویش خواندهاند)
|
||||||
|
- **۰۴** — مدت نوبتهای چندسرویسی **عوض میشود** (فرمول «زمان تنها/زمان اضافه»)
|
||||||
|
- **۰۶** — پس از ارتقای یک کلینیک به `resource`، کلاینت باید مسیر جدید صدا بزند
|
||||||
|
- **۰۸** — مبلغ نمایشی رزرو ممکن است عوض شود
|
||||||
|
- **۱۳** — سایت باید `cancellation-preview` را نشان دهد
|
||||||
|
|
||||||
|
در گزارش هر تسک، **فایلهای بررسیشده را نام ببر**؛ «بررسی شد» بیارزش است.
|
||||||
|
|
||||||
|
### وقتی متن تسک با کد واقعی نمیخواند
|
||||||
|
|
||||||
|
تسکها از خواندن کد در تاریخ نوشتنشان ساخته شدهاند. اگر جایی متن با کد امروز نخواند:
|
||||||
|
|
||||||
|
1. **کد واقعی مرجع است**، نه متن تسک
|
||||||
|
2. اختلاف را به کاربر بگو
|
||||||
|
3. متن تسک را اصلاح کن (فایل تسک هم سند است — guidelines §۴)
|
||||||
|
4. بعد پیادهسازی کن
|
||||||
|
|
||||||
|
موارد شناختهشدهای که تسکها خودشان علامت زدهاند و باید **پیش از کدنویسی** تأیید شوند:
|
||||||
|
|
||||||
|
- تسک ۱۴ ردیف ۲.۹ — وجود `patient_sessions.started_at/ended_at`؛ اگر نبود، گزارش
|
||||||
|
`plan-accuracy` به تسک جدا موکول میشود، **نه** ساخته شدن با داده حدسی
|
||||||
|
- تسک ۰۰ب بخش ۱ — وجود `service_items` و `service_total_minutes` در پاسخ
|
||||||
|
`GET /api/v1/appointments/user`؛ اگر نبود، به تسک ۰۰ برمیگردد
|
||||||
|
- تسک ۰۷ database.md — وابستگی معکوس: جدول `resource_occupancy` در تسک **۰۶** migrate
|
||||||
|
میشود، تعریفش در ۰۷ است
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
# چکلیست — تسک ۰۰ (تکمیل نوبتدهی سرویسی در clinicpro)
|
# چکلیست — تسک ۰۰ (تکمیل نوبتدهی سرویسی در clinicpro)
|
||||||
|
|
||||||
**وضعیت کلی:** ⏳ شروع نشده
|
**وضعیت کلی:** 🔄 در حال انجام — قابلیت ۱ از ۱۰ (خط سرخ) تمام شد
|
||||||
**آخرین بازبینی:** —
|
**آخرین بازبینی:** ۱۴۰۵/۰۵/۰۸
|
||||||
|
|
||||||
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
|
قواعد: [_shared/definition-of-done.md](../_shared/definition-of-done.md) ·
|
||||||
خط سرخها: [_shared/red-lines.md](../_shared/red-lines.md) ·
|
خط سرخها: [_shared/red-lines.md](../_shared/red-lines.md) ·
|
||||||
@@ -13,15 +13,16 @@ UI: [_shared/ui-conventions.md](../_shared/ui-conventions.md)
|
|||||||
|
|
||||||
| # | مورد | وضعیت | یادداشت |
|
| # | مورد | وضعیت | یادداشت |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| ۰.۱ | `SlotModeFrozenTest` + سه fixture ساخته شد **پیش از** هر تغییر کد | ⏳ | ترتیب مهم است |
|
| ۰.۱ | `SlotModeFrozenTest` + سه fixture ساخته شد **پیش از** هر تغییر کد | ✅ | `tests/Appointment/SlotModeFrozenTest.php` + `tests/Appointment/fixtures/` — هیچ کد تولیدیای هنوز لمس نشده |
|
||||||
| ۰.۲ | fixture ها با تاریخ ثابتاند، نه `time()` | ⏳ | |
|
| ۰.۲ | ~~fixture ها با تاریخ ثابتاند، نه `time()`~~ → fixture **ساختاری** است | ✅ | **انحراف عمدی از متن تسک.** تاریخ ثابتِ گذشته را `isWithinBookingWindow` رد میکند و snapshot خالی چیزی را تضمین نمیکند. بهجایش: برنامهٔ قطعی (هر ۷ روز یک شیفت ۰۹:۰۰–۱۱:۰۰/۳۰ دقیقه) روی `+3 days`، و epoch/uuid با placeholder نرمال میشوند. آنچه قفل میشود: کلیدها، ترتیب، نوعها، ساعتهای محلی |
|
||||||
| ۰.۳ | کامنت «read-only، هیچ تسکی بهروزش نمیکند» بالای هر سه fixture | ⏳ | |
|
| ۰.۳ | کامنت «read-only، هیچ تسکی بهروزش نمیکند» بالای هر سه fixture | ✅ | کلید `_readme` در دو JSON (در loader حذف میشود) + docblock در فایل PHP |
|
||||||
| ۰.۴ | هیچ متد موجود `SlotCalculatorService` ویرایش نشد | ⏳ | فقط پارامتر اختیاری `excludeAppointmentId` روی `getServiceStartTimes` |
|
| ۰.۴ | هیچ متد موجود `SlotCalculatorService` ویرایش نشد | ⏳ | قابلیت ۴ — فقط پارامتر اختیاری `excludeAppointmentId` روی `getServiceStartTimes`؛ fixture امضا **یک بار** بهروز میشود (header خودش این را مجاز کرده) |
|
||||||
| ۰.۵ | `GET /appointment-slots` بیتبهبیت دستنخورده | ⏳ | |
|
| ۰.۵ | `GET /appointment-slots` بیتبهبیت دستنخورده | ⏳ | در پایان تسک تأیید میشود |
|
||||||
| ۰.۶ | `GET /month-availability/{doctorUuid}` دستنخورده | ⏳ | |
|
| ۰.۶ | `GET /month-availability/{doctorUuid}` دستنخورده | ⏳ | در پایان تسک تأیید میشود |
|
||||||
| ۰.۷ | `active_slot_key` و `refreshActiveSlotKey()` دستنخورده | ⏳ | تنها استثنا: صدا زدنش در `setIsReserve` |
|
| ۰.۷ | `active_slot_key` و `refreshActiveSlotKey()` دستنخورده | ✅ | `setIsReserve()` **وجود ندارد**؛ toggle رزرو از قبل با `rescheduleTo($start,$end,$isReserve)` انجام میشود که خودش `refreshActiveSlotKey()` را صدا میزند ([Appointment.php:316](../../../src/Appointment/Entity/Appointment.php)). یادداشت قبلی چکلیست غلط بود |
|
||||||
| ۰.۸ | `isSlotTaken` امضا و معنا دستنخورده | ⏳ | |
|
| ۰.۸ | `isSlotTaken` امضا و معنا دستنخورده | ⏳ | در پایان تسک تأیید میشود |
|
||||||
| ۰.۹ | `--group=slot-mode-frozen` در پایان سبز | ⏳ | |
|
| ۰.۹ | `--group=slot-mode-frozen` سبز | ✅ | `OK (3 tests, 8 assertions)` — نیازمند `#[Group]` attribute بود، نه `@group` (PHPUnit 12 annotation را حذف کرده) |
|
||||||
|
| ۰.۱۰ | baseline: کل `tests/Appointment` پیش از تغییرات سبز | ✅ | `OK (166 tests, 382 assertions)` |
|
||||||
|
|
||||||
## ۱. بکاند
|
## ۱. بکاند
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\Appointment;
|
||||||
|
|
||||||
|
use App\Appointment\Entity\WeeklySchedule;
|
||||||
|
use App\Appointment\Service\SlotCalculatorService;
|
||||||
|
use App\Doctor\Entity\Doctor;
|
||||||
|
use App\Tests\ApiTestCase;
|
||||||
|
use PHPUnit\Framework\Attributes\Group;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* قرارداد نوبتدهی اسلاتی — منجمد.
|
||||||
|
*
|
||||||
|
* حالت `booking_mode = slot` منطق تولیدیِ زنده است و در فاز موتور چندمنبعی
|
||||||
|
* ({@see docs/new_feture/taskes/_shared/red-lines.md}) هیچ تسکی اجازهٔ تغییرش را ندارد.
|
||||||
|
* این تست سه چیز را قفل میکند:
|
||||||
|
*
|
||||||
|
* ۱. شکل پاسخ `GET /api/v1/appointment-slots`
|
||||||
|
* ۲. شکل پاسخ `GET /api/v1/appointment-settings/month-availability/{doctorUuid}`
|
||||||
|
* ۳. امضای متدهای عمومی `SlotCalculatorService`
|
||||||
|
*
|
||||||
|
* ⛔ سه فایل `fixtures/slot-mode-*` و `fixtures/month-availability-*` و
|
||||||
|
* `fixtures/slot-calculator-signatures.php` پس از تسک ۰۰ read-only اند. اگر این تست
|
||||||
|
* قرمز شد، **کد باید برگردد، نه fixture**.
|
||||||
|
*
|
||||||
|
* چرا fixture ساختاری است و نه snapshot خام: `buildAllSessions()` تاریخ گذشته را رد
|
||||||
|
* میکند (`isWithinBookingWindow`) و `getAllSlotsWithAvailability()` مقدار
|
||||||
|
* `is_available` را با `time()` میسنجد. پس snapshot با تاریخ ثابتِ گذشته همیشه خالی
|
||||||
|
* است و چیزی را تضمین نمیکند. بهجایش برنامهٔ قطعی روی تاریخِ محاسبهشدهٔ نزدیک ساخته
|
||||||
|
* میشود و مقادیر epoch با placeholder جایگزین میشوند؛ آنچه میماند دقیقاً همان چیزی
|
||||||
|
* است که قرارداد را میسازد: کلیدها، ترتیب، نوعها و ساعتهای محلی.
|
||||||
|
*/
|
||||||
|
#[Group('slot-mode-frozen')]
|
||||||
|
class SlotModeFrozenTest extends ApiTestCase
|
||||||
|
{
|
||||||
|
private const EPOCH_PLACEHOLDER = '<epoch>';
|
||||||
|
private const UUID_PLACEHOLDER = '<uuid>';
|
||||||
|
|
||||||
|
/** روزِ مبنا: نه امروز (تا `is_available` با گذر ساعت نلرزد) و داخل بازهٔ رزرو. */
|
||||||
|
private const OFFSET_DAYS = 3;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* برنامهٔ هفتگیِ قطعی: هر هفت روز یک شیفت یکسان، تا نتیجه به روزِ هفتهٔ اجرای تست
|
||||||
|
* وابسته نباشد.
|
||||||
|
*/
|
||||||
|
private function makeSlotModeDoctor(): array
|
||||||
|
{
|
||||||
|
$owner = $this->createUser(['ROLE_DOCTOR']);
|
||||||
|
$doctor = new Doctor($owner, 'دکتر قرارداد اسلاتی');
|
||||||
|
$this->em->persist($doctor);
|
||||||
|
|
||||||
|
$day = [
|
||||||
|
'sessions' => [[
|
||||||
|
'active' => true,
|
||||||
|
'start_time' => '09:00',
|
||||||
|
'end_time' => '11:00',
|
||||||
|
'duration_per_patient' => 30,
|
||||||
|
'has_rest' => false,
|
||||||
|
'patient_limit' => null,
|
||||||
|
'location_id' => 1,
|
||||||
|
]],
|
||||||
|
];
|
||||||
|
|
||||||
|
$setting = [];
|
||||||
|
foreach (range(0, 6) as $dayKey) {
|
||||||
|
$setting[(string) $dayKey] = $day;
|
||||||
|
}
|
||||||
|
|
||||||
|
$schedule = $this->newWeeklySchedule($doctor, $setting);
|
||||||
|
$schedule->setMeta([
|
||||||
|
'booking_mode' => WeeklySchedule::MODE_SLOT,
|
||||||
|
'online_booking_enabled' => true,
|
||||||
|
'booking_window_value' => 3,
|
||||||
|
'booking_window_unit' => 'month',
|
||||||
|
'buffer_minutes' => 0,
|
||||||
|
]);
|
||||||
|
$this->em->persist($schedule);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
return [$doctor, date('Y-m-d', strtotime('+' . self::OFFSET_DAYS . ' days'))];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAppointmentSlotsContractIsFrozen(): void
|
||||||
|
{
|
||||||
|
[$doctor, $date] = $this->makeSlotModeDoctor();
|
||||||
|
|
||||||
|
$this->client->request('GET', '/api/v1/appointment-slots?' . http_build_query([
|
||||||
|
'doctor_uuid' => $doctor->getUuid(),
|
||||||
|
'date' => $date,
|
||||||
|
]));
|
||||||
|
|
||||||
|
self::assertSame(200, $this->client->getResponse()->getStatusCode());
|
||||||
|
|
||||||
|
$actual = $this->normalize(
|
||||||
|
json_decode($this->client->getResponse()->getContent(), true),
|
||||||
|
['date' => '<date>'],
|
||||||
|
);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
$this->fixture('slot-mode-contract.json'),
|
||||||
|
$actual,
|
||||||
|
'قرارداد appointment-slots عوض شده است. کد را برگردان، نه fixture را.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testMonthAvailabilityContractIsFrozen(): void
|
||||||
|
{
|
||||||
|
[$doctor, $date] = $this->makeSlotModeDoctor();
|
||||||
|
$ts = (int) strtotime($date);
|
||||||
|
|
||||||
|
$this->client->request('GET', sprintf(
|
||||||
|
'/api/v1/appointment-settings/month-availability/%s?year=%d&month=%d',
|
||||||
|
$doctor->getUuid(),
|
||||||
|
(int) date('Y', $ts),
|
||||||
|
(int) date('n', $ts),
|
||||||
|
));
|
||||||
|
|
||||||
|
self::assertSame(200, $this->client->getResponse()->getStatusCode());
|
||||||
|
|
||||||
|
$body = json_decode($this->client->getResponse()->getContent(), true);
|
||||||
|
|
||||||
|
// فهرست روزها داده است نه قرارداد (به «امروز» وابسته است)؛ قرارداد این است که
|
||||||
|
// هر روزِ ماه دقیقاً در یکی از دو فهرست باشد.
|
||||||
|
$daysInMonth = (int) date('t', $ts);
|
||||||
|
self::assertCount(
|
||||||
|
$daysInMonth,
|
||||||
|
array_merge($body['data']['enabled_dates'], $body['data']['disabled_dates']),
|
||||||
|
'هر روزِ ماه باید دقیقاً در یکی از enabled/disabled باشد',
|
||||||
|
);
|
||||||
|
|
||||||
|
$actual = $this->normalize($body, [
|
||||||
|
'year' => '<year>',
|
||||||
|
'month' => '<month>',
|
||||||
|
'enabled_dates' => '<dates>',
|
||||||
|
'disabled_dates' => '<dates>',
|
||||||
|
]);
|
||||||
|
|
||||||
|
self::assertSame(
|
||||||
|
$this->fixture('month-availability-contract.json'),
|
||||||
|
$actual,
|
||||||
|
'قرارداد month-availability عوض شده است. کد را برگردان، نه fixture را.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* امضای متدهای عمومیِ SlotCalculatorService. افزودن پارامتر اختیاری مجاز است ولی
|
||||||
|
* باید **یک بار** در fixture ثبت شود؛ تغییر یا حذف پارامتر موجود ممنوع.
|
||||||
|
*/
|
||||||
|
public function testSlotCalculatorPublicApiIsFrozen(): void
|
||||||
|
{
|
||||||
|
self::assertSame(
|
||||||
|
require __DIR__ . '/fixtures/slot-calculator-signatures.php',
|
||||||
|
$this->publicSignaturesOf(SlotCalculatorService::class),
|
||||||
|
'امضای عمومی SlotCalculatorService عوض شده است. کد را برگردان، نه fixture را.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, array<string, string>> */
|
||||||
|
private function publicSignaturesOf(string $class): array
|
||||||
|
{
|
||||||
|
$out = [];
|
||||||
|
foreach ((new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
|
||||||
|
if ($method->isConstructor() || $method->getDeclaringClass()->getName() !== $class) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$params = [];
|
||||||
|
foreach ($method->getParameters() as $p) {
|
||||||
|
$params[$p->getName()] = sprintf(
|
||||||
|
'%s%s',
|
||||||
|
(string) ($p->getType() ?? 'mixed'),
|
||||||
|
$p->isDefaultValueAvailable()
|
||||||
|
? ' = ' . var_export($p->getDefaultValue(), true)
|
||||||
|
: '',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$out[$method->getName()] = [
|
||||||
|
'returns' => (string) ($method->getReturnType() ?? 'mixed'),
|
||||||
|
'params' => $params,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
ksort($out);
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* مقادیر دادهایِ وابسته به زمان را با placeholder جایگزین میکند تا آنچه مقایسه
|
||||||
|
* میشود فقط قرارداد بماند: کلیدها، ترتیب، نوعها و ساعتهای محلی.
|
||||||
|
*
|
||||||
|
* @param array<string, string> $overrides کلید → placeholder
|
||||||
|
*/
|
||||||
|
private function normalize(mixed $value, array $overrides = [], ?string $key = null): mixed
|
||||||
|
{
|
||||||
|
if ($key !== null && array_key_exists($key, $overrides)) {
|
||||||
|
return $overrides[$key];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_array($value)) {
|
||||||
|
$out = [];
|
||||||
|
foreach ($value as $k => $v) {
|
||||||
|
$out[$k] = $this->normalize($v, $overrides, is_string($k) ? $k : $key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $out;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_int($value) && $value > 1_000_000_000) {
|
||||||
|
return self::EPOCH_PLACEHOLDER;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (is_string($value) && preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-/', $value) === 1) {
|
||||||
|
return self::UUID_PLACEHOLDER;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fixture را میخواند و کلید `_readme` (هشدار read-only بودن فایل) را کنار میگذارد
|
||||||
|
* تا مقایسه فقط روی قرارداد انجام شود.
|
||||||
|
*
|
||||||
|
* @return array<mixed>
|
||||||
|
*/
|
||||||
|
private function fixture(string $name): array
|
||||||
|
{
|
||||||
|
$path = __DIR__ . '/fixtures/' . $name;
|
||||||
|
self::assertFileExists($path, 'fixture قرارداد وجود ندارد: ' . $name);
|
||||||
|
|
||||||
|
$decoded = json_decode(file_get_contents($path), true, flags: JSON_THROW_ON_ERROR);
|
||||||
|
unset($decoded['_readme']);
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"_readme": "READ-ONLY — قرارداد منجمد GET /api/v1/appointment-settings/month-availability/{doctorUuid}. فهرست روزها به «امروز» وابسته است و placeholder میگیرد؛ قرارداد، کلیدها و ترتیب و نوعهاست. رجوع: docs/new_feture/taskes/_shared/red-lines.md",
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"year": "<year>",
|
||||||
|
"clinic_uuid": null,
|
||||||
|
"month": "<month>",
|
||||||
|
"disabled_dates": "<dates>",
|
||||||
|
"enabled_dates": "<dates>",
|
||||||
|
"online_booking_enabled": true,
|
||||||
|
"booking_window": {
|
||||||
|
"value": 3,
|
||||||
|
"unit": "month"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* READ-ONLY — امضای منجمد متدهای عمومی SlotCalculatorService.
|
||||||
|
*
|
||||||
|
* افزودن پارامتر **اختیاری** جدید مجاز است و باید یک بار همینجا ثبت شود؛ تغییر نام،
|
||||||
|
* تغییر نوع، تغییر ترتیب یا حذف پارامتر موجود ممنوع است.
|
||||||
|
* رجوع: docs/new_feture/taskes/_shared/red-lines.md
|
||||||
|
*/
|
||||||
|
return [
|
||||||
|
'explainEmptyDay' => [
|
||||||
|
'returns' => '?string',
|
||||||
|
'params' => [
|
||||||
|
'doctor' => 'App\Doctor\Entity\Doctor',
|
||||||
|
'date' => 'string',
|
||||||
|
'clinic' => '?App\Clinic\Entity\Clinic = NULL',
|
||||||
|
'forManagement' => 'bool = false',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'findNextAvailableStart' => [
|
||||||
|
'returns' => '?int',
|
||||||
|
'params' => [
|
||||||
|
'doctor' => 'App\Doctor\Entity\Doctor',
|
||||||
|
'clinic' => '?App\Clinic\Entity\Clinic = NULL',
|
||||||
|
'daysAhead' => 'int = 30',
|
||||||
|
'forManagement' => 'bool = false',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'getAllSlotsWithAvailability' => [
|
||||||
|
'returns' => 'array',
|
||||||
|
'params' => [
|
||||||
|
'doctor' => 'App\Doctor\Entity\Doctor',
|
||||||
|
'date' => 'string',
|
||||||
|
'clinic' => '?App\Clinic\Entity\Clinic = NULL',
|
||||||
|
'forManagement' => 'bool = false',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'getAvailableSlots' => [
|
||||||
|
'returns' => 'array',
|
||||||
|
'params' => [
|
||||||
|
'doctor' => 'App\Doctor\Entity\Doctor',
|
||||||
|
'date' => 'string',
|
||||||
|
'clinic' => '?App\Clinic\Entity\Clinic = NULL',
|
||||||
|
'forManagement' => 'bool = false',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'getServiceStartTimes' => [
|
||||||
|
'returns' => 'array',
|
||||||
|
'params' => [
|
||||||
|
'doctor' => 'App\Doctor\Entity\Doctor',
|
||||||
|
'date' => 'string',
|
||||||
|
'durationMinutes' => 'int',
|
||||||
|
'clinic' => '?App\Clinic\Entity\Clinic = NULL',
|
||||||
|
'forManagement' => 'bool = false',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'hasAnyAvailability' => [
|
||||||
|
'returns' => 'bool',
|
||||||
|
'params' => [
|
||||||
|
'doctor' => 'App\Doctor\Entity\Doctor',
|
||||||
|
'date' => 'string',
|
||||||
|
'clinic' => '?App\Clinic\Entity\Clinic = NULL',
|
||||||
|
'forManagement' => 'bool = false',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'resolveSlotLocationId' => [
|
||||||
|
'returns' => '?int',
|
||||||
|
'params' => [
|
||||||
|
'doctor' => 'App\Doctor\Entity\Doctor',
|
||||||
|
'slotStart' => 'int',
|
||||||
|
'clinic' => '?App\Clinic\Entity\Clinic = NULL',
|
||||||
|
'forManagement' => 'bool = false',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"_readme": "READ-ONLY — قرارداد منجمد GET /api/v1/appointment-slots. هیچ تسکی از فاز موتور چندمنبعی اجازهٔ بهروزرسانی این فایل را ندارد. اگر SlotModeFrozenTest قرمز شد، کد باید برگردد نه این فایل. رجوع: docs/new_feture/taskes/_shared/red-lines.md",
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"doctor_uuid": "<uuid>",
|
||||||
|
"clinic_uuid": null,
|
||||||
|
"date": "<date>",
|
||||||
|
"sessions": [
|
||||||
|
{
|
||||||
|
"start_time": "09:00",
|
||||||
|
"end_time": "11:00",
|
||||||
|
"slots": [
|
||||||
|
{ "start": "<epoch>", "end": "<epoch>", "start_time": "09:00", "end_time": "09:30", "location_id": 1, "is_available": true },
|
||||||
|
{ "start": "<epoch>", "end": "<epoch>", "start_time": "09:30", "end_time": "10:00", "location_id": 1, "is_available": true },
|
||||||
|
{ "start": "<epoch>", "end": "<epoch>", "start_time": "10:00", "end_time": "10:30", "location_id": 1, "is_available": true },
|
||||||
|
{ "start": "<epoch>", "end": "<epoch>", "start_time": "10:30", "end_time": "11:00", "location_id": 1, "is_available": true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"empty_reason": null
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user