The no-show records existed and drove the risk tag, but the patient's file
never showed the number behind it — the operator saw a tag with no evidence.
GET /patient/{uuid}/no-shows returns the count, the policy threshold and the
window, and the banner shows it only when the count is above zero: "0 no-shows"
on every healthy patient's file is an accusation nobody made.
The badge does not block anything and the docs say so. Blocking is an
eligibility policy from task 09 built on the same tag; a clinic that wants to
see the risk but still take a deposit must not have to switch the count off.
A test pins that a tagged patient still books.
Both report pages kept their range and branch in local state, so going back
from a resource lost the report and a shared link opened someone else's
default. They use useUrlState now, like every other list in the panel.
Three tests that were owed:
- the service-level cancellation policy beats the tenant one with no blending,
checked through the number that comes out rather than through the resolver
- a patient over the no-show threshold can still book
- occupied includes the waiting segment while active does not — if those two
came back equal the whole utilization report would be pointless
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
8.2 KiB
Cancellation — سیاست لغو، جریمه و عدم حضور
اندپوینتهای src/Cancellation/*. مستند بند ۱۱: «هر کلینیک تنظیم میکند تا چند ساعت قبل
لغو رایگان است، جریمه چقدر است، بیعانه برمیگردد یا نه، و بعد از چند بار عدم حضور بیمار
پرریسک علامت بخورد.»
همهٔ مسیرها IS_AUTHENTICATED_FULLY میخواهند و به محیط جاری محدودند (404 برای محیط دیگر).
دو قاعدهای که شکستنشان گران است
۱. لغو توسط کلینیک هرگز جریمه ندارد. این شرط اولین خط محاسبه است، نه جایی وسط آن —
اگر بعد از بررسی پنجرهٔ زمانی میآمد، یک refactor میتوانست ترتیب را عوض کند و کلینیک
از بیمار برای لغو خودش جریمه بگیرد.
۲. جریمه هرگز از مبلغ پرداختی بیشتر نمیشود. جریمهٔ بیشتر یعنی بدهی، و بدهی مسئلهٔ
صورتحساب است نه لغو. برای نوبت نقدی (پرداختی صفر) جریمه صفر میشود و پاسخ توضیحش را
در notes میدهد.
پیشفرض بدون جریمه است. اگر پیشفرض جریمهدار بود، لحظهٔ deploy همهٔ بیماران با نوبت نزدیک مشمول جریمه میشدند و کلینیک خبر نداشت. فعالکردن جریمه یک تصمیم کسبوکاری صریح است.
GET /api/v1/cancellation-policy
{
"success": true,
"data": {
"default": {
"uuid": "…",
"service_uuid": null,
"free_window_hours": 24,
"penalty_mode": "percent",
"penalty_value": 50,
"deposit_refundable": false,
"credit_refundable": true,
"no_show_threshold": 3,
"risk_tag_uuid": null,
"active": true,
"created_at": 1785486000
},
"overrides": []
}
}
PUT /api/v1/cancellation-policy · PUT /api/v1/service-item/{uuid}/cancellation-policy
| Field | Type | Description |
|---|---|---|
free_window_hours |
int | تا چند ساعت قبل، لغو رایگان است |
penalty_mode |
string | none | percent | fixed |
penalty_value |
int | درصد ۰..۱۰۰ یا مبلغ ریالی |
deposit_refundable |
bool | پس از پنجرهٔ رایگان |
credit_refundable |
bool | اعتبار پکیج (تسک ۱۱) |
no_show_threshold |
int | بعد از چند بار عدم حضور، برچسب پرریسک |
risk_tag_uuid |
string|null | برچسبی از tenant_tags |
active |
bool |
سیاستِ سرویس و سیاستِ محیط ترکیب نمیشوند: اگر سرویس سیاست فعال دارد، همان کامل برنده است. «۲۴ ساعت از محیط ولی ۵۰٪ از سرویس» چیزی است که هیچ اپراتوری نمیتواند در ذهنش شبیهسازی کند.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_VALIDATION_001 |
422 | درصد بیرون بازهٔ ۰..۱۰۰ یا حالت جریمهٔ ناشناخته |
GET /api/v1/appointment/{uuid}/cancellation-preview
| Query | Type | Description |
|---|---|---|
by |
string | doctor برای لغو از سمت کلینیک؛ پیشفرض بیمار |
{
"success": true,
"data": {
"penalty_rials": 2000000,
"deposit_refundable": false,
"credit_refundable": true,
"within_free_window": false,
"notes": [],
"paid_rials": 4000000
}
}
همان محاسبهای که خودِ لغو انجام میدهد — بیمار نباید عددی ببیند که با آنچه کسر میشود فرق دارد.
POST /api/v1/appointment/{uuid}/cancel
{ "by": "doctor", "reason": "بیمار تماس گرفت" }
by اختیاری است؛ نبودنش یعنی لغو از سمت بیمار. reason هم اختیاری است و روی تایملاین
نوبت (AppointmentEvent) مینشیند — همان چیزی که اپراتور در صفحهٔ نوبت میبیند. بدون آن،
لغو از مسیر سیاست هیچ ردی در تاریخچهٔ نوبت نمیگذاشت.
پنل این اندپوینت را از دکمهٔ «لغو نوبت» صدا میزند و پیش از تأیید نتیجهٔ
cancellation-preview را نشان میدهد؛ عددی که اپراتور میبیند همان است که کسر میشود.
Response 200
{
"success": true,
"data": {
"appointment_uuid": "…",
"status": "cancelled_by_user",
"released_resources": 3,
"waitlist_notified": 2,
"penalty_charged": true,
"penalty_rials": 1000000,
"deposit_refundable": false,
"credit_refundable": true,
"within_free_window": false,
"notes": []
}
}
ترتیب کارها: اعتبارسنجی → آزادسازی ظرفیت و بازگشت اعتبار → کسر جریمه → اطلاع به لیست انتظار. اگر اطلاعرسانی اول بود، ده نفر برای ظرفیتی خبر میشدند که هنوز آزاد نشده.
موجودی ناکافی لغو را شکست نمیدهد: penalty_charged: false برمیگردد ولی نوبت آزاد
میشود. نوبت نباید گروگان پول بماند.
سیاستی که credit_refundable: false دارد، اعتبارِ برگشتهٔ پکیج را با یک ردیف
adjustment منفی پس میگیرد — ردیف refund حذف نمیشود، چون دفتر append-only است.
Errors
| Code | HTTP | Description |
|---|---|---|
ERR_SLOT_TAKEN |
409 | نوبت قبلاً لغو شده |
ERR_VALIDATION_001 |
422 | نوبت گذشته — برای گذشته no_show یا completed معنا دارد |
GET /api/v1/patient/{uuid}/no-shows
خلاصهٔ عدم حضور یک بیمار — همان چیزی که پروندهٔ او نشان میدهد.
{ "success": true, "data": { "count": 3, "threshold": 3, "window_days": 365, "at_risk": true } }
at_risk یک نشانه است، نه مانع. مسدودسازی کارِ قانون eligibility تسک ۰۹ روی همین
برچسب است؛ کلینیکی که میخواهد بیمار پرریسک را ببیند ولی بیعانه بگیرد، نباید مجبور شود
این شمارش را خاموش کند. تست testATaggedPatientCanStillBook همین را پین میکند.
پنل نشان را فقط وقتی میآورد که count > 0 باشد — «۰ غیبت» روی پروندهٔ هر بیمار سالم،
اتهام بیجاست.
۴۰۴ برای بیمار محیط دیگر.
POST /api/v1/appointment/{uuid}/no-show
{
"success": true,
"data": { "recorded": true, "count": 3, "threshold": 3, "tagged": true }
}
هر عدم حضور یک ردیف است نه یک شمارنده: شمارنده «چه زمانی و کدام نوبت» را از دست میدهد و پنجرهٔ ۱۲ ماهه را غیرقابل محاسبه میکند. بیماری که سه سال پیش سه بار نیامده، امروز پرریسک نیست.
ثبت دوباره روی همان نوبت recorded: false میدهد و شمارش را بالا نمیبرد.
⚠️ برچسب پرریسک مسدود نمیکند. مسدودسازی یک قانون eligibility (تسک ۰۹) روی همین
برچسب است. تفکیکش عمدی است: کلینیکی که میخواهد بیمار پرریسک را ببیند ولی بیعانه بگیرد،
نباید مجبور شود برچسب را خاموش کند.
طبقهبندی محیط
| جدول | وضعیت |
|---|---|
cancellation_policies · no_show_records |
جفت محیط |
تراکنش جریمه در کیف پول با entity_type/entity_id نوبت ثبت میشود، وگرنه کلینیک الف
جریمهٔ ثبتشده در کلینیک ب را میدید.
تستها
ddev exec php bin/phpunit tests/Cancellation # ۱۴ تست